mithril_client_cli/commands/cardano_db/
verify.rs

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