mithril_client_cli/commands/cardano_db_v2/
download.rs

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, ConfigSource},
22    utils::{
23        self, CardanoDbDownloadChecker, CardanoDbUtils, ExpanderUtils, IndicatifFeedbackReceiver,
24        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/// Clap command to download a Cardano db and verify its associated certificate.
39#[derive(Parser, Debug, Clone)]
40pub struct CardanoDbV2DownloadCommand {
41    #[clap(flatten)]
42    shared_args: SharedArgs,
43
44    /// Hash of the Cardano db snapshot to download  or `latest` for the latest artifact
45    ///
46    /// Use the `list` command to get that information.
47    hash: String,
48
49    /// Directory where the immutable and ancillary files will be downloaded.
50    ///
51    /// By default, a subdirectory will be created in this directory to extract and verify the
52    /// certificate.
53    #[clap(long)]
54    download_dir: Option<PathBuf>,
55
56    /// Genesis verification key to check the certificate chain.
57    #[clap(long, env = "GENESIS_VERIFICATION_KEY")]
58    genesis_verification_key: Option<String>,
59
60    /// The first immutable file number to download.
61    ///
62    /// If not set, the download process will start from the first immutable file.
63    #[clap(long)]
64    start: Option<ImmutableFileNumber>,
65
66    /// The last immutable file number to download.
67    ///
68    /// If not set, the download will continue until the last certified immutable file.
69    #[clap(long)]
70    end: Option<ImmutableFileNumber>,
71
72    /// Include ancillary files in the download, if set the `ancillary_verification_key` is required
73    /// in order to verify the ancillary files.
74    ///
75    /// By default, only finalized immutable files are downloaded.
76    /// The last ledger state snapshot and the last immutable file (the ancillary files) can be
77    /// downloaded with this option.
78    #[clap(long, requires = "ancillary_verification_key")]
79    include_ancillary: bool,
80
81    /// Ancillary verification key to verify the ancillary files.
82    #[clap(long, env = "ANCILLARY_VERIFICATION_KEY")]
83    ancillary_verification_key: Option<String>,
84
85    /// Allow existing files in the download directory to be overridden.
86    #[clap(long)]
87    allow_override: bool,
88}
89
90impl CardanoDbV2DownloadCommand {
91    /// Is JSON output enabled
92    pub fn is_json_output_enabled(&self) -> bool {
93        self.shared_args.json
94    }
95
96    fn immutable_file_range(
97        start: Option<ImmutableFileNumber>,
98        end: Option<ImmutableFileNumber>,
99    ) -> ImmutableFileRange {
100        match (start, end) {
101            (None, None) => ImmutableFileRange::Full,
102            (Some(start), None) => ImmutableFileRange::From(start),
103            (Some(start), Some(end)) => ImmutableFileRange::Range(start, end),
104            (None, Some(end)) => ImmutableFileRange::UpTo(end),
105        }
106    }
107
108    /// Command execution
109    pub async fn execute(&self, context: CommandContext) -> MithrilResult<()> {
110        let params = context.config_parameters()?.add_source(self)?;
111        let download_dir: &String = &params.require("download_dir")?;
112        let restoration_options = RestorationOptions {
113            db_dir: Path::new(download_dir).join("db_v2"),
114            immutable_file_range: Self::immutable_file_range(self.start, self.end),
115            download_unpack_options: DownloadUnpackOptions {
116                allow_override: self.allow_override,
117                include_ancillary: self.include_ancillary,
118                ..DownloadUnpackOptions::default()
119            },
120            disk_space_safety_margin_ratio: DISK_SPACE_SAFETY_MARGIN_RATIO,
121        };
122        let logger = context.logger();
123
124        let progress_output_type = if self.is_json_output_enabled() {
125            ProgressOutputType::JsonReporter
126        } else {
127            ProgressOutputType::Tty
128        };
129        let progress_printer = ProgressPrinter::new(progress_output_type, 6);
130        let client = client_builder(&params)?
131            .add_feedback_receiver(Arc::new(IndicatifFeedbackReceiver::new(
132                progress_output_type,
133                logger.clone(),
134            )))
135            .set_ancillary_verification_key(self.ancillary_verification_key.clone())
136            .with_logger(logger.clone())
137            .build()?;
138
139        let get_list_of_artifact_ids = || async {
140            let cardano_db_snapshots =
141                client.cardano_database_v2().list().await.with_context(|| {
142                    "Can not get the list of artifacts while retrieving the latest cardano db hash"
143                })?;
144
145            Ok(cardano_db_snapshots
146                .iter()
147                .map(|cardano_db| cardano_db.hash.to_owned())
148                .collect::<Vec<String>>())
149        };
150
151        let cardano_db_message = client
152            .cardano_database_v2()
153            .get(
154                &ExpanderUtils::expand_eventual_id_alias(&self.hash, get_list_of_artifact_ids())
155                    .await?,
156            )
157            .await?
158            .with_context(|| format!("Can not get the cardano db for hash: '{}'", self.hash))?;
159
160        Self::check_local_disk_info(
161            1,
162            &progress_printer,
163            &restoration_options,
164            &cardano_db_message,
165            self.allow_override,
166        )?;
167
168        let certificate = Self::fetch_certificate_and_verifying_chain(
169            2,
170            &progress_printer,
171            &client,
172            &cardano_db_message.certificate_hash,
173        )
174        .await?;
175
176        Self::download_and_unpack_cardano_database_snapshot(
177            logger,
178            3,
179            &progress_printer,
180            client.cardano_database_v2(),
181            &cardano_db_message,
182            &restoration_options,
183        )
184        .await
185        .with_context(|| {
186            format!(
187                "Can not download and unpack cardano db snapshot for hash: '{}'",
188                self.hash
189            )
190        })?;
191
192        let merkle_proof = Self::compute_verify_merkle_proof(
193            4,
194            &progress_printer,
195            &client,
196            &certificate,
197            &cardano_db_message,
198            &restoration_options.immutable_file_range,
199            &restoration_options.db_dir,
200        )
201        .await?;
202
203        let message = Self::compute_cardano_db_snapshot_message(
204            5,
205            &progress_printer,
206            &certificate,
207            &merkle_proof,
208        )
209        .await?;
210
211        Self::verify_cardano_db_snapshot_signature(
212            logger,
213            6,
214            &progress_printer,
215            &certificate,
216            &message,
217            &cardano_db_message,
218            &restoration_options.db_dir,
219        )
220        .await?;
221
222        Self::log_download_information(
223            &restoration_options.db_dir,
224            &cardano_db_message,
225            self.is_json_output_enabled(),
226        )?;
227
228        Ok(())
229    }
230
231    fn compute_total_immutables_restored_size(
232        cardano_db: &CardanoDatabaseSnapshot,
233        restoration_options: &RestorationOptions,
234    ) -> u64 {
235        let total_immutables_restored = restoration_options
236            .immutable_file_range
237            .length(cardano_db.beacon.immutable_file_number);
238
239        total_immutables_restored * cardano_db.immutables.average_size_uncompressed
240    }
241
242    fn add_safety_margin(size: u64, margin_ratio: f64) -> u64 {
243        (size as f64 * (1.0 + margin_ratio)) as u64
244    }
245
246    fn compute_required_disk_space_for_snapshot(
247        cardano_db: &CardanoDatabaseSnapshot,
248        restoration_options: &RestorationOptions,
249    ) -> u64 {
250        if restoration_options.immutable_file_range == ImmutableFileRange::Full {
251            cardano_db.total_db_size_uncompressed
252        } else {
253            let total_immutables_restored_size =
254                Self::compute_total_immutables_restored_size(cardano_db, restoration_options);
255
256            let mut total_size =
257                total_immutables_restored_size + cardano_db.digests.size_uncompressed;
258            if restoration_options
259                .download_unpack_options
260                .include_ancillary
261            {
262                total_size += cardano_db.ancillary.size_uncompressed;
263            }
264
265            Self::add_safety_margin(
266                total_size,
267                restoration_options.disk_space_safety_margin_ratio,
268            )
269        }
270    }
271
272    fn check_local_disk_info(
273        step_number: u16,
274        progress_printer: &ProgressPrinter,
275        restoration_options: &RestorationOptions,
276        cardano_db: &CardanoDatabaseSnapshot,
277        allow_override: bool,
278    ) -> MithrilResult<()> {
279        progress_printer.report_step(step_number, "Checking local disk info…")?;
280
281        CardanoDbDownloadChecker::ensure_dir_exist(&restoration_options.db_dir)?;
282        if let Err(e) = CardanoDbDownloadChecker::check_prerequisites_for_uncompressed_data(
283            &restoration_options.db_dir,
284            Self::compute_required_disk_space_for_snapshot(cardano_db, restoration_options),
285            allow_override,
286        ) {
287            progress_printer
288                .report_step(step_number, &CardanoDbUtils::check_disk_space_error(e)?)?;
289        }
290
291        Ok(())
292    }
293
294    async fn fetch_certificate_and_verifying_chain(
295        step_number: u16,
296        progress_printer: &ProgressPrinter,
297        client: &Client,
298        certificate_hash: &str,
299    ) -> MithrilResult<MithrilCertificate> {
300        progress_printer.report_step(
301            step_number,
302            "Fetching the certificate and verifying the certificate chain…",
303        )?;
304        let certificate = client
305            .certificate()
306            .verify_chain(certificate_hash)
307            .await
308            .with_context(|| {
309                format!(
310                    "Can not verify the certificate chain from certificate_hash: '{}'",
311                    certificate_hash
312                )
313            })?;
314
315        Ok(certificate)
316    }
317
318    async fn download_and_unpack_cardano_database_snapshot(
319        logger: &Logger,
320        step_number: u16,
321        progress_printer: &ProgressPrinter,
322        client: Arc<CardanoDatabaseClient>,
323        cardano_database_snapshot: &CardanoDatabaseSnapshot,
324        restoration_options: &RestorationOptions,
325    ) -> MithrilResult<()> {
326        progress_printer.report_step(
327            step_number,
328            "Downloading and unpacking the cardano db snapshot",
329        )?;
330        client
331            .download_unpack(
332                cardano_database_snapshot,
333                &restoration_options.immutable_file_range,
334                &restoration_options.db_dir,
335                restoration_options.download_unpack_options,
336            )
337            .await?;
338
339        // The cardano db snapshot download does not fail if the statistic call fails.
340        // It would be nice to implement tests to verify the behavior of `add_statistics`
341        let full_restoration = restoration_options.immutable_file_range == ImmutableFileRange::Full;
342        let include_ancillary = restoration_options
343            .download_unpack_options
344            .include_ancillary;
345        let number_of_immutable_files_restored = restoration_options
346            .immutable_file_range
347            .length(cardano_database_snapshot.beacon.immutable_file_number);
348        if let Err(e) = client
349            .add_statistics(
350                full_restoration,
351                include_ancillary,
352                number_of_immutable_files_restored,
353            )
354            .await
355        {
356            warn!(
357                logger, "Could not increment cardano db snapshot download statistics";
358                "error" => ?e
359            );
360        }
361
362        // Append 'clean' file to speedup node bootstrap
363        if let Err(error) = File::create(restoration_options.db_dir.join("clean")) {
364            warn!(
365                logger, "Could not create clean shutdown marker file in directory '{}'", restoration_options.db_dir.display();
366                "error" => error.to_string()
367            );
368        };
369
370        Ok(())
371    }
372
373    async fn compute_verify_merkle_proof(
374        step_number: u16,
375        progress_printer: &ProgressPrinter,
376        client: &Client,
377        certificate: &MithrilCertificate,
378        cardano_database_snapshot: &CardanoDatabaseSnapshot,
379        immutable_file_range: &ImmutableFileRange,
380        unpacked_dir: &Path,
381    ) -> MithrilResult<MKProof> {
382        progress_printer.report_step(step_number, "Computing and verifying the Merkle proof…")?;
383        let merkle_proof = client
384            .cardano_database_v2()
385            .compute_merkle_proof(
386                certificate,
387                cardano_database_snapshot,
388                immutable_file_range,
389                unpacked_dir,
390            )
391            .await?;
392
393        merkle_proof
394            .verify()
395            .with_context(|| "Merkle proof verification failed")?;
396
397        Ok(merkle_proof)
398    }
399
400    async fn compute_cardano_db_snapshot_message(
401        step_number: u16,
402        progress_printer: &ProgressPrinter,
403        certificate: &MithrilCertificate,
404        merkle_proof: &MKProof,
405    ) -> MithrilResult<ProtocolMessage> {
406        progress_printer.report_step(step_number, "Computing the cardano db snapshot message")?;
407        let message = CardanoDbUtils::wait_spinner(
408            progress_printer,
409            MessageBuilder::new().compute_cardano_database_message(certificate, merkle_proof),
410        )
411        .await
412        .with_context(|| "Can not compute the cardano db snapshot message")?;
413
414        Ok(message)
415    }
416
417    async fn verify_cardano_db_snapshot_signature(
418        logger: &Logger,
419        step_number: u16,
420        progress_printer: &ProgressPrinter,
421        certificate: &MithrilCertificate,
422        message: &ProtocolMessage,
423        cardano_db_snapshot: &CardanoDatabaseSnapshot,
424        db_dir: &Path,
425    ) -> MithrilResult<()> {
426        progress_printer.report_step(step_number, "Verifying the cardano db signature…")?;
427        if !certificate.match_message(message) {
428            debug!(
429                logger,
430                "Merkle root verification failed, removing unpacked files & directory."
431            );
432
433            if let Err(error) = std::fs::remove_dir_all(db_dir) {
434                warn!(
435                    logger, "Error while removing unpacked files & directory";
436                    "error" => error.to_string()
437                );
438            }
439
440            return Err(anyhow!(
441                "Certificate verification failed (cardano db snapshot hash = '{}').",
442                cardano_db_snapshot.hash.clone()
443            ));
444        }
445
446        Ok(())
447    }
448
449    fn log_download_information(
450        db_dir: &Path,
451        cardano_db_snapshot: &CardanoDatabaseSnapshot,
452        json_output: bool,
453    ) -> MithrilResult<()> {
454        let canonicalized_filepath = &db_dir.canonicalize().with_context(|| {
455            format!(
456                "Could not get canonicalized filepath of '{}'",
457                db_dir.display()
458            )
459        })?;
460
461        if json_output {
462            println!(
463                r#"{{"timestamp": "{}", "db_directory": "{}"}}"#,
464                Utc::now().to_rfc3339(),
465                canonicalized_filepath.display()
466            );
467        } else {
468            let cardano_node_version = &cardano_db_snapshot.cardano_node_version;
469            println!(
470                r###"Cardano database snapshot '{}' archives have been successfully unpacked. Immutable files have been successfully checked against Mithril multi-signature contained in the certificate.
471                    
472    Files in the directory '{}' can be used to run a Cardano node with version >= {cardano_node_version}.
473
474    If you are using Cardano Docker image, you can restore a Cardano Node with:
475    
476    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:{cardano_node_version}
477    
478    "###,
479                cardano_db_snapshot.hash,
480                db_dir.display(),
481                canonicalized_filepath.display(),
482                cardano_db_snapshot.network,
483            );
484        }
485
486        Ok(())
487    }
488}
489
490impl ConfigSource for CardanoDbV2DownloadCommand {
491    fn collect(&self) -> Result<HashMap<String, String>, ConfigError> {
492        let mut map = HashMap::new();
493
494        if let Some(download_dir) = self.download_dir.clone() {
495            let param = "download_dir".to_string();
496            map.insert(
497                param.clone(),
498                utils::path_to_string(&download_dir)
499                    .map_err(|e| ConfigError::Conversion(param, e))?,
500            );
501        }
502
503        if let Some(genesis_verification_key) = self.genesis_verification_key.clone() {
504            map.insert(
505                "genesis_verification_key".to_string(),
506                genesis_verification_key,
507            );
508        }
509
510        Ok(map)
511    }
512}
513
514#[cfg(test)]
515mod tests {
516    use mithril_client::{
517        common::{
518            AncillaryMessagePart, CardanoDbBeacon, DigestsMessagePart, ImmutablesMessagePart,
519            ProtocolMessagePartKey, SignedEntityType,
520        },
521        MithrilCertificateMetadata,
522    };
523    use mithril_common::test_utils::TempDir;
524
525    use super::*;
526
527    fn dummy_certificate() -> MithrilCertificate {
528        let mut protocol_message = ProtocolMessage::new();
529        protocol_message.set_message_part(
530            ProtocolMessagePartKey::CardanoDatabaseMerkleRoot,
531            CardanoDatabaseSnapshot::dummy().hash.to_string(),
532        );
533        protocol_message.set_message_part(
534            ProtocolMessagePartKey::NextAggregateVerificationKey,
535            "whatever".to_string(),
536        );
537        let beacon = CardanoDbBeacon::new(10, 100);
538
539        MithrilCertificate {
540            hash: "hash".to_string(),
541            previous_hash: "previous_hash".to_string(),
542            epoch: beacon.epoch,
543            signed_entity_type: SignedEntityType::CardanoDatabase(beacon),
544            metadata: MithrilCertificateMetadata::dummy(),
545            protocol_message: protocol_message.clone(),
546            signed_message: "signed_message".to_string(),
547            aggregate_verification_key: String::new(),
548            multi_signature: String::new(),
549            genesis_signature: String::new(),
550        }
551    }
552
553    #[test]
554    fn ancillary_verification_key_is_mandatory_when_include_ancillary_is_true() {
555        CardanoDbV2DownloadCommand::try_parse_from([
556            "cdbv2-command",
557            "--include-ancillary",
558            "whatever_hash",
559        ])
560        .expect_err("The command should fail because ancillary_verification_key is not set");
561    }
562
563    #[tokio::test]
564    async fn verify_cardano_db_snapshot_signature_should_remove_db_dir_if_messages_mismatch() {
565        let progress_printer = ProgressPrinter::new(ProgressOutputType::Tty, 1);
566        let certificate = dummy_certificate();
567        let mut message = ProtocolMessage::new();
568        message.set_message_part(
569            ProtocolMessagePartKey::CardanoDatabaseMerkleRoot,
570            "merkle-root-123456".to_string(),
571        );
572        message.set_message_part(
573            ProtocolMessagePartKey::NextAggregateVerificationKey,
574            "avk-123456".to_string(),
575        );
576        let cardano_db = CardanoDatabaseSnapshot::dummy();
577        let db_dir = TempDir::create(
578            "client-cli",
579            "verify_cardano_db_snapshot_signature_should_remove_db_dir_if_messages_mismatch",
580        );
581
582        let result = CardanoDbV2DownloadCommand::verify_cardano_db_snapshot_signature(
583            &Logger::root(slog::Discard, slog::o!()),
584            1,
585            &progress_printer,
586            &certificate,
587            &message,
588            &cardano_db,
589            &db_dir,
590        )
591        .await;
592
593        assert!(result.is_err());
594        assert!(
595            !db_dir.exists(),
596            "The db directory should have been removed but it still exists"
597        );
598    }
599
600    #[test]
601    fn immutable_file_range_without_start_without_end_returns_variant_full() {
602        let range = CardanoDbV2DownloadCommand::immutable_file_range(None, None);
603
604        assert_eq!(range, ImmutableFileRange::Full);
605    }
606
607    #[test]
608    fn immutable_file_range_with_start_without_end_returns_variant_from() {
609        let start = Some(12);
610
611        let range = CardanoDbV2DownloadCommand::immutable_file_range(start, None);
612
613        assert_eq!(range, ImmutableFileRange::From(12));
614    }
615
616    #[test]
617    fn immutable_file_range_with_start_with_end_returns_variant_range() {
618        let start = Some(12);
619        let end = Some(345);
620
621        let range = CardanoDbV2DownloadCommand::immutable_file_range(start, end);
622
623        assert_eq!(range, ImmutableFileRange::Range(12, 345));
624    }
625
626    #[test]
627    fn immutable_file_range_without_start_with_end_returns_variant_up_to() {
628        let end = Some(345);
629
630        let range = CardanoDbV2DownloadCommand::immutable_file_range(None, end);
631
632        assert_eq!(range, ImmutableFileRange::UpTo(345));
633    }
634
635    #[test]
636    fn compute_required_disk_space_for_snapshot_when_full_restoration() {
637        let cardano_db_snapshot = CardanoDatabaseSnapshot {
638            total_db_size_uncompressed: 123,
639            ..CardanoDatabaseSnapshot::dummy()
640        };
641        let restoration_options = RestorationOptions {
642            immutable_file_range: ImmutableFileRange::Full,
643            db_dir: PathBuf::from("db_dir"),
644            download_unpack_options: DownloadUnpackOptions::default(),
645            disk_space_safety_margin_ratio: 0.0,
646        };
647
648        let required_size = CardanoDbV2DownloadCommand::compute_required_disk_space_for_snapshot(
649            &cardano_db_snapshot,
650            &restoration_options,
651        );
652
653        assert_eq!(required_size, 123);
654    }
655
656    #[test]
657    fn compute_required_disk_space_for_snapshot_when_partial_restoration_and_no_ancillary_files() {
658        let cardano_db_snapshot = CardanoDatabaseSnapshot {
659            digests: DigestsMessagePart {
660                size_uncompressed: 50,
661                locations: vec![],
662            },
663            immutables: ImmutablesMessagePart {
664                average_size_uncompressed: 100,
665                locations: vec![],
666            },
667            ancillary: AncillaryMessagePart {
668                size_uncompressed: 300,
669                locations: vec![],
670            },
671            ..CardanoDatabaseSnapshot::dummy()
672        };
673        let restoration_options = RestorationOptions {
674            immutable_file_range: ImmutableFileRange::Range(10, 19),
675            db_dir: PathBuf::from("db_dir"),
676            download_unpack_options: DownloadUnpackOptions {
677                include_ancillary: false,
678                ..DownloadUnpackOptions::default()
679            },
680            disk_space_safety_margin_ratio: 0.0,
681        };
682
683        let required_size = CardanoDbV2DownloadCommand::compute_required_disk_space_for_snapshot(
684            &cardano_db_snapshot,
685            &restoration_options,
686        );
687
688        let digest_size = cardano_db_snapshot.digests.size_uncompressed;
689        let average_size_uncompressed_immutable =
690            cardano_db_snapshot.immutables.average_size_uncompressed;
691
692        let expected_size = digest_size + 10 * average_size_uncompressed_immutable;
693        assert_eq!(required_size, expected_size);
694    }
695
696    #[test]
697    fn compute_required_disk_space_for_snapshot_when_partial_restoration_and_ancillary_files() {
698        let cardano_db_snapshot = CardanoDatabaseSnapshot {
699            digests: DigestsMessagePart {
700                size_uncompressed: 50,
701                locations: vec![],
702            },
703            immutables: ImmutablesMessagePart {
704                average_size_uncompressed: 100,
705                locations: vec![],
706            },
707            ancillary: AncillaryMessagePart {
708                size_uncompressed: 300,
709                locations: vec![],
710            },
711            ..CardanoDatabaseSnapshot::dummy()
712        };
713        let restoration_options = RestorationOptions {
714            immutable_file_range: ImmutableFileRange::Range(10, 19),
715            db_dir: PathBuf::from("db_dir"),
716            download_unpack_options: DownloadUnpackOptions {
717                include_ancillary: true,
718                ..DownloadUnpackOptions::default()
719            },
720            disk_space_safety_margin_ratio: 0.0,
721        };
722
723        let required_size = CardanoDbV2DownloadCommand::compute_required_disk_space_for_snapshot(
724            &cardano_db_snapshot,
725            &restoration_options,
726        );
727
728        let digest_size = cardano_db_snapshot.digests.size_uncompressed;
729        let average_size_uncompressed_immutable =
730            cardano_db_snapshot.immutables.average_size_uncompressed;
731        let ancillary_size = cardano_db_snapshot.ancillary.size_uncompressed;
732
733        let expected_size = digest_size + 10 * average_size_uncompressed_immutable + ancillary_size;
734        assert_eq!(required_size, expected_size);
735    }
736
737    #[test]
738    fn add_safety_margin_apply_margin_with_ratio() {
739        assert_eq!(CardanoDbV2DownloadCommand::add_safety_margin(100, 0.1), 110);
740        assert_eq!(CardanoDbV2DownloadCommand::add_safety_margin(100, 0.5), 150);
741        assert_eq!(CardanoDbV2DownloadCommand::add_safety_margin(100, 1.5), 250);
742
743        assert_eq!(CardanoDbV2DownloadCommand::add_safety_margin(0, 0.1), 0);
744
745        assert_eq!(CardanoDbV2DownloadCommand::add_safety_margin(100, 0.0), 100);
746    }
747}