1use std::{
2 collections::HashMap,
3 fs::File,
4 path::{Path, PathBuf},
5 sync::Arc,
6};
7
8use anyhow::{anyhow, Context};
9use chrono::Utc;
10use clap::Parser;
11use slog::{debug, warn, Logger};
12
13use mithril_client::{
14 cardano_database_client::{CardanoDatabaseClient, DownloadUnpackOptions, ImmutableFileRange},
15 common::{ImmutableFileNumber, MKProof, ProtocolMessage},
16 CardanoDatabaseSnapshot, Client, MessageBuilder, MithrilCertificate, MithrilResult,
17};
18
19use crate::{
20 commands::{client_builder, SharedArgs},
21 configuration::{ConfigError, ConfigParameters, ConfigSource},
22 utils::{
23 self, AncillaryLogMessage, CardanoDbDownloadChecker, CardanoDbUtils, ExpanderUtils,
24 IndicatifFeedbackReceiver, ProgressOutputType, ProgressPrinter,
25 },
26 CommandContext,
27};
28
29const DISK_SPACE_SAFETY_MARGIN_RATIO: f64 = 0.1;
30
31struct RestorationOptions {
32 db_dir: PathBuf,
33 immutable_file_range: ImmutableFileRange,
34 download_unpack_options: DownloadUnpackOptions,
35 disk_space_safety_margin_ratio: f64,
36}
37
38#[derive(Parser, Debug, Clone)]
40pub struct CardanoDbV2DownloadCommand {
41 #[clap(flatten)]
42 shared_args: SharedArgs,
43
44 hash: String,
48
49 #[clap(long)]
54 download_dir: Option<PathBuf>,
55
56 #[clap(long, env = "GENESIS_VERIFICATION_KEY")]
58 genesis_verification_key: Option<String>,
59
60 #[clap(long)]
64 start: Option<ImmutableFileNumber>,
65
66 #[clap(long)]
70 end: Option<ImmutableFileNumber>,
71
72 #[clap(long)]
79 include_ancillary: bool,
80
81 #[clap(long, env = "ANCILLARY_VERIFICATION_KEY")]
83 ancillary_verification_key: Option<String>,
84
85 #[clap(long)]
87 allow_override: bool,
88}
89
90impl CardanoDbV2DownloadCommand {
91 pub async fn execute(&self, context: CommandContext) -> MithrilResult<()> {
93 let params = context.config_parameters()?.add_source(self)?;
94 let prepared_command = self.prepare(¶ms)?;
95
96 prepared_command.execute(context.logger(), params).await
97 }
98
99 fn prepare(&self, params: &ConfigParameters) -> MithrilResult<PreparedCardanoDbV2Download> {
100 let ancillary_verification_key = if self.include_ancillary {
101 AncillaryLogMessage::warn_ancillary_not_signed_by_mithril();
102 Some(params.require("ancillary_verification_key")?)
103 } else {
104 AncillaryLogMessage::warn_fast_bootstrap_not_available();
105 None
106 };
107
108 Ok(PreparedCardanoDbV2Download {
109 shared_args: self.shared_args.clone(),
110 hash: self.hash.clone(),
111 download_dir: params.require("download_dir")?,
112 start: self.start,
113 end: self.end,
114 include_ancillary: self.include_ancillary,
115 ancillary_verification_key,
116 allow_override: self.allow_override,
117 })
118 }
119}
120
121#[derive(Debug, Clone)]
122struct PreparedCardanoDbV2Download {
123 shared_args: SharedArgs,
124 hash: String,
125 download_dir: String,
126 start: Option<ImmutableFileNumber>,
127 end: Option<ImmutableFileNumber>,
128 include_ancillary: bool,
129 ancillary_verification_key: Option<String>,
130 allow_override: bool,
131}
132
133impl PreparedCardanoDbV2Download {
134 pub async fn execute(&self, logger: &Logger, params: ConfigParameters) -> MithrilResult<()> {
135 let restoration_options = RestorationOptions {
136 db_dir: Path::new(&self.download_dir).join("db_v2"),
137 immutable_file_range: Self::immutable_file_range(self.start, self.end),
138 download_unpack_options: DownloadUnpackOptions {
139 allow_override: self.allow_override,
140 include_ancillary: self.include_ancillary,
141 ..DownloadUnpackOptions::default()
142 },
143 disk_space_safety_margin_ratio: DISK_SPACE_SAFETY_MARGIN_RATIO,
144 };
145
146 let progress_output_type = if self.is_json_output_enabled() {
147 ProgressOutputType::JsonReporter
148 } else {
149 ProgressOutputType::Tty
150 };
151 let progress_printer = ProgressPrinter::new(progress_output_type, 6);
152 let client = client_builder(¶ms)?
153 .add_feedback_receiver(Arc::new(IndicatifFeedbackReceiver::new(
154 progress_output_type,
155 logger.clone(),
156 )))
157 .set_ancillary_verification_key(self.ancillary_verification_key.clone())
158 .with_logger(logger.clone())
159 .build()?;
160
161 let get_list_of_artifact_ids = || async {
162 let cardano_db_snapshots =
163 client.cardano_database_v2().list().await.with_context(|| {
164 "Can not get the list of artifacts while retrieving the latest cardano db hash"
165 })?;
166
167 Ok(cardano_db_snapshots
168 .iter()
169 .map(|cardano_db| cardano_db.hash.to_owned())
170 .collect::<Vec<String>>())
171 };
172
173 let cardano_db_message = client
174 .cardano_database_v2()
175 .get(
176 &ExpanderUtils::expand_eventual_id_alias(&self.hash, get_list_of_artifact_ids())
177 .await?,
178 )
179 .await?
180 .with_context(|| format!("Can not get the cardano db for hash: '{}'", self.hash))?;
181
182 Self::check_local_disk_info(
183 1,
184 &progress_printer,
185 &restoration_options,
186 &cardano_db_message,
187 self.allow_override,
188 )?;
189
190 let certificate = Self::fetch_certificate_and_verifying_chain(
191 2,
192 &progress_printer,
193 &client,
194 &cardano_db_message.certificate_hash,
195 )
196 .await?;
197
198 Self::download_and_unpack_cardano_database_snapshot(
199 logger,
200 3,
201 &progress_printer,
202 client.cardano_database_v2(),
203 &cardano_db_message,
204 &restoration_options,
205 )
206 .await
207 .with_context(|| {
208 format!(
209 "Can not download and unpack cardano db snapshot for hash: '{}'",
210 self.hash
211 )
212 })?;
213
214 let merkle_proof = Self::compute_verify_merkle_proof(
215 4,
216 &progress_printer,
217 &client,
218 &certificate,
219 &cardano_db_message,
220 &restoration_options.immutable_file_range,
221 &restoration_options.db_dir,
222 )
223 .await?;
224
225 let message = Self::compute_cardano_db_snapshot_message(
226 5,
227 &progress_printer,
228 &certificate,
229 &merkle_proof,
230 )
231 .await?;
232
233 Self::verify_cardano_db_snapshot_signature(
234 logger,
235 6,
236 &progress_printer,
237 &certificate,
238 &message,
239 &cardano_db_message,
240 &restoration_options.db_dir,
241 )
242 .await?;
243
244 Self::log_download_information(
245 &restoration_options.db_dir,
246 &cardano_db_message,
247 self.is_json_output_enabled(),
248 restoration_options
249 .download_unpack_options
250 .include_ancillary,
251 )?;
252
253 Ok(())
254 }
255
256 pub fn is_json_output_enabled(&self) -> bool {
258 self.shared_args.json
259 }
260
261 fn immutable_file_range(
262 start: Option<ImmutableFileNumber>,
263 end: Option<ImmutableFileNumber>,
264 ) -> ImmutableFileRange {
265 match (start, end) {
266 (None, None) => ImmutableFileRange::Full,
267 (Some(start), None) => ImmutableFileRange::From(start),
268 (Some(start), Some(end)) => ImmutableFileRange::Range(start, end),
269 (None, Some(end)) => ImmutableFileRange::UpTo(end),
270 }
271 }
272
273 fn compute_total_immutables_restored_size(
274 cardano_db: &CardanoDatabaseSnapshot,
275 restoration_options: &RestorationOptions,
276 ) -> u64 {
277 let total_immutables_restored = restoration_options
278 .immutable_file_range
279 .length(cardano_db.beacon.immutable_file_number);
280
281 total_immutables_restored * cardano_db.immutables.average_size_uncompressed
282 }
283
284 fn add_safety_margin(size: u64, margin_ratio: f64) -> u64 {
285 (size as f64 * (1.0 + margin_ratio)) as u64
286 }
287
288 fn compute_required_disk_space_for_snapshot(
289 cardano_db: &CardanoDatabaseSnapshot,
290 restoration_options: &RestorationOptions,
291 ) -> u64 {
292 if restoration_options.immutable_file_range == ImmutableFileRange::Full {
293 cardano_db.total_db_size_uncompressed
294 } else {
295 let total_immutables_restored_size =
296 Self::compute_total_immutables_restored_size(cardano_db, restoration_options);
297
298 let mut total_size =
299 total_immutables_restored_size + cardano_db.digests.size_uncompressed;
300 if restoration_options
301 .download_unpack_options
302 .include_ancillary
303 {
304 total_size += cardano_db.ancillary.size_uncompressed;
305 }
306
307 Self::add_safety_margin(
308 total_size,
309 restoration_options.disk_space_safety_margin_ratio,
310 )
311 }
312 }
313
314 fn check_local_disk_info(
315 step_number: u16,
316 progress_printer: &ProgressPrinter,
317 restoration_options: &RestorationOptions,
318 cardano_db: &CardanoDatabaseSnapshot,
319 allow_override: bool,
320 ) -> MithrilResult<()> {
321 progress_printer.report_step(step_number, "Checking local disk info…")?;
322
323 CardanoDbDownloadChecker::ensure_dir_exist(&restoration_options.db_dir)?;
324 if let Err(e) = CardanoDbDownloadChecker::check_prerequisites_for_uncompressed_data(
325 &restoration_options.db_dir,
326 Self::compute_required_disk_space_for_snapshot(cardano_db, restoration_options),
327 allow_override,
328 ) {
329 progress_printer
330 .report_step(step_number, &CardanoDbUtils::check_disk_space_error(e)?)?;
331 }
332
333 Ok(())
334 }
335
336 async fn fetch_certificate_and_verifying_chain(
337 step_number: u16,
338 progress_printer: &ProgressPrinter,
339 client: &Client,
340 certificate_hash: &str,
341 ) -> MithrilResult<MithrilCertificate> {
342 progress_printer.report_step(
343 step_number,
344 "Fetching the certificate and verifying the certificate chain…",
345 )?;
346 let certificate = client
347 .certificate()
348 .verify_chain(certificate_hash)
349 .await
350 .with_context(|| {
351 format!(
352 "Can not verify the certificate chain from certificate_hash: '{certificate_hash}'"
353 )
354 })?;
355
356 Ok(certificate)
357 }
358
359 async fn download_and_unpack_cardano_database_snapshot(
360 logger: &Logger,
361 step_number: u16,
362 progress_printer: &ProgressPrinter,
363 client: Arc<CardanoDatabaseClient>,
364 cardano_database_snapshot: &CardanoDatabaseSnapshot,
365 restoration_options: &RestorationOptions,
366 ) -> MithrilResult<()> {
367 progress_printer.report_step(
368 step_number,
369 "Downloading and unpacking the cardano db snapshot",
370 )?;
371 client
372 .download_unpack(
373 cardano_database_snapshot,
374 &restoration_options.immutable_file_range,
375 &restoration_options.db_dir,
376 restoration_options.download_unpack_options,
377 )
378 .await?;
379
380 let full_restoration = restoration_options.immutable_file_range == ImmutableFileRange::Full;
383 let include_ancillary = restoration_options
384 .download_unpack_options
385 .include_ancillary;
386 let number_of_immutable_files_restored = restoration_options
387 .immutable_file_range
388 .length(cardano_database_snapshot.beacon.immutable_file_number);
389 if let Err(e) = client
390 .add_statistics(
391 full_restoration,
392 include_ancillary,
393 number_of_immutable_files_restored,
394 )
395 .await
396 {
397 warn!(
398 logger, "Could not increment cardano db snapshot download statistics";
399 "error" => ?e
400 );
401 }
402
403 if let Err(error) = File::create(restoration_options.db_dir.join("clean")) {
405 warn!(
406 logger, "Could not create clean shutdown marker file in directory '{}'", restoration_options.db_dir.display();
407 "error" => error.to_string()
408 );
409 };
410
411 Ok(())
412 }
413
414 async fn compute_verify_merkle_proof(
415 step_number: u16,
416 progress_printer: &ProgressPrinter,
417 client: &Client,
418 certificate: &MithrilCertificate,
419 cardano_database_snapshot: &CardanoDatabaseSnapshot,
420 immutable_file_range: &ImmutableFileRange,
421 unpacked_dir: &Path,
422 ) -> MithrilResult<MKProof> {
423 progress_printer.report_step(step_number, "Computing and verifying the Merkle proof…")?;
424 let merkle_proof = client
425 .cardano_database_v2()
426 .compute_merkle_proof(
427 certificate,
428 cardano_database_snapshot,
429 immutable_file_range,
430 unpacked_dir,
431 )
432 .await?;
433
434 merkle_proof
435 .verify()
436 .with_context(|| "Merkle proof verification failed")?;
437
438 Ok(merkle_proof)
439 }
440
441 async fn compute_cardano_db_snapshot_message(
442 step_number: u16,
443 progress_printer: &ProgressPrinter,
444 certificate: &MithrilCertificate,
445 merkle_proof: &MKProof,
446 ) -> MithrilResult<ProtocolMessage> {
447 progress_printer.report_step(step_number, "Computing the cardano db snapshot message")?;
448 let message = CardanoDbUtils::wait_spinner(
449 progress_printer,
450 MessageBuilder::new().compute_cardano_database_message(certificate, merkle_proof),
451 )
452 .await
453 .with_context(|| "Can not compute the cardano db snapshot message")?;
454
455 Ok(message)
456 }
457
458 async fn verify_cardano_db_snapshot_signature(
459 logger: &Logger,
460 step_number: u16,
461 progress_printer: &ProgressPrinter,
462 certificate: &MithrilCertificate,
463 message: &ProtocolMessage,
464 cardano_db_snapshot: &CardanoDatabaseSnapshot,
465 db_dir: &Path,
466 ) -> MithrilResult<()> {
467 progress_printer.report_step(step_number, "Verifying the cardano db signature…")?;
468 if !certificate.match_message(message) {
469 debug!(
470 logger,
471 "Merkle root verification failed, removing unpacked files & directory."
472 );
473
474 if let Err(error) = std::fs::remove_dir_all(db_dir) {
475 warn!(
476 logger, "Error while removing unpacked files & directory";
477 "error" => error.to_string()
478 );
479 }
480
481 return Err(anyhow!(
482 "Certificate verification failed (cardano db snapshot hash = '{}').",
483 cardano_db_snapshot.hash.clone()
484 ));
485 }
486
487 Ok(())
488 }
489
490 fn log_download_information(
491 db_dir: &Path,
492 cardano_db_snapshot: &CardanoDatabaseSnapshot,
493 json_output: bool,
494 include_ancillary: bool,
495 ) -> MithrilResult<()> {
496 let canonicalized_filepath = &db_dir.canonicalize().with_context(|| {
497 format!(
498 "Could not get canonicalized filepath of '{}'",
499 db_dir.display()
500 )
501 })?;
502
503 let docker_cmd = format!(
504 "docker run -v cardano-node-ipc:/ipc -v cardano-node-data:/data --mount type=bind,source=\"{}\",target=/data/db/ -e NETWORK={} ghcr.io/intersectmbo/cardano-node:{}",
505 canonicalized_filepath.display(),
506 cardano_db_snapshot.network,
507 cardano_db_snapshot.cardano_node_version
508 );
509
510 let snapshot_converter_cmd = |flavor| {
511 format!(
512 "mithril-client --unstable tools utxo-hd snapshot-converter --db-directory {} --cardano-node-version {} --utxo-hd-flavor {} --cardano-network {} --commit",
513 db_dir.display(),
514 cardano_db_snapshot.cardano_node_version,
515 flavor,
516 cardano_db_snapshot.network
517 )
518 };
519
520 if json_output {
521 let json = if include_ancillary {
522 serde_json::json!({
523 "timestamp": Utc::now().to_rfc3339(),
524 "db_directory": canonicalized_filepath,
525 "run_docker_cmd": docker_cmd,
526 "snapshot_converter_cmd_to_lmdb": snapshot_converter_cmd("LMDB"),
527 "snapshot_converter_cmd_to_legacy": snapshot_converter_cmd("Legacy")
528 })
529 } else {
530 serde_json::json!({
531 "timestamp": Utc::now().to_rfc3339(),
532 "db_directory": canonicalized_filepath,
533 "run_docker_cmd": docker_cmd
534 })
535 };
536
537 println!("{}", json);
538 } else {
539 let cardano_node_version = &cardano_db_snapshot.cardano_node_version;
540 println!(
541 r###"Cardano database snapshot '{}' archives have been successfully unpacked. Immutable files have been successfully checked against Mithril multi-signature contained in the certificate.
542
543 Files in the directory '{}' can be used to run a Cardano node with version >= {cardano_node_version}.
544
545 If you are using Cardano Docker image, you can restore a Cardano Node with:
546
547 {}
548
549 "###,
550 cardano_db_snapshot.hash,
551 db_dir.display(),
552 docker_cmd
553 );
554
555 if include_ancillary {
556 println!(
557 r###"Upgrade and replace the restored ledger state snapshot to 'LMDB' flavor by running the command:
558
559 {}
560
561 Or to 'Legacy' flavor by running the command:
562
563 {}
564
565 "###,
566 snapshot_converter_cmd("LMDB"),
567 snapshot_converter_cmd("Legacy"),
568 );
569 }
570 }
571
572 Ok(())
573 }
574}
575
576impl ConfigSource for CardanoDbV2DownloadCommand {
577 fn collect(&self) -> Result<HashMap<String, String>, ConfigError> {
578 let mut map = HashMap::new();
579
580 if let Some(download_dir) = self.download_dir.clone() {
581 let param = "download_dir".to_string();
582 map.insert(
583 param.clone(),
584 utils::path_to_string(&download_dir)
585 .map_err(|e| ConfigError::Conversion(param, e))?,
586 );
587 }
588
589 if let Some(genesis_verification_key) = self.genesis_verification_key.clone() {
590 map.insert(
591 "genesis_verification_key".to_string(),
592 genesis_verification_key,
593 );
594 }
595
596 if let Some(ancillary_verification_key) = self.ancillary_verification_key.clone() {
597 map.insert(
598 "ancillary_verification_key".to_string(),
599 ancillary_verification_key,
600 );
601 }
602
603 Ok(map)
604 }
605}
606
607#[cfg(test)]
608mod tests {
609 use config::ConfigBuilder;
610 use mithril_client::{
611 common::{
612 AncillaryMessagePart, CardanoDbBeacon, DigestsMessagePart, ImmutablesMessagePart,
613 ProtocolMessagePartKey, SignedEntityType,
614 },
615 MithrilCertificateMetadata,
616 };
617 use mithril_common::test_utils::TempDir;
618
619 use super::*;
620
621 fn dummy_certificate() -> MithrilCertificate {
622 let mut protocol_message = ProtocolMessage::new();
623 protocol_message.set_message_part(
624 ProtocolMessagePartKey::CardanoDatabaseMerkleRoot,
625 CardanoDatabaseSnapshot::dummy().hash.to_string(),
626 );
627 protocol_message.set_message_part(
628 ProtocolMessagePartKey::NextAggregateVerificationKey,
629 "whatever".to_string(),
630 );
631 let beacon = CardanoDbBeacon::new(10, 100);
632
633 MithrilCertificate {
634 hash: "hash".to_string(),
635 previous_hash: "previous_hash".to_string(),
636 epoch: beacon.epoch,
637 signed_entity_type: SignedEntityType::CardanoDatabase(beacon),
638 metadata: MithrilCertificateMetadata::dummy(),
639 protocol_message: protocol_message.clone(),
640 signed_message: "signed_message".to_string(),
641 aggregate_verification_key: String::new(),
642 multi_signature: String::new(),
643 genesis_signature: String::new(),
644 }
645 }
646
647 fn dummy_command() -> CardanoDbV2DownloadCommand {
648 CardanoDbV2DownloadCommand {
649 shared_args: SharedArgs { json: false },
650 hash: "whatever_hash".to_string(),
651 download_dir: Some(std::path::PathBuf::from("whatever_dir")),
652 genesis_verification_key: Some("whatever".to_string()),
653 start: None,
654 end: None,
655 include_ancillary: true,
656 ancillary_verification_key: Some("whatever".to_string()),
657 allow_override: false,
658 }
659 }
660
661 #[tokio::test]
662 async fn ancillary_verification_key_is_mandatory_when_include_ancillary_is_true() {
663 let command = CardanoDbV2DownloadCommand {
664 include_ancillary: true,
665 ancillary_verification_key: None,
666 ..dummy_command()
667 };
668 let command_context = CommandContext::new(
669 ConfigBuilder::default(),
670 false,
671 Logger::root(slog::Discard, slog::o!()),
672 );
673
674 let result = command.execute(command_context).await;
675
676 assert!(result.is_err());
677 assert_eq!(
678 result.unwrap_err().to_string(),
679 "Parameter 'ancillary_verification_key' is mandatory."
680 );
681 }
682
683 #[test]
684 fn ancillary_verification_key_can_be_read_through_configuration_file() {
685 let command = CardanoDbV2DownloadCommand {
686 ancillary_verification_key: None,
687 ..dummy_command()
688 };
689 let config = config::Config::builder()
690 .set_default("ancillary_verification_key", "value from config")
691 .expect("Failed to build config builder");
692 let command_context =
693 CommandContext::new(config, false, Logger::root(slog::Discard, slog::o!()));
694 let config_parameters = command_context
695 .config_parameters()
696 .unwrap()
697 .add_source(&command)
698 .unwrap();
699
700 let result = command.prepare(&config_parameters);
701
702 assert!(result.is_ok());
703 }
704
705 #[test]
706 fn db_download_dir_is_mandatory_to_execute_command() {
707 let command = CardanoDbV2DownloadCommand {
708 download_dir: None,
709 ..dummy_command()
710 };
711 let command_context = CommandContext::new(
712 ConfigBuilder::default(),
713 false,
714 Logger::root(slog::Discard, slog::o!()),
715 );
716 let config_parameters = command_context
717 .config_parameters()
718 .unwrap()
719 .add_source(&command)
720 .unwrap();
721
722 let result = command.prepare(&config_parameters);
723
724 assert!(result.is_err());
725 assert_eq!(
726 result.unwrap_err().to_string(),
727 "Parameter 'download_dir' is mandatory."
728 );
729 }
730
731 #[tokio::test]
732 async fn verify_cardano_db_snapshot_signature_should_remove_db_dir_if_messages_mismatch() {
733 let progress_printer = ProgressPrinter::new(ProgressOutputType::Tty, 1);
734 let certificate = dummy_certificate();
735 let mut message = ProtocolMessage::new();
736 message.set_message_part(
737 ProtocolMessagePartKey::CardanoDatabaseMerkleRoot,
738 "merkle-root-123456".to_string(),
739 );
740 message.set_message_part(
741 ProtocolMessagePartKey::NextAggregateVerificationKey,
742 "avk-123456".to_string(),
743 );
744 let cardano_db = CardanoDatabaseSnapshot::dummy();
745 let db_dir = TempDir::create(
746 "client-cli",
747 "verify_cardano_db_snapshot_signature_should_remove_db_dir_if_messages_mismatch",
748 );
749
750 let result = PreparedCardanoDbV2Download::verify_cardano_db_snapshot_signature(
751 &Logger::root(slog::Discard, slog::o!()),
752 1,
753 &progress_printer,
754 &certificate,
755 &message,
756 &cardano_db,
757 &db_dir,
758 )
759 .await;
760
761 assert!(result.is_err());
762 assert!(
763 !db_dir.exists(),
764 "The db directory should have been removed but it still exists"
765 );
766 }
767
768 #[test]
769 fn immutable_file_range_without_start_without_end_returns_variant_full() {
770 let range = PreparedCardanoDbV2Download::immutable_file_range(None, None);
771
772 assert_eq!(range, ImmutableFileRange::Full);
773 }
774
775 #[test]
776 fn immutable_file_range_with_start_without_end_returns_variant_from() {
777 let start = Some(12);
778
779 let range = PreparedCardanoDbV2Download::immutable_file_range(start, None);
780
781 assert_eq!(range, ImmutableFileRange::From(12));
782 }
783
784 #[test]
785 fn immutable_file_range_with_start_with_end_returns_variant_range() {
786 let start = Some(12);
787 let end = Some(345);
788
789 let range = PreparedCardanoDbV2Download::immutable_file_range(start, end);
790
791 assert_eq!(range, ImmutableFileRange::Range(12, 345));
792 }
793
794 #[test]
795 fn immutable_file_range_without_start_with_end_returns_variant_up_to() {
796 let end = Some(345);
797
798 let range = PreparedCardanoDbV2Download::immutable_file_range(None, end);
799
800 assert_eq!(range, ImmutableFileRange::UpTo(345));
801 }
802
803 #[test]
804 fn compute_required_disk_space_for_snapshot_when_full_restoration() {
805 let cardano_db_snapshot = CardanoDatabaseSnapshot {
806 total_db_size_uncompressed: 123,
807 ..CardanoDatabaseSnapshot::dummy()
808 };
809 let restoration_options = RestorationOptions {
810 immutable_file_range: ImmutableFileRange::Full,
811 db_dir: PathBuf::from("db_dir"),
812 download_unpack_options: DownloadUnpackOptions::default(),
813 disk_space_safety_margin_ratio: 0.0,
814 };
815
816 let required_size = PreparedCardanoDbV2Download::compute_required_disk_space_for_snapshot(
817 &cardano_db_snapshot,
818 &restoration_options,
819 );
820
821 assert_eq!(required_size, 123);
822 }
823
824 #[test]
825 fn compute_required_disk_space_for_snapshot_when_partial_restoration_and_no_ancillary_files() {
826 let cardano_db_snapshot = CardanoDatabaseSnapshot {
827 digests: DigestsMessagePart {
828 size_uncompressed: 50,
829 locations: vec![],
830 },
831 immutables: ImmutablesMessagePart {
832 average_size_uncompressed: 100,
833 locations: vec![],
834 },
835 ancillary: AncillaryMessagePart {
836 size_uncompressed: 300,
837 locations: vec![],
838 },
839 ..CardanoDatabaseSnapshot::dummy()
840 };
841 let restoration_options = RestorationOptions {
842 immutable_file_range: ImmutableFileRange::Range(10, 19),
843 db_dir: PathBuf::from("db_dir"),
844 download_unpack_options: DownloadUnpackOptions {
845 include_ancillary: false,
846 ..DownloadUnpackOptions::default()
847 },
848 disk_space_safety_margin_ratio: 0.0,
849 };
850
851 let required_size = PreparedCardanoDbV2Download::compute_required_disk_space_for_snapshot(
852 &cardano_db_snapshot,
853 &restoration_options,
854 );
855
856 let digest_size = cardano_db_snapshot.digests.size_uncompressed;
857 let average_size_uncompressed_immutable =
858 cardano_db_snapshot.immutables.average_size_uncompressed;
859
860 let expected_size = digest_size + 10 * average_size_uncompressed_immutable;
861 assert_eq!(required_size, expected_size);
862 }
863
864 #[test]
865 fn compute_required_disk_space_for_snapshot_when_partial_restoration_and_ancillary_files() {
866 let cardano_db_snapshot = CardanoDatabaseSnapshot {
867 digests: DigestsMessagePart {
868 size_uncompressed: 50,
869 locations: vec![],
870 },
871 immutables: ImmutablesMessagePart {
872 average_size_uncompressed: 100,
873 locations: vec![],
874 },
875 ancillary: AncillaryMessagePart {
876 size_uncompressed: 300,
877 locations: vec![],
878 },
879 ..CardanoDatabaseSnapshot::dummy()
880 };
881 let restoration_options = RestorationOptions {
882 immutable_file_range: ImmutableFileRange::Range(10, 19),
883 db_dir: PathBuf::from("db_dir"),
884 download_unpack_options: DownloadUnpackOptions {
885 include_ancillary: true,
886 ..DownloadUnpackOptions::default()
887 },
888 disk_space_safety_margin_ratio: 0.0,
889 };
890
891 let required_size = PreparedCardanoDbV2Download::compute_required_disk_space_for_snapshot(
892 &cardano_db_snapshot,
893 &restoration_options,
894 );
895
896 let digest_size = cardano_db_snapshot.digests.size_uncompressed;
897 let average_size_uncompressed_immutable =
898 cardano_db_snapshot.immutables.average_size_uncompressed;
899 let ancillary_size = cardano_db_snapshot.ancillary.size_uncompressed;
900
901 let expected_size = digest_size + 10 * average_size_uncompressed_immutable + ancillary_size;
902 assert_eq!(required_size, expected_size);
903 }
904
905 #[test]
906 fn add_safety_margin_apply_margin_with_ratio() {
907 assert_eq!(
908 PreparedCardanoDbV2Download::add_safety_margin(100, 0.1),
909 110
910 );
911 assert_eq!(
912 PreparedCardanoDbV2Download::add_safety_margin(100, 0.5),
913 150
914 );
915 assert_eq!(
916 PreparedCardanoDbV2Download::add_safety_margin(100, 1.5),
917 250
918 );
919
920 assert_eq!(PreparedCardanoDbV2Download::add_safety_margin(0, 0.1), 0);
921
922 assert_eq!(
923 PreparedCardanoDbV2Download::add_safety_margin(100, 0.0),
924 100
925 );
926 }
927}