mithril_client_cli/commands/cardano_db/
verify.rs1use 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#[derive(Parser, Debug, Clone)]
33pub struct CardanoDbVerifyCommand {
34 #[arg(short, long, value_enum, default_value_t = CardanoDbCommandsBackend::V2)]
36 backend: CardanoDbCommandsBackend,
37
38 digest: String,
42
43 #[clap(long)]
45 db_dir: Option<PathBuf>,
46
47 #[clap(long, env = "GENESIS_VERIFICATION_KEY")]
49 genesis_verification_key: Option<String>,
50
51 #[clap(long)]
55 start: Option<ImmutableFileNumber>,
56
57 #[clap(long)]
61 end: Option<ImmutableFileNumber>,
62
63 #[clap(long)]
65 allow_missing: bool,
66}
67
68impl CardanoDbVerifyCommand {
69 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}