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, 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/// 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)]
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    /// Command execution
92    pub async fn execute(&self, context: CommandContext) -> MithrilResult<()> {
93        let params = context.config_parameters()?.add_source(self)?;
94        let prepared_command = self.prepare(&params)?;
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(&params)?
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    /// Is JSON output enabled
257    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        // The cardano db snapshot download does not fail if the statistic call fails.
381        // It would be nice to implement tests to verify the behavior of `add_statistics`
382        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        // Append 'clean' file to speedup node bootstrap
404        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}