mithril_client_cli/commands/cardano_db/
verify.rs1use 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#[derive(Parser, Debug, Clone)]
35pub struct CardanoDbVerifyCommand {
36 #[arg(short, long, value_enum, default_value_t = CardanoDbCommandsBackend::V2)]
38 backend: CardanoDbCommandsBackend,
39
40 digest: String,
44
45 #[clap(long)]
47 db_dir: Option<PathBuf>,
48
49 #[clap(long, env = "GENESIS_VERIFICATION_KEY")]
51 genesis_verification_key: Option<String>,
52
53 #[clap(long)]
57 start: Option<ImmutableFileNumber>,
58
59 #[clap(long)]
63 end: Option<ImmutableFileNumber>,
64
65 #[clap(long)]
67 allow_missing: bool,
68}
69
70impl CardanoDbVerifyCommand {
71 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}