mithril_client_cli/commands/cardano_db/
verify.rs

1use std::{
2    collections::HashMap,
3    path::{Path, PathBuf},
4    sync::Arc,
5};
6
7use crate::{
8    CommandContext,
9    commands::{
10        cardano_db::{
11            CardanoDbCommandsBackend,
12            shared_steps::{self, ComputeCardanoDatabaseMessageOptions},
13        },
14        client_builder,
15    },
16    configuration::{ConfigError, ConfigSource},
17    utils::{
18        self, ExpanderUtils, IndicatifFeedbackReceiver, ProgressOutputType, ProgressPrinter,
19        print_simple_warning,
20    },
21};
22use anyhow::Context;
23use chrono::{DateTime, Utc};
24use clap::Parser;
25use mithril_client::{
26    CardanoDatabaseSnapshot, MithrilResult, RequiredAggregatorCapabilities,
27    cardano_database_client::{
28        CardanoDatabaseVerificationError, ImmutableFileRange, ImmutableVerificationResult,
29    },
30    common::{ImmutableFileNumber, SignedEntityTypeDiscriminants},
31};
32
33/// Clap command to verify a Cardano db and its associated certificate.
34#[derive(Parser, Debug, Clone)]
35pub struct CardanoDbVerifyCommand {
36    ///Backend to use, either: `v1` (default, full database restoration only) or `v2` (full or partial database restoration)
37    #[arg(short, long, value_enum, default_value_t = CardanoDbCommandsBackend::V2)]
38    backend: CardanoDbCommandsBackend,
39
40    /// Digest of the Cardano db snapshot to verify  or `latest` for the latest artifact
41    ///
42    /// Use the `list` command to get that information.
43    digest: String,
44
45    /// Directory from where the immutable will be verified.
46    #[clap(long)]
47    db_dir: Option<PathBuf>,
48
49    /// Genesis verification key to check the certificate chain.
50    #[clap(long, env = "GENESIS_VERIFICATION_KEY")]
51    genesis_verification_key: Option<String>,
52
53    /// The first immutable file number to verify.
54    ///
55    /// If not set, the verify process will start from the first immutable file.
56    #[clap(long)]
57    start: Option<ImmutableFileNumber>,
58
59    /// The last immutable file number to verify.
60    ///
61    /// If not set, the verify will continue until the last certified immutable file.
62    #[clap(long)]
63    end: Option<ImmutableFileNumber>,
64
65    /// If set, the verification will not fail if some immutable files are missing.
66    #[clap(long)]
67    allow_missing: bool,
68}
69
70impl CardanoDbVerifyCommand {
71    /// Main command execution
72    pub async fn execute(&self, mut context: CommandContext) -> MithrilResult<()> {
73        match self.backend {
74            CardanoDbCommandsBackend::V1 => Err(anyhow::anyhow!(
75                r#"The "verify" subcommand is not available for the v1, use --backend v2 instead"#,
76            )),
77            CardanoDbCommandsBackend::V2 => {
78                context.config_parameters_mut().add_source(self)?;
79                self.verify(&context).await
80            }
81        }
82    }
83
84    async fn verify(&self, context: &CommandContext) -> MithrilResult<()> {
85        let db_dir = context.config_parameters().require("db_dir")?;
86        let db_dir = Path::new(&db_dir);
87
88        let progress_output_type = if context.is_json_output_enabled() {
89            ProgressOutputType::JsonReporter
90        } else {
91            ProgressOutputType::Tty
92        };
93        let progress_printer = ProgressPrinter::new(progress_output_type, 5);
94        let client = client_builder(context.config_parameters())?
95            .with_capabilities(RequiredAggregatorCapabilities::And(vec![
96                RequiredAggregatorCapabilities::SignedEntityType(
97                    SignedEntityTypeDiscriminants::CardanoDatabase,
98                ),
99            ]))
100            .add_feedback_receiver(Arc::new(IndicatifFeedbackReceiver::new(
101                progress_output_type,
102                context.logger().clone(),
103            )))
104            .with_logger(context.logger().clone())
105            .build()?;
106
107        client.cardano_database_v2().check_has_immutables(db_dir)?;
108
109        let get_list_of_artifact_ids = || async {
110            let cardano_db_snapshots = client.cardano_database_v2().list().await.with_context(
111                || "Can not get the list of artifacts while retrieving the latest cardano db hash",
112            )?;
113
114            Ok(cardano_db_snapshots
115                .iter()
116                .map(|cardano_db| cardano_db.hash.to_owned())
117                .collect::<Vec<String>>())
118        };
119
120        let cardano_db_message = client
121            .cardano_database_v2()
122            .get(
123                &ExpanderUtils::expand_eventual_id_alias(&self.digest, get_list_of_artifact_ids())
124                    .await?,
125            )
126            .await?
127            .with_context(|| format!("Can not get the cardano db for hash: '{}'", self.digest))?;
128
129        let immutable_file_range = shared_steps::immutable_file_range(self.start, self.end);
130
131        print_immutables_range_to_verify(
132            &cardano_db_message,
133            &immutable_file_range,
134            context.is_json_output_enabled(),
135        )?;
136
137        let certificate = shared_steps::fetch_certificate_and_verifying_chain(
138            1,
139            &progress_printer,
140            &client,
141            &cardano_db_message.certificate_hash,
142        )
143        .await?;
144
145        let verified_digests = shared_steps::download_and_verify_digests(
146            2,
147            &progress_printer,
148            &client,
149            &certificate,
150            &cardano_db_message,
151        )
152        .await?;
153
154        let options = ComputeCardanoDatabaseMessageOptions {
155            db_dir: db_dir.to_path_buf(),
156            immutable_file_range,
157            allow_missing: self.allow_missing,
158        };
159
160        let merkle_proof = shared_steps::verify_cardano_database(
161            3,
162            &progress_printer,
163            &client,
164            &certificate,
165            &cardano_db_message,
166            &options,
167            &verified_digests,
168        )
169        .await;
170
171        match merkle_proof {
172            Err(e) => match e.downcast_ref::<CardanoDatabaseVerificationError>() {
173                Some(CardanoDatabaseVerificationError::ImmutableFilesVerification(lists)) => {
174                    Self::print_immutables_verification_error(
175                        lists,
176                        context.is_json_output_enabled(),
177                    );
178                    Ok(())
179                }
180                _ => Err(e),
181            },
182            Ok(merkle_proof) => {
183                let message = shared_steps::compute_cardano_db_snapshot_message(
184                    4,
185                    &progress_printer,
186                    &certificate,
187                    &merkle_proof,
188                )
189                .await?;
190
191                shared_steps::verify_message_matches_certificate(
192                    &context.logger().clone(),
193                    5,
194                    &progress_printer,
195                    &certificate,
196                    &message,
197                    &cardano_db_message,
198                    db_dir,
199                )
200                .await?;
201
202                Self::log_verified_information(
203                    db_dir,
204                    &cardano_db_message.hash,
205                    context.is_json_output_enabled(),
206                )?;
207
208                Ok(())
209            }
210        }
211    }
212
213    fn log_verified_information(
214        db_dir: &Path,
215        snapshot_hash: &str,
216        json_output: bool,
217    ) -> MithrilResult<()> {
218        if json_output {
219            let canonical_filepath = &db_dir.canonicalize().with_context(|| {
220                format!("Could not get canonical filepath of '{}'", db_dir.display())
221            })?;
222            let json = serde_json::json!({
223                "timestamp": Utc::now().to_rfc3339(),
224                "verified_db_directory": canonical_filepath
225            });
226            println!("{json}");
227        } else {
228            println!(
229                "Cardano database snapshot '{snapshot_hash}' archives have been successfully verified. Immutable files have been successfully verified with Mithril."
230            );
231        }
232        Ok(())
233    }
234
235    fn print_immutables_verification_error(lists: &ImmutableVerificationResult, json_output: bool) {
236        let utc_now = Utc::now();
237
238        let report_path: Option<PathBuf> = write_json_file_error(utc_now, lists)
239            .inspect_err(|err| {
240                print_simple_warning(
241                    &format!("Could not save error report file: {err:?}"),
242                    json_output,
243                );
244            })
245            .ok();
246
247        let error_message = "Verifying immutables files has failed";
248        if json_output {
249            let report_path_value = match &report_path {
250                Some(path) => serde_json::Value::String(path.to_string_lossy().to_string()),
251                None => serde_json::Value::Null,
252            };
253
254            let json = serde_json::json!({
255                "timestamp": utc_now.to_rfc3339(),
256                "verify_error" : {
257                    "message": error_message,
258                    "immutables_verification_error_file": report_path_value,
259                    "immutables_dir": lists.immutables_dir,
260                    "missing_files_count": lists.missing.len(),
261                    "tampered_files_count": lists.tampered.len(),
262                    "non_verifiable_files_count": lists.non_verifiable.len(),
263                }
264            });
265
266            println!("{json}");
267        } else {
268            println!("{error_message}");
269            if let Some(path) = &report_path {
270                println!(
271                    "See the lists of all missing, tampered and non verifiable files in {}",
272                    path.display()
273                );
274            } else {
275                println!("(Detailed error report could not be saved to disk)");
276            }
277            if !lists.missing.is_empty() {
278                println!("Number of missing immutable files: {}", lists.missing.len());
279            }
280            if !lists.tampered.is_empty() {
281                println!(
282                    "Number of tampered immutable files: {:?}",
283                    lists.tampered.len()
284                );
285            }
286            if !lists.non_verifiable.is_empty() {
287                println!(
288                    "Number of non verifiable immutable files: {:?}",
289                    lists.non_verifiable.len()
290                );
291            }
292        }
293    }
294}
295
296fn write_json_file_error(
297    date: DateTime<Utc>,
298    lists: &ImmutableVerificationResult,
299) -> MithrilResult<PathBuf> {
300    let file_path = PathBuf::from(format!(
301        "immutables_verification_error-{}.json",
302        date.timestamp()
303    ));
304
305    let json_content = serde_json::to_string_pretty(&serde_json::json!({
306        "timestamp": date.to_rfc3339(),
307        "immutables_dir": lists.immutables_dir,
308        "missing-files": lists.missing,
309        "tampered-files": lists.tampered,
310        "non-verifiable-files": lists.non_verifiable,
311    }))
312    .context("Failed to serialize verification error report to JSON")?;
313
314    std::fs::write(&file_path, json_content).with_context(|| {
315        format!(
316            "Failed to write error report to file: {}",
317            file_path.display()
318        )
319    })?;
320
321    Ok(file_path)
322}
323
324fn print_immutables_range_to_verify(
325    cardano_db_message: &CardanoDatabaseSnapshot,
326    immutable_file_range: &ImmutableFileRange,
327    json_output: bool,
328) -> Result<(), anyhow::Error> {
329    let range_to_verify =
330        immutable_file_range.to_range_inclusive(cardano_db_message.beacon.immutable_file_number)?;
331    if json_output {
332        let json = serde_json::json!({
333            "timestamp": Utc::now().to_rfc3339(),
334            "local_immutable_range_to_verify": {
335                "start": range_to_verify.start(),
336                "end": range_to_verify.end(),
337            },
338        });
339        println!("{json}");
340    } else {
341        eprintln!(
342            "Verifying local immutable files from number {} to {}",
343            range_to_verify.start(),
344            range_to_verify.end()
345        );
346    }
347    Ok(())
348}
349
350impl ConfigSource for CardanoDbVerifyCommand {
351    fn collect(&self) -> Result<HashMap<String, String>, ConfigError> {
352        let mut map = HashMap::new();
353
354        if let Some(download_dir) = self.db_dir.clone() {
355            let param = "db_dir".to_string();
356            map.insert(
357                param.clone(),
358                utils::path_to_string(&download_dir)
359                    .map_err(|e| ConfigError::Conversion(param, e))?,
360            );
361        }
362
363        if let Some(genesis_verification_key) = self.genesis_verification_key.clone() {
364            map.insert(
365                "genesis_verification_key".to_string(),
366                genesis_verification_key,
367            );
368        }
369
370        Ok(map)
371    }
372}