From ca0dbb8c719f78574c2b0223b92fff33da458cdd Mon Sep 17 00:00:00 2001 From: Damien LACHAUME / PALO-IT Date: Mon, 18 Dec 2023 17:32:44 +0100 Subject: [PATCH 01/13] Implement `add_statistics` in `mithril-client` library --- mithril-client/src/aggregator_client.rs | 70 +++++++++++ mithril-client/src/snapshot_client.rs | 117 ++++++++++-------- mithril-client/tests/extensions/fake.rs | 3 +- .../snapshot_list_get_show_download_verify.rs | 12 ++ 4 files changed, 148 insertions(+), 54 deletions(-) diff --git a/mithril-client/src/aggregator_client.rs b/mithril-client/src/aggregator_client.rs index 5ae014c21d0..9cf71561385 100644 --- a/mithril-client/src/aggregator_client.rs +++ b/mithril-client/src/aggregator_client.rs @@ -68,6 +68,9 @@ pub enum AggregatorRequest { }, /// Lists the aggregator [snapshots][crate::Snapshot] ListSnapshots, + + /// Increments Aggregator's download statistics + AddStatistics, } impl AggregatorRequest { @@ -88,6 +91,7 @@ impl AggregatorRequest { format!("artifact/snapshot/{}", digest) } AggregatorRequest::ListSnapshots => "artifact/snapshots".to_string(), + AggregatorRequest::AddStatistics => "statistics/snapshot".to_string(), } } } @@ -101,6 +105,13 @@ pub trait AggregatorClient: Sync + Send { &self, request: AggregatorRequest, ) -> Result; + + /// Post information to the Aggregator, the URL is a relative path for a resource + async fn post_content( + &self, + request: AggregatorRequest, + content: &str, + ) -> Result; } /// Responsible of HTTP transport and API version check. @@ -208,6 +219,49 @@ impl AggregatorHTTPClient { } } + #[cfg_attr(target_family = "wasm", async_recursion(?Send))] + #[cfg_attr(not(target_family = "wasm"), async_recursion)] + async fn post(&self, url: Url, json: &str) -> Result { + debug!(self.logger, "POST url='{url}' json='{json}'."); + let request_builder = self.http_client.post(url.to_owned()).body(json.to_owned()); + let current_api_version = self + .compute_current_api_version() + .await + .unwrap() + .to_string(); + debug!( + self.logger, + "Prepare request with version: {current_api_version}" + ); + let request_builder = + request_builder.header(MITHRIL_API_VERSION_HEADER, current_api_version); + + let response = request_builder.send().await.map_err(|e| { + AggregatorClientError::SubsystemError( + anyhow!(e).context("Error while POSTing data '{json}' to URL='{url}'."), + ) + })?; + + match response.status() { + StatusCode::OK | StatusCode::CREATED => Ok(response), + StatusCode::PRECONDITION_FAILED => { + if self.discard_current_api_version().await.is_some() + && !self.api_versions.read().await.is_empty() + { + return self.post(url, json).await; + } + + Err(self.handle_api_error(&response).await) + } + StatusCode::NOT_FOUND => Err(AggregatorClientError::RemoteServerLogical(anyhow!( + "Url='{url} not found" + ))), + status_code => Err(AggregatorClientError::RemoteServerTechnical(anyhow!( + "Unhandled error {status_code}" + ))), + } + } + /// API version error handling async fn handle_api_error(&self, response: &Response) -> AggregatorClientError { if let Some(version) = response.headers().get(MITHRIL_API_VERSION_HEADER) { @@ -254,6 +308,22 @@ impl AggregatorClient for AggregatorHTTPClient { ))) }) } + + async fn post_content( + &self, + request: AggregatorRequest, + content: &str, + ) -> Result { + let response = self + .post(self.get_url_for_route(&request.route())?, content) + .await?; + + response.text().await.map_err(|e| { + AggregatorClientError::SubsystemError( + anyhow!(e).context("Could not find a text body in the response."), + ) + }) + } } #[cfg(test)] diff --git a/mithril-client/src/snapshot_client.rs b/mithril-client/src/snapshot_client.rs index 93fd57315c6..39dcd856c80 100644 --- a/mithril-client/src/snapshot_client.rs +++ b/mithril-client/src/snapshot_client.rs @@ -154,65 +154,76 @@ impl SnapshotClient { } cfg_fs! { - /// Download and unpack the given snapshot to the given directory - /// - /// **NOTE**: The directory should already exist, and the user running the binary - /// must have read/write access to it. - pub async fn download_unpack( - &self, - snapshot: &Snapshot, - target_dir: &std::path::Path, - ) -> MithrilResult<()> { - use crate::feedback::MithrilEvent; + /// Download and unpack the given snapshot to the given directory + /// + /// **NOTE**: The directory should already exist, and the user running the binary + /// must have read/write access to it. + pub async fn download_unpack( + &self, + snapshot: &Snapshot, + target_dir: &std::path::Path, + ) -> MithrilResult<()> { + use crate::feedback::MithrilEvent; - for location in snapshot.locations.as_slice() { - if self.snapshot_downloader.probe(location).await.is_ok() { - let download_id = MithrilEvent::new_snapshot_download_id(); - self.feedback_sender - .send_event(MithrilEvent::SnapshotDownloadStarted { - digest: snapshot.digest.clone(), - download_id: download_id.clone(), - size: snapshot.size, - }) - .await; - return match self - .snapshot_downloader - .download_unpack( - location, - target_dir, - snapshot.compression_algorithm.unwrap_or_default(), - &download_id, - snapshot.size, - ) - .await - { - Ok(()) => { - // todo: add snapshot statistics to cli (it was previously done here) - // note: the snapshot download does not fail if the statistic call fails. - self.feedback_sender - .send_event(MithrilEvent::SnapshotDownloadCompleted { download_id }) - .await; - Ok(()) - } - Err(e) => { - slog::warn!( - self.logger, - "Failed downloading snapshot from '{location}' Error: {e}." - ); - Err(e) - } - }; + for location in snapshot.locations.as_slice() { + if self.snapshot_downloader.probe(location).await.is_ok() { + let download_id = MithrilEvent::new_snapshot_download_id(); + self.feedback_sender + .send_event(MithrilEvent::SnapshotDownloadStarted { + digest: snapshot.digest.clone(), + download_id: download_id.clone(), + size: snapshot.size, + }) + .await; + return match self + .snapshot_downloader + .download_unpack( + location, + target_dir, + snapshot.compression_algorithm.unwrap_or_default(), + &download_id, + snapshot.size, + ) + .await + { + Ok(()) => { + self.feedback_sender + .send_event(MithrilEvent::SnapshotDownloadCompleted { download_id }) + .await; + Ok(()) + } + Err(e) => { + slog::warn!( + self.logger, + "Failed downloading snapshot from '{location}' Error: {e}." + ); + Err(e) + } + }; + } } - } - let locations = snapshot.locations.join(", "); + let locations = snapshot.locations.join(", "); - Err(SnapshotClientError::NoWorkingLocation { - digest: snapshot.digest.clone(), - locations, + Err(SnapshotClientError::NoWorkingLocation { + digest: snapshot.digest.clone(), + locations, + } + .into()) } - .into()) } + + /// Increments Aggregator's download statistics + pub async fn add_statistics(&self, snapshot: &Snapshot) -> MithrilResult<()> { + let _response = self + .aggregator_client + .post_content( + AggregatorRequest::AddStatistics, + &serde_json::to_string(snapshot)?, + ) + .await?; + + Ok(()) } } diff --git a/mithril-client/tests/extensions/fake.rs b/mithril-client/tests/extensions/fake.rs index b9d59df426e..871d34840fd 100644 --- a/mithril-client/tests/extensions/fake.rs +++ b/mithril-client/tests/extensions/fake.rs @@ -163,7 +163,8 @@ mod file { serde_json::to_string(&data.clone()).unwrap() }), ) - .or(warp::path!("certificate" / String).map(move |_hash| certificate_json.clone())); + .or(warp::path!("certificate" / String).map(move |_hash| certificate_json.clone())) + .or(warp::path!("statistics" / "snapshot").map(|| "")); let snapshot_archive_path = build_fake_zstd_snapshot(immutable_db, work_dir); diff --git a/mithril-client/tests/snapshot_list_get_show_download_verify.rs b/mithril-client/tests/snapshot_list_get_show_download_verify.rs index 3b7b6c5d54f..120ed911399 100644 --- a/mithril-client/tests/snapshot_list_get_show_download_verify.rs +++ b/mithril-client/tests/snapshot_list_get_show_download_verify.rs @@ -58,6 +58,18 @@ async fn snapshot_list_get_show_download_verify() { .await .expect("download/unpack snapshot should not fail"); + client + .snapshot() + .add_statistics(&snapshot) + .await + .expect("add_statistics should not fail"); + + // TODO: find a way to verify that the last route called is the right one + // assert_eq!( + // "statistics/snapshot", + // fake_aggregator.get_last_route_called() + // ); + let message = MessageBuilder::new() .compute_snapshot_message(&certificate, &unpacked_dir) .await From c57473806eb1b8602b4a3fa4d97b96d664fee369 Mon Sep 17 00:00:00 2001 From: Damien LACHAUME / PALO-IT Date: Mon, 18 Dec 2023 17:39:37 +0100 Subject: [PATCH 02/13] Remove `http_client` in `mithril-client` CLI --- Cargo.lock | 3 - mithril-client-cli/Cargo.toml | 3 - .../src/commands/snapshot/download.rs | 7 +- mithril-client-cli/src/http_client.rs | 142 ------------------ mithril-client-cli/src/lib.rs | 1 - mithril-client-cli/src/utils/snapshot.rs | 44 +----- 6 files changed, 2 insertions(+), 198 deletions(-) delete mode 100644 mithril-client-cli/src/http_client.rs diff --git a/Cargo.lock b/Cargo.lock index 075a5abb5a9..95f84e5ae6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3283,7 +3283,6 @@ name = "mithril-client-cli" version = "0.5.12" dependencies = [ "anyhow", - "async-recursion", "async-trait", "chrono", "clap", @@ -3297,7 +3296,6 @@ dependencies = [ "mithril-common", "openssl", "openssl-probe", - "reqwest", "semver", "serde", "serde_json", @@ -3308,7 +3306,6 @@ dependencies = [ "slog-term", "thiserror", "tokio", - "warp", ] [[package]] diff --git a/mithril-client-cli/Cargo.toml b/mithril-client-cli/Cargo.toml index 9af9c9368ab..ea7adc22a4d 100644 --- a/mithril-client-cli/Cargo.toml +++ b/mithril-client-cli/Cargo.toml @@ -23,7 +23,6 @@ assets = [["../target/release/mithril-client", "usr/bin/", "755"]] [dependencies] anyhow = "1.0.75" -async-recursion = "1.0.5" async-trait = "0.1.73" chrono = { version = "0.4.31", features = ["serde"] } clap = { version = "4.4.6", features = ["derive", "env"] } @@ -37,7 +36,6 @@ mithril-client = { path = "../mithril-client", features = ["fs"] } mithril-common = { path = "../mithril-common", features = ["full"] } openssl = { version = "0.10.57", features = ["vendored"], optional = true } openssl-probe = { version = "0.1.5", optional = true } -reqwest = { version = "0.11.22", features = ["json", "stream"] } semver = "1.0.19" serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.107" @@ -54,7 +52,6 @@ tokio = { version = "1.32.0", features = ["full"] } [dev-dependencies] mithril-common = { path = "../mithril-common", features = ["test_http_server"] } -warp = "0.3" [features] portable = ["mithril-common/portable"] diff --git a/mithril-client-cli/src/commands/snapshot/download.rs b/mithril-client-cli/src/commands/snapshot/download.rs index 9bd95e63cb8..f649d7bc974 100644 --- a/mithril-client-cli/src/commands/snapshot/download.rs +++ b/mithril-client-cli/src/commands/snapshot/download.rs @@ -135,12 +135,7 @@ impl SnapshotDownloadCommand { // The snapshot download does not fail if the statistic call fails. // It would be nice to implement tests to verify the behavior of `add_statistics` - if let Err(e) = SnapshotUtils::add_statistics( - ¶ms.require("aggregator_endpoint")?, - &snapshot_message, - ) - .await - { + if let Err(e) = client.snapshot().add_statistics(&snapshot_message).await { warn!("Could not POST snapshot download statistics: {e:?}"); } diff --git a/mithril-client-cli/src/http_client.rs b/mithril-client-cli/src/http_client.rs deleted file mode 100644 index 7adc2f18efe..00000000000 --- a/mithril-client-cli/src/http_client.rs +++ /dev/null @@ -1,142 +0,0 @@ -use anyhow::anyhow; -use async_recursion::async_recursion; -use reqwest::{Client, Response, StatusCode}; -use semver::Version; -use slog_scope::debug; -use std::sync::Arc; -use thiserror::Error; -use tokio::sync::RwLock; - -use mithril_common::{StdError, MITHRIL_API_VERSION_HEADER}; - -/// Error tied with the Aggregator client -#[derive(Error, Debug)] -pub enum AggregatorHTTPClientError { - /// Error raised when querying the aggregator returned a 5XX error. - #[error("remote server technical error")] - RemoteServerTechnical(#[source] StdError), - - /// Error raised when querying the aggregator returned a 4XX error. - #[error("remote server logical error")] - RemoteServerLogical(#[source] StdError), - - /// Error raised when the server API version mismatch the client API version. - #[error("API version mismatch")] - ApiVersionMismatch(#[source] StdError), - - /// HTTP subsystem error - #[error("HTTP subsystem error")] - SubsystemError(#[source] StdError), -} - -/// Responsible of HTTP transport and API version check. -pub struct AggregatorHTTPClient { - aggregator_endpoint: String, - api_versions: Arc>>, -} - -impl AggregatorHTTPClient { - /// AggregatorHTTPClient factory - pub fn new(aggregator_endpoint: &str, api_versions: Vec) -> Self { - debug!("New AggregatorHTTPClient created"); - Self { - aggregator_endpoint: aggregator_endpoint.to_owned(), - api_versions: Arc::new(RwLock::new(api_versions)), - } - } - - pub async fn post_content( - &self, - url: &str, - json: &str, - ) -> Result { - let url = format!("{}/{}", self.aggregator_endpoint.trim_end_matches('/'), url); - let response = self.post(&url, json).await?; - - response.text().await.map_err(|e| { - AggregatorHTTPClientError::SubsystemError( - anyhow!(e).context("Could not find a text body in the response."), - ) - }) - } - - /// Computes the current api version - async fn compute_current_api_version(&self) -> Option { - self.api_versions.read().await.first().cloned() - } - - /// Discards the current api version - /// It discards the current version if and only if there is at least 2 versions available - async fn discard_current_api_version(&self) -> Option { - if self.api_versions.read().await.len() < 2 { - return None; - } - if let Some(current_api_version) = self.compute_current_api_version().await { - let mut api_versions = self.api_versions.write().await; - if let Some(index) = api_versions - .iter() - .position(|value| *value == current_api_version) - { - api_versions.remove(index); - return Some(current_api_version); - } - } - None - } - - /// Issue a POST HTTP request. - #[async_recursion] - async fn post(&self, url: &str, json: &str) -> Result { - debug!("POST url='{url}' json='{json}'."); - let request_builder = Client::new().post(url.to_owned()).body(json.to_owned()); - let current_api_version = self - .compute_current_api_version() - .await - .unwrap() - .to_string(); - debug!("Prepare request with version: {current_api_version}"); - let request_builder = - request_builder.header(MITHRIL_API_VERSION_HEADER, current_api_version); - - let response = request_builder.send().await.map_err(|e| { - AggregatorHTTPClientError::SubsystemError( - anyhow!(e).context(format!("Error while POSTing data '{json}' to URL='{url}'.")), - ) - })?; - - match response.status() { - StatusCode::OK | StatusCode::CREATED => Ok(response), - StatusCode::PRECONDITION_FAILED => { - if self.discard_current_api_version().await.is_some() - && !self.api_versions.read().await.is_empty() - { - return self.post(url, json).await; - } - - Err(self.handle_api_error(&response).await) - } - StatusCode::NOT_FOUND => Err(AggregatorHTTPClientError::RemoteServerLogical(anyhow!( - "Url='{url} not found" - ))), - status_code => Err(AggregatorHTTPClientError::RemoteServerTechnical(anyhow!( - "Unhandled error {status_code}" - ))), - } - } - - /// API version error handling - async fn handle_api_error(&self, response: &Response) -> AggregatorHTTPClientError { - if let Some(version) = response.headers().get(MITHRIL_API_VERSION_HEADER) { - AggregatorHTTPClientError::ApiVersionMismatch(anyhow!( - "server version: '{}', signer version: '{}'", - version.to_str().unwrap(), - self.compute_current_api_version().await.unwrap() - )) - } else { - AggregatorHTTPClientError::ApiVersionMismatch(anyhow!( - "version precondition failed, sent version '{}'.", - self.compute_current_api_version().await.unwrap() - )) - } - } -} diff --git a/mithril-client-cli/src/lib.rs b/mithril-client-cli/src/lib.rs index 1c0f768a925..3d975f59750 100644 --- a/mithril-client-cli/src/lib.rs +++ b/mithril-client-cli/src/lib.rs @@ -1,5 +1,4 @@ pub mod configuration; -pub mod http_client; pub mod utils; /// `mithril-common` re-exports diff --git a/mithril-client-cli/src/utils/snapshot.rs b/mithril-client-cli/src/utils/snapshot.rs index e31b49f204d..4e2224292e2 100644 --- a/mithril-client-cli/src/utils/snapshot.rs +++ b/mithril-client-cli/src/utils/snapshot.rs @@ -4,12 +4,8 @@ use indicatif::{MultiProgress, ProgressBar}; use std::time::Duration; use super::SnapshotUnpackerError; -use crate::http_client::AggregatorHTTPClient; - use mithril_client::MithrilResult; -use mithril_common::{ - api_version::APIVersionProvider, messages::SnapshotMessage, StdError, StdResult, -}; +use mithril_common::{StdError, StdResult}; /// Utility functions for to the Snapshot commands pub struct SnapshotUtils; @@ -47,50 +43,12 @@ impl SnapshotUtils { res = future => res, } } - - /// Increments Aggregator's download statistics - pub async fn add_statistics( - aggregator_endpoint: &str, - snapshot: &SnapshotMessage, - ) -> StdResult<()> { - let url = "statistics/snapshot"; - let json = serde_json::to_string(&snapshot)?; - let http_client = AggregatorHTTPClient::new( - aggregator_endpoint, - APIVersionProvider::compute_all_versions_sorted()?, - ); - let _response = http_client.post_content(url, &json).await?; - - Ok(()) - } } #[cfg(test)] mod test { use super::*; - use mithril_common::test_utils::test_http_server::test_http_server; use std::path::PathBuf; - use warp::Filter; - - #[tokio::test] - async fn add_statistics_should_return_ok() { - let server = - test_http_server(warp::path!("statistics" / "snapshot").map(move || "".to_string())); - let snapshot_message = SnapshotMessage::dummy(); - - let result = SnapshotUtils::add_statistics(&server.url(), &snapshot_message).await; - - assert!(result.is_ok()) - } - - #[tokio::test] - async fn add_statistics_should_return_error() { - let snapshot_message = SnapshotMessage::dummy(); - - let result = SnapshotUtils::add_statistics("http://whatever", &snapshot_message).await; - - assert!(result.is_err()) - } #[test] fn check_disk_space_error_should_return_warning_message_if_error_is_not_enough_space() { From e6a67c36bcd50a5fbe78bdfe9175272baac9d494 Mon Sep 17 00:00:00 2001 From: Damien LACHAUME / PALO-IT Date: Mon, 18 Dec 2023 18:45:48 +0100 Subject: [PATCH 03/13] Update code in the README of `mithril-client` library --- mithril-client/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mithril-client/README.md b/mithril-client/README.md index a144d5228f2..4ddf1b03fb6 100644 --- a/mithril-client/README.md +++ b/mithril-client/README.md @@ -5,7 +5,7 @@ * `mithril-client` defines all the tooling necessary to manipulate Mithril certified types available from a Mithril aggregator. * The different types of available data certified by Mithril are: - * Snapshot: list, get and download tarball. + * Snapshot: list, get, download tarball and add statistics. * Mithril stake distribution: list and get. * Certificate: list, get, and chain validation. @@ -37,6 +37,10 @@ async fn main() -> mithril_client::MithrilResult<()> { .snapshot() .download_unpack(&snapshot, target_directory) .await?; + + if let Err(e) = client.snapshot().add_statistics(&snapshot).await { + println!("Could not POST snapshot download statistics: {:?}", e); + } let message = MessageBuilder::new() .compute_snapshot_message(&certificate, target_directory) From 6e98e317c65e6cc2281860f3d230dd6f69b90243 Mon Sep 17 00:00:00 2001 From: Damien LACHAUME / PALO-IT Date: Mon, 18 Dec 2023 18:09:11 +0100 Subject: [PATCH 04/13] Add `add_statistics` call in snapshot example --- examples/client-snapshot/README.md | 1 + examples/client-snapshot/src/main.rs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/examples/client-snapshot/README.md b/examples/client-snapshot/README.md index dc585e7ebe4..462985c4775 100644 --- a/examples/client-snapshot/README.md +++ b/examples/client-snapshot/README.md @@ -11,6 +11,7 @@ In this example, the client interacts with a real aggregator on the network `tes - verify a certificate chain - compute a message for a Snapshot - verify that the certificate signs the computed message +- increments download statistics The crate [indicatif](https://docs.rs/indicatif/latest/indicatif/) is used to nicely report the progress to the console. diff --git a/examples/client-snapshot/src/main.rs b/examples/client-snapshot/src/main.rs index 64a60a4aa92..4baec5f1ad1 100644 --- a/examples/client-snapshot/src/main.rs +++ b/examples/client-snapshot/src/main.rs @@ -55,6 +55,10 @@ async fn main() -> MithrilResult<()> { .download_unpack(&snapshot, &unpacked_dir) .await?; + if let Err(e) = client.snapshot().add_statistics(&snapshot).await { + println!("Could not POST snapshot download statistics: {:?}", e); + } + println!("Computing snapshot '{}' message ...", snapshot.digest); let message = wait_spinner( &progress_bar, From 8a77db0f7ee01c6f109c89213826ec211010d5e4 Mon Sep 17 00:00:00 2001 From: Damien LACHAUME / PALO-IT Date: Mon, 18 Dec 2023 18:28:23 +0100 Subject: [PATCH 05/13] Update rust documentation --- mithril-client/src/lib.rs | 7 ++++++- mithril-client/src/snapshot_client.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/mithril-client/src/lib.rs b/mithril-client/src/lib.rs index 2579e15db51..5a3f671072e 100644 --- a/mithril-client/src/lib.rs +++ b/mithril-client/src/lib.rs @@ -6,7 +6,7 @@ //! //! It handles the different types that can be queried to a Mithril aggregator: //! -//! - [Snapshot][snapshot_client] list, get and download tarball. +//! - [Snapshot][snapshot_client] list, get, download tarball and add statistics. //! - [Mithril stake distribution][mithril_stake_distribution_client] list and get. //! - [Certificates][certificate_client] list, get, and chain validation. //! @@ -46,6 +46,11 @@ //! .download_unpack(&snapshot, &target_directory) //! .await?; //! +//! if let Err(e) = client.snapshot().add_statistics(&snapshot).await { +//! println!("Could not POST snapshot download statistics: {:?}", e); +//! } +//! +//! //! let message = MessageBuilder::new() //! .compute_snapshot_message(&certificate, &target_directory) //! .await?; diff --git a/mithril-client/src/snapshot_client.rs b/mithril-client/src/snapshot_client.rs index 39dcd856c80..8ec44f6c243 100644 --- a/mithril-client/src/snapshot_client.rs +++ b/mithril-client/src/snapshot_client.rs @@ -62,6 +62,32 @@ //! # Ok(()) //! # } //! ``` +//! +//! # Add statistics +//! **Note:** _Available on crate feature_ **fs** _only._ +//! +//! Increments Aggregator's download statistics using the [ClientBuilder][crate::client::ClientBuilder]. +//! +//! ```no_run +//! # async fn run() -> mithril_client::MithrilResult<()> { +//! use mithril_client::ClientBuilder; +//! use std::path::Path; +//! +//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?; +//! let snapshot = client.snapshot().get("SNAPSHOT_DIGEST").await?.unwrap(); +//! +//! // Note: the directory must already exist, and the user running the binary must have read/write access to it. +//! let target_directory = Path::new("/home/user/download/"); +//! client +//! .snapshot() +//! .download_unpack(&snapshot, target_directory) +//! .await?; +//! +//! client.snapshot().add_statistics(&snapshot).await.unwrap(); +//! # +//! # Ok(()) +//! # } +//! ``` use anyhow::Context; #[cfg(feature = "fs")] From 3e77491b13fca4ded8791b48df040634611ce9f8 Mon Sep 17 00:00:00 2001 From: Damien LACHAUME / PALO-IT Date: Mon, 18 Dec 2023 18:44:48 +0100 Subject: [PATCH 06/13] Update code in user guide --- .../developer-docs/nodes/mithril-client-library.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/website/root/manual/developer-docs/nodes/mithril-client-library.md b/docs/website/root/manual/developer-docs/nodes/mithril-client-library.md index 4d6c2bbad1f..7b500fd6900 100644 --- a/docs/website/root/manual/developer-docs/nodes/mithril-client-library.md +++ b/docs/website/root/manual/developer-docs/nodes/mithril-client-library.md @@ -12,7 +12,7 @@ import CompiledBinaries from '../../../compiled-binaries.md' Mithril client library can be used by Rust developers to use the Mithril network in their applications. It is responsible for handling the different types of data certified by Mithril, and available through a Mithril aggregator: -- [**Snapshot**](../../../glossary.md#snapshot): list, get and download tarball. +- [**Snapshot**](../../../glossary.md#snapshot): list, get, download tarball and add statistics. - [**Mithril stake distribution**](../../../glossary.md#stake-distribution): list and get. - [**Certificate**](../../../glossary.md#certificate): list, get, and chain validation. @@ -88,6 +88,10 @@ async fn main() -> mithril_client::MithrilResult<()> { .snapshot() .download_unpack(&snapshot, target_directory) .await?; + + if let Err(e) = client.snapshot().add_statistics(&snapshot).await { + println!("Could not POST snapshot download statistics: {:?}", e); + } let message = MessageBuilder::new() .compute_snapshot_message(&certificate, target_directory) @@ -142,6 +146,10 @@ async fn main() -> mithril_client::MithrilResult<()> { .snapshot() .download_unpack(&snapshot, target_directory) .await?; + + if let Err(e) = client.snapshot().add_statistics(&snapshot).await { + println!("Could not POST snapshot download statistics: {:?}", e); + } let message = MessageBuilder::new() .compute_snapshot_message(&certificate, target_directory) From 4aab4c5f3e9b745a1cd18f472828fbb868ac8ce0 Mon Sep 17 00:00:00 2001 From: Damien LACHAUME / PALO-IT Date: Tue, 19 Dec 2023 16:42:00 +0100 Subject: [PATCH 07/13] Enhance integration tests This allows to verify that the HTTP routes called by the library functions are the right ones --- mithril-client/tests/certificate_get_list.rs | 11 +- mithril-client/tests/extensions/fake.rs | 177 +++++++++++++++--- ...stake_distribution_list_get_show_verify.rs | 36 +++- .../snapshot_list_get_show_download_verify.rs | 54 +++++- 4 files changed, 236 insertions(+), 42 deletions(-) diff --git a/mithril-client/tests/certificate_get_list.rs b/mithril-client/tests/certificate_get_list.rs index a0e6ae17bf8..4ac66a21fdd 100644 --- a/mithril-client/tests/certificate_get_list.rs +++ b/mithril-client/tests/certificate_get_list.rs @@ -1,15 +1,16 @@ mod extensions; use crate::extensions::fake::FakeAggregator; -use mithril_client::ClientBuilder; +use mithril_client::{aggregator_client::AggregatorRequest, ClientBuilder}; #[tokio::test] async fn certificate_get_list() { let certificate_hash_list = vec!["certificate-123".to_string(), "certificate-456".to_string()]; let genesis_verification_key = mithril_common::test_utils::fake_keys::genesis_verification_key()[0]; - let fake_aggregator = FakeAggregator::spawn_with_certificate(&certificate_hash_list); - let client = ClientBuilder::aggregator(&fake_aggregator.url(), genesis_verification_key) + let fake_aggregator = FakeAggregator::new(); + let test_http_server = fake_aggregator.spawn_with_certificate(&certificate_hash_list); + let client = ClientBuilder::aggregator(&test_http_server.url(), genesis_verification_key) .build() .expect("Should be able to create a Client"); @@ -18,6 +19,10 @@ async fn certificate_get_list() { .list() .await .expect("List Certificate should not fail"); + assert_eq!( + fake_aggregator.get_last_call().await, + Some(format!("/{}", AggregatorRequest::ListCertificates.route())) + ); let mut hashes: Vec = certificates.into_iter().map(|c| c.hash).collect(); hashes.sort(); diff --git a/mithril-client/tests/extensions/fake.rs b/mithril-client/tests/extensions/fake.rs index 871d34840fd..0a9a34212bc 100644 --- a/mithril-client/tests/extensions/fake.rs +++ b/mithril-client/tests/extensions/fake.rs @@ -5,11 +5,16 @@ use mithril_client::{ }; use mithril_common::messages::CertificateMessage; use mithril_common::test_utils::test_http_server::{test_http_server, TestHttpServer}; +use std::convert::Infallible; use std::sync::Arc; +use tokio::sync::Mutex; +use warp::filters::path::FullPath; use warp::Filter; use crate::extensions::mock; +type FakeAggregatorCalls = Arc>>; + pub struct FakeCertificateVerifier; impl FakeCertificateVerifier { @@ -21,10 +26,47 @@ impl FakeCertificateVerifier { } } -pub struct FakeAggregator; +pub struct FakeAggregator { + calls: FakeAggregatorCalls, +} impl FakeAggregator { + pub fn new() -> Self { + FakeAggregator { + calls: Arc::new(Mutex::new(vec![])), + } + } + + async fn get_calls(&self) -> Vec { + let calls = self.calls.lock().await; + + calls.clone() + } + + pub async fn get_last_call(&self) -> Option { + self.get_calls().await.last().cloned() + } + + fn with_calls_middleware( + calls: FakeAggregatorCalls, + ) -> impl Filter + Clone { + warp::any().map(move || calls.clone()) + } + + async fn store_call_and_return_value( + _params: Vec, + full_path: FullPath, + calls: FakeAggregatorCalls, + returned_value: String, + ) -> Result { + let mut call_list = calls.lock().await; + call_list.push(full_path.as_str().to_string()); + + Ok(returned_value) + } + pub fn spawn_with_mithril_stake_distribution( + &self, msd_hash: &str, certificate_hash: &str, ) -> TestHttpServer { @@ -58,16 +100,44 @@ impl FakeAggregator { test_http_server( warp::path!("artifact" / "mithril-stake-distributions") - .map(move || mithril_stake_distribution_list_json.clone()) + .and(warp::path::full().map(move |p| p)) + .and(FakeAggregator::with_calls_middleware(self.calls.clone())) + .and_then(move |fullpath, calls| { + FakeAggregator::store_call_and_return_value( + vec![], + fullpath, + calls, + mithril_stake_distribution_list_json.clone(), + ) + }) .or( warp::path!("artifact" / "mithril-stake-distribution" / String) - .map(move |_hash| mithril_stake_distribution_json.clone()), + .and(warp::path::full().map(move |p| p)) + .and(FakeAggregator::with_calls_middleware(self.calls.clone())) + .and_then(move |param, fullpath, calls| { + FakeAggregator::store_call_and_return_value( + vec![param], + fullpath, + calls, + mithril_stake_distribution_json.clone(), + ) + }), ) - .or(warp::path!("certificate" / String).map(move |_hash| certificate_json.clone())), + .or(warp::path!("certificate" / String) + .and(warp::path::full().map(move |p| p)) + .and(FakeAggregator::with_calls_middleware(self.calls.clone())) + .and_then(move |param, fullpath, calls| { + FakeAggregator::store_call_and_return_value( + vec![param], + fullpath, + calls, + certificate_json.clone(), + ) + })), ) } - pub fn spawn_with_certificate(certificate_hash_list: &[String]) -> TestHttpServer { + pub fn spawn_with_certificate(&self, certificate_hash_list: &[String]) -> TestHttpServer { let certificate_json = serde_json::to_string(&CertificateMessage { hash: certificate_hash_list[0].to_string(), ..CertificateMessage::dummy() @@ -86,7 +156,16 @@ impl FakeAggregator { test_http_server( warp::path!("certificates") - .map(move || certificate_list_json.clone()) + .and(warp::path::full().map(move |p| p)) + .and(FakeAggregator::with_calls_middleware(self.calls.clone())) + .and_then(move |fullpath, calls| { + FakeAggregator::store_call_and_return_value( + vec![], + fullpath, + calls, + certificate_list_json.clone(), + ) + }) .or(warp::path!("certificate" / String).map(move |_hash| certificate_json.clone())), ) } @@ -108,6 +187,7 @@ mod file { impl FakeAggregator { #[cfg(feature = "fs")] pub async fn spawn_with_snapshot( + &self, snapshot_digest: &str, certificate_hash: &str, immutable_db: &DummyImmutableDb, @@ -156,31 +236,58 @@ mod file { let certificate_json = serde_json::to_string(&certificate).unwrap(); let routes = warp::path!("artifact" / "snapshots") - .map(move || snapshot_list_json.clone()) - .or( - warp::path!("artifact" / "snapshot" / String).map(move |_digest| { + .and(warp::path::full().map(move |p| p)) + .and(FakeAggregator::with_calls_middleware(self.calls.clone())) + .and_then(move |fullpath, calls| { + FakeAggregator::store_call_and_return_value( + vec![], + fullpath, + calls, + snapshot_list_json.clone(), + ) + }) + .or(warp::path!("artifact" / "snapshot" / String) + .and(warp::path::full().map(move |p| p)) + .and(FakeAggregator::with_calls_middleware(self.calls.clone())) + .and_then(move |param, fullpath, calls| { let data = snapshot_clone.read().unwrap(); - serde_json::to_string(&data.clone()).unwrap() - }), - ) - .or(warp::path!("certificate" / String).map(move |_hash| certificate_json.clone())) - .or(warp::path!("statistics" / "snapshot").map(|| "")); + FakeAggregator::store_call_and_return_value( + vec![param], + fullpath, + calls, + serde_json::to_string(&data.clone()).unwrap(), + ) + })) + .or(warp::path!("certificate" / String) + .and(warp::path::full().map(move |p| p)) + .and(FakeAggregator::with_calls_middleware(self.calls.clone())) + .and_then(move |param, fullpath, calls| { + FakeAggregator::store_call_and_return_value( + vec![param], + fullpath, + calls, + certificate_json.clone(), + ) + })) + .or(warp::path!("statistics" / "snapshot") + .and(warp::path::full().map(move |p| p)) + .and(FakeAggregator::with_calls_middleware(self.calls.clone())) + .and_then(move |fullpath, calls| { + FakeAggregator::store_call_and_return_value( + vec![], + fullpath, + calls, + "".to_string(), + ) + })); let snapshot_archive_path = build_fake_zstd_snapshot(immutable_db, work_dir); let routes = routes.or(warp::path!("artifact" / "snapshot" / String / "download") + .and(warp::path::full().map(move |p| p)) + .and(FakeAggregator::with_calls_middleware(self.calls.clone())) .and(warp::fs::file(snapshot_archive_path)) - .map(|_digest, reply: warp::fs::File| { - let filepath = reply.path().to_path_buf(); - Box::new(warp::reply::with_header( - reply, - "Content-Disposition", - format!( - "attachment; filename=\"{}\"", - filepath.file_name().unwrap().to_str().unwrap() - ), - )) as Box - })); + .and_then(store_call_and_download_return)); let server = test_http_server(routes); update_snapshot_location(&server.url(), snapshot_digest, snapshot); @@ -189,6 +296,26 @@ mod file { } } + pub async fn store_call_and_download_return( + _digest: String, + full_path: FullPath, + calls: FakeAggregatorCalls, + reply: warp::fs::File, + ) -> Result { + let mut call_list = calls.lock().await; + call_list.push(full_path.as_str().to_string()); + + let filepath = reply.path().to_path_buf(); + Ok(Box::new(warp::reply::with_header( + reply, + "Content-Disposition", + format!( + "attachment; filename=\"{}\"", + filepath.file_name().unwrap().to_str().unwrap() + ), + )) as Box) + } + /// Compress the given db into an zstd archive in the given target directory. /// /// return the path to the compressed archive. diff --git a/mithril-client/tests/mithril_stake_distribution_list_get_show_verify.rs b/mithril-client/tests/mithril_stake_distribution_list_get_show_verify.rs index dd89ed2afca..52746425aba 100644 --- a/mithril-client/tests/mithril_stake_distribution_list_get_show_verify.rs +++ b/mithril-client/tests/mithril_stake_distribution_list_get_show_verify.rs @@ -1,7 +1,7 @@ mod extensions; use crate::extensions::fake::{FakeAggregator, FakeCertificateVerifier}; -use mithril_client::{ClientBuilder, MessageBuilder}; +use mithril_client::{aggregator_client::AggregatorRequest, ClientBuilder, MessageBuilder}; #[tokio::test] async fn mithril_stake_distribution_list_get_show_verify() { @@ -9,9 +9,10 @@ async fn mithril_stake_distribution_list_get_show_verify() { mithril_common::test_utils::fake_keys::genesis_verification_key()[0]; let msd_hash = "msd_hash"; let certificate_hash = "certificate_hash"; - let fake_aggregator = - FakeAggregator::spawn_with_mithril_stake_distribution(msd_hash, certificate_hash); - let client = ClientBuilder::aggregator(&fake_aggregator.url(), genesis_verification_key) + let fake_aggregator = FakeAggregator::new(); + let test_http_server = + fake_aggregator.spawn_with_mithril_stake_distribution(msd_hash, certificate_hash); + let client = ClientBuilder::aggregator(&test_http_server.url(), genesis_verification_key) .with_certificate_verifier(FakeCertificateVerifier::build_that_validate_any_certificate()) .build() .expect("Should be able to create a Client"); @@ -21,6 +22,13 @@ async fn mithril_stake_distribution_list_get_show_verify() { .list() .await .expect("List MithrilStakeDistribution should not fail"); + assert_eq!( + fake_aggregator.get_last_call().await, + Some(format!( + "/{}", + AggregatorRequest::ListMithrilStakeDistributions.route() + )) + ); let last_hash = mithril_stake_distributions.first().unwrap().hash.as_ref(); @@ -31,12 +39,32 @@ async fn mithril_stake_distribution_list_get_show_verify() { .unwrap_or_else(|| { panic!("A MithrilStakeDistribution should exist for hash '{last_hash}'") }); + assert_eq!( + fake_aggregator.get_last_call().await, + Some(format!( + "/{}", + AggregatorRequest::GetMithrilStakeDistribution { + hash: (last_hash).to_string() + } + .route() + )) + ); let certificate = client .certificate() .verify_chain(&mithril_stake_distribution.certificate_hash) .await .expect("Validating the chain should not fail"); + assert_eq!( + fake_aggregator.get_last_call().await, + Some(format!( + "/{}", + AggregatorRequest::GetCertificate { + hash: mithril_stake_distribution.certificate_hash.clone() + } + .route() + )) + ); let message = MessageBuilder::new() .compute_mithril_stake_distribution_message(&mithril_stake_distribution) diff --git a/mithril-client/tests/snapshot_list_get_show_download_verify.rs b/mithril-client/tests/snapshot_list_get_show_download_verify.rs index 120ed911399..c0ed4111000 100644 --- a/mithril-client/tests/snapshot_list_get_show_download_verify.rs +++ b/mithril-client/tests/snapshot_list_get_show_download_verify.rs @@ -1,6 +1,7 @@ mod extensions; use crate::extensions::fake::{FakeAggregator, FakeCertificateVerifier}; +use mithril_client::aggregator_client::AggregatorRequest; use mithril_client::feedback::SlogFeedbackReceiver; use mithril_client::{ClientBuilder, MessageBuilder}; use mithril_common::digesters::DummyImmutablesDbBuilder; @@ -17,10 +18,11 @@ async fn snapshot_list_get_show_download_verify() { .with_immutables(&[1, 2, 3]) .append_immutable_trio() .build(); - let fake_aggregator = - FakeAggregator::spawn_with_snapshot(digest, certificate_hash, &immutable_db, &work_dir) - .await; - let client = ClientBuilder::aggregator(&fake_aggregator.url(), genesis_verification_key) + let fake_aggregator = FakeAggregator::new(); + let test_http_server = fake_aggregator + .spawn_with_snapshot(digest, certificate_hash, &immutable_db, &work_dir) + .await; + let client = ClientBuilder::aggregator(&test_http_server.url(), genesis_verification_key) .with_certificate_verifier(FakeCertificateVerifier::build_that_validate_any_certificate()) .add_feedback_receiver(Arc::new(SlogFeedbackReceiver::new( extensions::test_logger(), @@ -33,6 +35,10 @@ async fn snapshot_list_get_show_download_verify() { .list() .await .expect("List MithrilStakeDistribution should not fail"); + assert_eq!( + fake_aggregator.get_last_call().await, + Some(format!("/{}", AggregatorRequest::ListSnapshots.route())) + ); let last_digest = snapshots.first().unwrap().digest.as_ref(); @@ -42,6 +48,16 @@ async fn snapshot_list_get_show_download_verify() { .await .expect("Get Snapshot should not fail ") .unwrap_or_else(|| panic!("A Snapshot should exist for hash '{last_digest}'")); + assert_eq!( + fake_aggregator.get_last_call().await, + Some(format!( + "/{}", + AggregatorRequest::GetSnapshot { + digest: (last_digest.to_string()) + } + .route() + )) + ); let unpacked_dir = work_dir.join("unpack"); std::fs::create_dir(&unpacked_dir).unwrap(); @@ -51,24 +67,42 @@ async fn snapshot_list_get_show_download_verify() { .verify_chain(&snapshot.certificate_hash) .await .expect("Validating the chain should not fail"); + assert_eq!( + fake_aggregator.get_last_call().await, + Some(format!( + "/{}", + AggregatorRequest::GetCertificate { + hash: (snapshot.certificate_hash.clone()) + } + .route() + )) + ); client .snapshot() .download_unpack(&snapshot, &unpacked_dir) .await .expect("download/unpack snapshot should not fail"); + assert_eq!( + fake_aggregator.get_last_call().await, + Some(format!( + "/{}/download", + AggregatorRequest::GetSnapshot { + digest: (snapshot.digest.clone()) + } + .route() + )) + ); client .snapshot() .add_statistics(&snapshot) .await .expect("add_statistics should not fail"); - - // TODO: find a way to verify that the last route called is the right one - // assert_eq!( - // "statistics/snapshot", - // fake_aggregator.get_last_route_called() - // ); + assert_eq!( + fake_aggregator.get_last_call().await, + Some(format!("/{}", AggregatorRequest::AddStatistics.route())) + ); let message = MessageBuilder::new() .compute_snapshot_message(&certificate, &unpacked_dir) From d7dcaa2500cf34c618c667a8f7741423dce983bb Mon Sep 17 00:00:00 2001 From: Damien LACHAUME / PALO-IT Date: Wed, 20 Dec 2023 11:34:34 +0100 Subject: [PATCH 08/13] Reorganize test tools to enhance code readability --- mithril-client/tests/extensions/fake.rs | 160 ++++-------------- mithril-client/tests/extensions/mod.rs | 1 + .../tests/extensions/routes/certificate.rs | 54 ++++++ .../routes/mithril_stake_distribution.rs | 49 ++++++ mithril-client/tests/extensions/routes/mod.rs | 16 ++ .../tests/extensions/routes/snapshot.rs | 87 ++++++++++ .../tests/extensions/routes/statistics.rs | 21 +++ 7 files changed, 259 insertions(+), 129 deletions(-) create mode 100644 mithril-client/tests/extensions/routes/certificate.rs create mode 100644 mithril-client/tests/extensions/routes/mithril_stake_distribution.rs create mode 100644 mithril-client/tests/extensions/routes/mod.rs create mode 100644 mithril-client/tests/extensions/routes/snapshot.rs create mode 100644 mithril-client/tests/extensions/routes/statistics.rs diff --git a/mithril-client/tests/extensions/fake.rs b/mithril-client/tests/extensions/fake.rs index 0a9a34212bc..28aa047c8d0 100644 --- a/mithril-client/tests/extensions/fake.rs +++ b/mithril-client/tests/extensions/fake.rs @@ -1,3 +1,5 @@ +use super::routes; +use crate::extensions::mock; use mithril_client::certificate_client::CertificateVerifier; use mithril_client::{ MessageBuilder, MithrilCertificateListItem, MithrilStakeDistribution, @@ -11,9 +13,7 @@ use tokio::sync::Mutex; use warp::filters::path::FullPath; use warp::Filter; -use crate::extensions::mock; - -type FakeAggregatorCalls = Arc>>; +pub type FakeAggregatorCalls = Arc>>; pub struct FakeCertificateVerifier; @@ -47,13 +47,7 @@ impl FakeAggregator { self.get_calls().await.last().cloned() } - fn with_calls_middleware( - calls: FakeAggregatorCalls, - ) -> impl Filter + Clone { - warp::any().map(move || calls.clone()) - } - - async fn store_call_and_return_value( + pub async fn store_call_and_return_value( _params: Vec, full_path: FullPath, calls: FakeAggregatorCalls, @@ -99,41 +93,16 @@ impl FakeAggregator { .unwrap(); test_http_server( - warp::path!("artifact" / "mithril-stake-distributions") - .and(warp::path::full().map(move |p| p)) - .and(FakeAggregator::with_calls_middleware(self.calls.clone())) - .and_then(move |fullpath, calls| { - FakeAggregator::store_call_and_return_value( - vec![], - fullpath, - calls, - mithril_stake_distribution_list_json.clone(), - ) - }) - .or( - warp::path!("artifact" / "mithril-stake-distribution" / String) - .and(warp::path::full().map(move |p| p)) - .and(FakeAggregator::with_calls_middleware(self.calls.clone())) - .and_then(move |param, fullpath, calls| { - FakeAggregator::store_call_and_return_value( - vec![param], - fullpath, - calls, - mithril_stake_distribution_json.clone(), - ) - }), - ) - .or(warp::path!("certificate" / String) - .and(warp::path::full().map(move |p| p)) - .and(FakeAggregator::with_calls_middleware(self.calls.clone())) - .and_then(move |param, fullpath, calls| { - FakeAggregator::store_call_and_return_value( - vec![param], - fullpath, - calls, - certificate_json.clone(), - ) - })), + routes::mithril_stake_distribution::routes( + self.calls.clone(), + mithril_stake_distribution_list_json, + mithril_stake_distribution_json, + ) + .or(routes::certificate::routes( + self.calls.clone(), + None, + certificate_json, + )), ) } @@ -154,20 +123,11 @@ impl FakeAggregator { ) .unwrap(); - test_http_server( - warp::path!("certificates") - .and(warp::path::full().map(move |p| p)) - .and(FakeAggregator::with_calls_middleware(self.calls.clone())) - .and_then(move |fullpath, calls| { - FakeAggregator::store_call_and_return_value( - vec![], - fullpath, - calls, - certificate_list_json.clone(), - ) - }) - .or(warp::path!("certificate" / String).map(move |_hash| certificate_json.clone())), - ) + test_http_server(routes::certificate::routes( + self.calls.clone(), + Some(certificate_list_json), + certificate_json, + )) } } @@ -235,59 +195,21 @@ mod file { .compute_hash(); let certificate_json = serde_json::to_string(&certificate).unwrap(); - let routes = warp::path!("artifact" / "snapshots") - .and(warp::path::full().map(move |p| p)) - .and(FakeAggregator::with_calls_middleware(self.calls.clone())) - .and_then(move |fullpath, calls| { - FakeAggregator::store_call_and_return_value( - vec![], - fullpath, - calls, - snapshot_list_json.clone(), - ) - }) - .or(warp::path!("artifact" / "snapshot" / String) - .and(warp::path::full().map(move |p| p)) - .and(FakeAggregator::with_calls_middleware(self.calls.clone())) - .and_then(move |param, fullpath, calls| { - let data = snapshot_clone.read().unwrap(); - FakeAggregator::store_call_and_return_value( - vec![param], - fullpath, - calls, - serde_json::to_string(&data.clone()).unwrap(), - ) - })) - .or(warp::path!("certificate" / String) - .and(warp::path::full().map(move |p| p)) - .and(FakeAggregator::with_calls_middleware(self.calls.clone())) - .and_then(move |param, fullpath, calls| { - FakeAggregator::store_call_and_return_value( - vec![param], - fullpath, - calls, - certificate_json.clone(), - ) - })) - .or(warp::path!("statistics" / "snapshot") - .and(warp::path::full().map(move |p| p)) - .and(FakeAggregator::with_calls_middleware(self.calls.clone())) - .and_then(move |fullpath, calls| { - FakeAggregator::store_call_and_return_value( - vec![], - fullpath, - calls, - "".to_string(), - ) - })); + let routes = + routes::snapshot::routes(self.calls.clone(), snapshot_list_json, snapshot_clone) + .or(routes::certificate::routes( + self.calls.clone(), + None, + certificate_json, + )) + .or(routes::statistics::routes(self.calls.clone())); let snapshot_archive_path = build_fake_zstd_snapshot(immutable_db, work_dir); - let routes = routes.or(warp::path!("artifact" / "snapshot" / String / "download") - .and(warp::path::full().map(move |p| p)) - .and(FakeAggregator::with_calls_middleware(self.calls.clone())) - .and(warp::fs::file(snapshot_archive_path)) - .and_then(store_call_and_download_return)); + let routes = routes.or(routes::snapshot::download( + self.calls.clone(), + snapshot_archive_path, + )); let server = test_http_server(routes); update_snapshot_location(&server.url(), snapshot_digest, snapshot); @@ -296,26 +218,6 @@ mod file { } } - pub async fn store_call_and_download_return( - _digest: String, - full_path: FullPath, - calls: FakeAggregatorCalls, - reply: warp::fs::File, - ) -> Result { - let mut call_list = calls.lock().await; - call_list.push(full_path.as_str().to_string()); - - let filepath = reply.path().to_path_buf(); - Ok(Box::new(warp::reply::with_header( - reply, - "Content-Disposition", - format!( - "attachment; filename=\"{}\"", - filepath.file_name().unwrap().to_str().unwrap() - ), - )) as Box) - } - /// Compress the given db into an zstd archive in the given target directory. /// /// return the path to the compressed archive. diff --git a/mithril-client/tests/extensions/mod.rs b/mithril-client/tests/extensions/mod.rs index 573b031ce9f..168344ab2b3 100644 --- a/mithril-client/tests/extensions/mod.rs +++ b/mithril-client/tests/extensions/mod.rs @@ -4,6 +4,7 @@ pub mod fake; pub mod mock; +mod routes; use std::path::PathBuf; diff --git a/mithril-client/tests/extensions/routes/certificate.rs b/mithril-client/tests/extensions/routes/certificate.rs new file mode 100644 index 00000000000..ad47f4ee438 --- /dev/null +++ b/mithril-client/tests/extensions/routes/certificate.rs @@ -0,0 +1,54 @@ +use super::middleware::with_calls_middleware; +use crate::extensions::fake::{FakeAggregator, FakeAggregatorCalls}; +use warp::Filter; + +pub fn routes( + calls: FakeAggregatorCalls, + certificate_certificates_returned_value: Option, + certificate_certificate_hash_returned_value: String, +) -> impl Filter + Clone { + certificate_certificates( + calls.clone(), + certificate_certificates_returned_value.unwrap_or_default(), + ) + .or(certificate_certificate_hash( + calls, + certificate_certificate_hash_returned_value, + )) +} + +/// Route: /certificates +fn certificate_certificates( + calls: FakeAggregatorCalls, + returned_value: String, +) -> impl Filter + Clone { + warp::path!("certificates") + .and(warp::path::full().map(move |p| p)) + .and(with_calls_middleware(calls.clone())) + .and_then(move |fullpath, calls| { + FakeAggregator::store_call_and_return_value( + vec![], + fullpath, + calls, + returned_value.clone(), + ) + }) +} + +/// Route: /certificate/{certificate_hash} +fn certificate_certificate_hash( + calls: FakeAggregatorCalls, + returned_value: String, +) -> impl Filter + Clone { + warp::path!("certificate" / String) + .and(warp::path::full().map(move |p| p)) + .and(with_calls_middleware(calls.clone())) + .and_then(move |param, fullpath, calls| { + FakeAggregator::store_call_and_return_value( + vec![param], + fullpath, + calls, + returned_value.clone(), + ) + }) +} diff --git a/mithril-client/tests/extensions/routes/mithril_stake_distribution.rs b/mithril-client/tests/extensions/routes/mithril_stake_distribution.rs new file mode 100644 index 00000000000..a2260134325 --- /dev/null +++ b/mithril-client/tests/extensions/routes/mithril_stake_distribution.rs @@ -0,0 +1,49 @@ +use super::middleware::with_calls_middleware; +use crate::extensions::fake::{FakeAggregator, FakeAggregatorCalls}; +use warp::Filter; + +pub fn routes( + calls: FakeAggregatorCalls, + stake_distributions_returned_value: String, + stake_distribution_by_id_returned_value: String, +) -> impl Filter + Clone { + mithril_stake_distributions(calls.clone(), stake_distributions_returned_value).or( + mithril_stake_distribution_by_id(calls, stake_distribution_by_id_returned_value), + ) +} + +/// Route: /artifact/mithril-stake-distributions +fn mithril_stake_distributions( + calls: FakeAggregatorCalls, + returned_value: String, +) -> impl Filter + Clone { + warp::path!("artifact" / "mithril-stake-distributions") + .and(warp::path::full().map(move |p| p)) + .and(with_calls_middleware(calls.clone())) + .and_then(move |fullpath, calls| { + FakeAggregator::store_call_and_return_value( + vec![], + fullpath, + calls, + returned_value.clone(), + ) + }) +} + +/// Route: /artifact/mithril-stake-distribution/:id +fn mithril_stake_distribution_by_id( + calls: FakeAggregatorCalls, + returned_value: String, +) -> impl Filter + Clone { + warp::path!("artifact" / "mithril-stake-distribution" / String) + .and(warp::path::full().map(move |p| p)) + .and(with_calls_middleware(calls.clone())) + .and_then(move |param, fullpath, calls| { + FakeAggregator::store_call_and_return_value( + vec![param], + fullpath, + calls, + returned_value.clone(), + ) + }) +} diff --git a/mithril-client/tests/extensions/routes/mod.rs b/mithril-client/tests/extensions/routes/mod.rs new file mode 100644 index 00000000000..028c9fa70c9 --- /dev/null +++ b/mithril-client/tests/extensions/routes/mod.rs @@ -0,0 +1,16 @@ +pub mod certificate; +pub mod mithril_stake_distribution; +pub mod snapshot; +pub mod statistics; + +mod middleware { + use crate::extensions::fake::FakeAggregatorCalls; + use std::convert::Infallible; + use warp::Filter; + + pub fn with_calls_middleware( + calls: FakeAggregatorCalls, + ) -> impl Filter + Clone { + warp::any().map(move || calls.clone()) + } +} diff --git a/mithril-client/tests/extensions/routes/snapshot.rs b/mithril-client/tests/extensions/routes/snapshot.rs new file mode 100644 index 00000000000..b1fce732805 --- /dev/null +++ b/mithril-client/tests/extensions/routes/snapshot.rs @@ -0,0 +1,87 @@ +use super::middleware::with_calls_middleware; +use crate::extensions::fake::{FakeAggregator, FakeAggregatorCalls}; +use mithril_client::Snapshot; +use std::{ + convert::Infallible, + path::PathBuf, + sync::{Arc, RwLock}, +}; +use warp::{filters::path::FullPath, Filter}; + +pub fn routes( + calls: FakeAggregatorCalls, + snapshots_returned_value: String, + snapshot_by_id_returned_value: Arc>, +) -> impl Filter + Clone { + snapshots(calls.clone(), snapshots_returned_value) + .or(snapshot_by_id(calls.clone(), snapshot_by_id_returned_value)) +} + +/// Route: /artifact/snapshots +fn snapshots( + calls: FakeAggregatorCalls, + returned_value: String, +) -> impl Filter + Clone { + warp::path!("artifact" / "snapshots") + .and(warp::path::full().map(move |p| p)) + .and(with_calls_middleware(calls.clone())) + .and_then(move |fullpath, calls| { + FakeAggregator::store_call_and_return_value( + vec![], + fullpath, + calls, + returned_value.clone(), + ) + }) +} + +/// Route: /artifact/snapshot/:id +fn snapshot_by_id( + calls: FakeAggregatorCalls, + returned_value: Arc>, +) -> impl Filter + Clone { + warp::path!("artifact" / "snapshot" / String) + .and(warp::path::full().map(move |p| p)) + .and(with_calls_middleware(calls.clone())) + .and_then(move |param, fullpath, calls| { + let data = returned_value.read().unwrap(); + FakeAggregator::store_call_and_return_value( + vec![param], + fullpath, + calls, + serde_json::to_string(&data.clone()).unwrap(), + ) + }) +} + +/// Route: /artifact/snapshots/{digest}/download +pub fn download( + calls: FakeAggregatorCalls, + archive_path: PathBuf, +) -> impl Filter + Clone { + warp::path!("artifact" / "snapshot" / String / "download") + .and(warp::path::full().map(move |p| p)) + .and(with_calls_middleware(calls.clone())) + .and(warp::fs::file(archive_path)) + .and_then(store_call_and_download_return) +} + +async fn store_call_and_download_return( + _digest: String, + full_path: FullPath, + calls: FakeAggregatorCalls, + reply: warp::fs::File, +) -> Result { + let mut call_list = calls.lock().await; + call_list.push(full_path.as_str().to_string()); + + let filepath = reply.path().to_path_buf(); + Ok(Box::new(warp::reply::with_header( + reply, + "Content-Disposition", + format!( + "attachment; filename=\"{}\"", + filepath.file_name().unwrap().to_str().unwrap() + ), + )) as Box) +} diff --git a/mithril-client/tests/extensions/routes/statistics.rs b/mithril-client/tests/extensions/routes/statistics.rs new file mode 100644 index 00000000000..e8b6ce52375 --- /dev/null +++ b/mithril-client/tests/extensions/routes/statistics.rs @@ -0,0 +1,21 @@ +use super::middleware::with_calls_middleware; +use crate::extensions::fake::{FakeAggregator, FakeAggregatorCalls}; +use warp::Filter; + +pub fn routes( + calls: FakeAggregatorCalls, +) -> impl Filter + Clone { + add_statistics(calls.clone()) +} + +/// Route: /statistics/snapshot +fn add_statistics( + calls: FakeAggregatorCalls, +) -> impl Filter + Clone { + warp::path!("statistics" / "snapshot") + .and(warp::path::full().map(move |p| p)) + .and(with_calls_middleware(calls.clone())) + .and_then(move |fullpath, calls| { + FakeAggregator::store_call_and_return_value(vec![], fullpath, calls, "".to_string()) + }) +} From 8931101ab386e558ecfa878a6809b024e622d6bc Mon Sep 17 00:00:00 2001 From: Damien LACHAUME / PALO-IT Date: Tue, 19 Dec 2023 17:41:20 +0100 Subject: [PATCH 09/13] Refactor snapshot download command - Split `execute` function - Use `mithril-client` types instead of `mithril-common` types --- .../src/commands/snapshot/download.rs | 156 +++++++++++++++--- .../src/commands/snapshot/list.rs | 4 +- mithril-client-cli/src/lib.rs | 14 -- mithril-client-cli/src/main.rs | 2 +- 4 files changed, 132 insertions(+), 44 deletions(-) diff --git a/mithril-client-cli/src/commands/snapshot/download.rs b/mithril-client-cli/src/commands/snapshot/download.rs index f649d7bc974..d96234b42ba 100644 --- a/mithril-client-cli/src/commands/snapshot/download.rs +++ b/mithril-client-cli/src/commands/snapshot/download.rs @@ -10,7 +10,9 @@ use std::{ sync::Arc, }; -use mithril_client::{ClientBuilder, MessageBuilder}; +use mithril_client::{ + common::ProtocolMessage, Client, ClientBuilder, MessageBuilder, MithrilCertificate, Snapshot, +}; use mithril_client_cli::{ configuration::ConfigParameters, utils::{ @@ -90,24 +92,86 @@ impl SnapshotDownloadCommand { .await? .with_context(|| format!("Can not get the snapshot for digest: '{}'", self.digest))?; - progress_printer.report_step(1, "Checking local disk info…")?; - if let Err(e) = SnapshotUnpacker::check_prerequisites( + SnapshotDownloadCommand::check_local_disk_info( + 1, + &progress_printer, + &db_dir, + &snapshot_message, + )?; + + let certificate = SnapshotDownloadCommand::fetching_certificate_and_verifying_chain( + 2, + &progress_printer, + &client, + &snapshot_message, + ) + .await?; + + SnapshotDownloadCommand::downloading_and_unpacking_snapshot( + 3, + &progress_printer, + &client, + &snapshot_message, &db_dir, + ) + .await?; + + let message = SnapshotDownloadCommand::computing_snapshot_digest( + 4, + &progress_printer, + &certificate, + &db_dir, + ) + .await?; + + SnapshotDownloadCommand::verifying_snapshot_signature( + 5, + &progress_printer, + &certificate, + &message, + &snapshot_message, + ) + .await?; + + SnapshotDownloadCommand::log_download_information(&db_dir, &snapshot_message, self.json)?; + + Ok(()) + } + + fn check_local_disk_info( + step_number: u16, + progress_printer: &ProgressPrinter, + db_dir: &PathBuf, + snapshot_message: &Snapshot, + ) -> StdResult<()> { + progress_printer.report_step(step_number, "Checking local disk info…")?; + if let Err(e) = SnapshotUnpacker::check_prerequisites( + db_dir, snapshot_message.size, snapshot_message.compression_algorithm.unwrap_or_default(), ) { - progress_printer.report_step(1, &SnapshotUtils::check_disk_space_error(e)?)?; + progress_printer + .report_step(step_number, &SnapshotUtils::check_disk_space_error(e)?)?; } - std::fs::create_dir_all(&db_dir).with_context(|| { + std::fs::create_dir_all(db_dir).with_context(|| { format!( "Download: could not create target directory '{}'.", db_dir.display() ) })?; + Ok(()) + } + + async fn fetching_certificate_and_verifying_chain( + step_number: u16, + progress_printer: &ProgressPrinter, + client: &Client, + snapshot_message: &Snapshot, + ) -> StdResult { progress_printer.report_step( - 2, + step_number, "Fetching the certificate and verifying the certificate chain…", )?; let certificate = client @@ -117,25 +181,35 @@ impl SnapshotDownloadCommand { .with_context(|| { format!( "Can not verify the certificate chain from certificate_hash: '{}'", - &snapshot_message.certificate_hash + snapshot_message.certificate_hash ) })?; - progress_printer.report_step(3, "Downloading and unpacking the snapshot…")?; + Ok(certificate) + } + + async fn downloading_and_unpacking_snapshot( + step_number: u16, + progress_printer: &ProgressPrinter, + client: &Client, + snapshot_message: &Snapshot, + db_dir: &Path, + ) -> StdResult<()> { + progress_printer.report_step(step_number, "Downloading and unpacking the snapshot…")?; client .snapshot() - .download_unpack(&snapshot_message, &db_dir) + .download_unpack(snapshot_message, db_dir) .await .with_context(|| { format!( "Snapshot Service can not download and verify the snapshot for digest: '{}'", - self.digest + snapshot_message.digest ) })?; // The snapshot download does not fail if the statistic call fails. // It would be nice to implement tests to verify the behavior of `add_statistics` - if let Err(e) = client.snapshot().add_statistics(&snapshot_message).await { + if let Err(e) = client.snapshot().add_statistics(snapshot_message).await { warn!("Could not POST snapshot download statistics: {e:?}"); } @@ -147,21 +221,40 @@ impl SnapshotDownloadCommand { ); }; - progress_printer.report_step(4, "Computing the snapshot digest…")?; + Ok(()) + } + + async fn computing_snapshot_digest( + step_number: u16, + progress_printer: &ProgressPrinter, + certificate: &MithrilCertificate, + db_dir: &Path, + ) -> StdResult { + progress_printer.report_step(step_number, "Computing the snapshot digest…")?; let message = SnapshotUtils::wait_spinner( - &progress_printer, - MessageBuilder::new().compute_snapshot_message(&certificate, &db_dir), + progress_printer, + MessageBuilder::new().compute_snapshot_message(certificate, db_dir), ) .await .with_context(|| { format!( "Can not compute the snapshot message from the directory: '{:?}'", - &db_dir + db_dir ) })?; - progress_printer.report_step(5, "Verifying the snapshot signature…")?; - if !certificate.match_message(&message) { + Ok(message) + } + + async fn verifying_snapshot_signature( + step_number: u16, + progress_printer: &ProgressPrinter, + certificate: &MithrilCertificate, + message: &ProtocolMessage, + snapshot_message: &Snapshot, + ) -> StdResult<()> { + progress_printer.report_step(step_number, "Verifying the snapshot signature…")?; + if !certificate.match_message(message) { debug!("Digest verification failed, removing unpacked files & directory."); return Err(anyhow!( @@ -170,6 +263,14 @@ impl SnapshotDownloadCommand { )); } + Ok(()) + } + + fn log_download_information( + db_dir: &Path, + snapshot_message: &Snapshot, + json_output: bool, + ) -> StdResult<()> { let canonicalized_filepath = &db_dir.canonicalize().with_context(|| { format!( "Could not get canonicalized filepath of '{}'", @@ -177,7 +278,7 @@ impl SnapshotDownloadCommand { ) })?; - if self.json { + if json_output { println!( r#"{{"timestamp": "{}", "db_directory": "{}"}}"#, Utc::now().to_rfc3339(), @@ -186,18 +287,19 @@ impl SnapshotDownloadCommand { } else { println!( r###"Snapshot '{}' has been unpacked and successfully checked against Mithril multi-signature contained in the certificate. - -Files in the directory '{}' can be used to run a Cardano node with version >= {}. - -If you are using Cardano Docker image, you can restore a Cardano Node with: - -docker run -v cardano-node-ipc:/ipc -v cardano-node-data:/data --mount type=bind,source="{}",target=/data/db/ -e NETWORK={} inputoutput/cardano-node:8.1.2 - -"###, - &self.digest, + + Files in the directory '{}' can be used to run a Cardano node with version >= {}. + + If you are using Cardano Docker image, you can restore a Cardano Node with: + + docker run -v cardano-node-ipc:/ipc -v cardano-node-data:/data --mount type=bind,source="{}",target=/data/db/ -e NETWORK={} inputoutput/cardano-node:8.1.2 + + "###, + snapshot_message.digest, db_dir.display(), snapshot_message .cardano_node_version + .clone() .unwrap_or("latest".to_string()), canonicalized_filepath.display(), snapshot_message.beacon.network, diff --git a/mithril-client-cli/src/commands/snapshot/list.rs b/mithril-client-cli/src/commands/snapshot/list.rs index 3d93ab227ce..2b20ac41520 100644 --- a/mithril-client-cli/src/commands/snapshot/list.rs +++ b/mithril-client-cli/src/commands/snapshot/list.rs @@ -1,12 +1,12 @@ use clap::Parser; use cli_table::{format::Justify, print_stdout, Cell, Table}; use config::{builder::DefaultState, ConfigBuilder}; -use mithril_common::test_utils::fake_keys; use slog_scope::logger; use std::{collections::HashMap, sync::Arc}; use mithril_client::ClientBuilder; -use mithril_client_cli::{common::StdResult, configuration::ConfigParameters}; +use mithril_client_cli::configuration::ConfigParameters; +use mithril_common::{test_utils::fake_keys, StdResult}; /// Clap command to list existing snapshots #[derive(Parser, Debug, Clone)] diff --git a/mithril-client-cli/src/lib.rs b/mithril-client-cli/src/lib.rs index 3d975f59750..e3830564c28 100644 --- a/mithril-client-cli/src/lib.rs +++ b/mithril-client-cli/src/lib.rs @@ -1,16 +1,2 @@ pub mod configuration; pub mod utils; - -/// `mithril-common` re-exports -pub mod common { - pub use mithril_common::{ - certificate_chain::CertificateVerifier, - crypto_helper::{ProtocolGenesisVerificationKey, ProtocolGenesisVerifier}, - entities::{Beacon, CompressionAlgorithm, Epoch}, - messages::{ - MithrilStakeDistributionListMessage, SnapshotListItemMessage, SnapshotListMessage, - SnapshotMessage, - }, - StdError, StdResult, - }; -} diff --git a/mithril-client-cli/src/main.rs b/mithril-client-cli/src/main.rs index 03c6766d778..1efd304d5a5 100644 --- a/mithril-client-cli/src/main.rs +++ b/mithril-client-cli/src/main.rs @@ -13,7 +13,7 @@ use std::io::Write; use std::sync::Arc; use std::{fs::File, path::PathBuf}; -use mithril_client_cli::common::StdResult; +use mithril_common::StdResult; use commands::{ mithril_stake_distribution::MithrilStakeDistributionCommands, snapshot::SnapshotCommands, From 5f4ff94d9cdfd0b724ea5fceae90dc1d7a5c42dc Mon Sep 17 00:00:00 2001 From: Damien LACHAUME / PALO-IT Date: Wed, 20 Dec 2023 18:35:56 +0100 Subject: [PATCH 10/13] PR review: fix typos --- .../nodes/mithril-client-library.md | 6 +++--- examples/client-snapshot/README.md | 2 +- examples/client-snapshot/src/main.rs | 2 +- .../src/commands/snapshot/download.rs | 2 +- mithril-client/README.md | 4 ++-- mithril-client/src/aggregator_client.rs | 8 ++++---- mithril-client/src/lib.rs | 4 ++-- mithril-client/src/snapshot_client.rs | 6 +++--- mithril-client/tests/extensions/fake.rs | 1 - .../tests/extensions/routes/certificate.rs | 16 +++------------- .../routes/mithril_stake_distribution.rs | 16 +++------------- .../tests/extensions/routes/snapshot.rs | 10 ++-------- .../tests/extensions/routes/statistics.rs | 2 +- .../snapshot_list_get_show_download_verify.rs | 5 ++++- 14 files changed, 30 insertions(+), 54 deletions(-) diff --git a/docs/website/root/manual/developer-docs/nodes/mithril-client-library.md b/docs/website/root/manual/developer-docs/nodes/mithril-client-library.md index 7b500fd6900..7fa5bade0cc 100644 --- a/docs/website/root/manual/developer-docs/nodes/mithril-client-library.md +++ b/docs/website/root/manual/developer-docs/nodes/mithril-client-library.md @@ -12,7 +12,7 @@ import CompiledBinaries from '../../../compiled-binaries.md' Mithril client library can be used by Rust developers to use the Mithril network in their applications. It is responsible for handling the different types of data certified by Mithril, and available through a Mithril aggregator: -- [**Snapshot**](../../../glossary.md#snapshot): list, get, download tarball and add statistics. +- [**Snapshot**](../../../glossary.md#snapshot): list, get, download tarball and record statistics. - [**Mithril stake distribution**](../../../glossary.md#stake-distribution): list and get. - [**Certificate**](../../../glossary.md#certificate): list, get, and chain validation. @@ -90,7 +90,7 @@ async fn main() -> mithril_client::MithrilResult<()> { .await?; if let Err(e) = client.snapshot().add_statistics(&snapshot).await { - println!("Could not POST snapshot download statistics: {:?}", e); + println!("Could not increment snapshot download statistics: {:?}", e); } let message = MessageBuilder::new() @@ -148,7 +148,7 @@ async fn main() -> mithril_client::MithrilResult<()> { .await?; if let Err(e) = client.snapshot().add_statistics(&snapshot).await { - println!("Could not POST snapshot download statistics: {:?}", e); + println!("Could not increment snapshot download statistics: {:?}", e); } let message = MessageBuilder::new() diff --git a/examples/client-snapshot/README.md b/examples/client-snapshot/README.md index 462985c4775..17d72616c28 100644 --- a/examples/client-snapshot/README.md +++ b/examples/client-snapshot/README.md @@ -11,7 +11,7 @@ In this example, the client interacts with a real aggregator on the network `tes - verify a certificate chain - compute a message for a Snapshot - verify that the certificate signs the computed message -- increments download statistics +- increments snapshot download statistics The crate [indicatif](https://docs.rs/indicatif/latest/indicatif/) is used to nicely report the progress to the console. diff --git a/examples/client-snapshot/src/main.rs b/examples/client-snapshot/src/main.rs index 4baec5f1ad1..49ff194acba 100644 --- a/examples/client-snapshot/src/main.rs +++ b/examples/client-snapshot/src/main.rs @@ -56,7 +56,7 @@ async fn main() -> MithrilResult<()> { .await?; if let Err(e) = client.snapshot().add_statistics(&snapshot).await { - println!("Could not POST snapshot download statistics: {:?}", e); + println!("Could not increment snapshot download statistics: {:?}", e); } println!("Computing snapshot '{}' message ...", snapshot.digest); diff --git a/mithril-client-cli/src/commands/snapshot/download.rs b/mithril-client-cli/src/commands/snapshot/download.rs index d96234b42ba..9cf4afe2e86 100644 --- a/mithril-client-cli/src/commands/snapshot/download.rs +++ b/mithril-client-cli/src/commands/snapshot/download.rs @@ -210,7 +210,7 @@ impl SnapshotDownloadCommand { // The snapshot download does not fail if the statistic call fails. // It would be nice to implement tests to verify the behavior of `add_statistics` if let Err(e) = client.snapshot().add_statistics(snapshot_message).await { - warn!("Could not POST snapshot download statistics: {e:?}"); + warn!("Could not increment snapshot download statistics: {e:?}"); } // Append 'clean' file to speedup node bootstrap diff --git a/mithril-client/README.md b/mithril-client/README.md index 4ddf1b03fb6..2e10fcfab48 100644 --- a/mithril-client/README.md +++ b/mithril-client/README.md @@ -5,7 +5,7 @@ * `mithril-client` defines all the tooling necessary to manipulate Mithril certified types available from a Mithril aggregator. * The different types of available data certified by Mithril are: - * Snapshot: list, get, download tarball and add statistics. + * Snapshot: list, get, download tarball and record statistics. * Mithril stake distribution: list and get. * Certificate: list, get, and chain validation. @@ -39,7 +39,7 @@ async fn main() -> mithril_client::MithrilResult<()> { .await?; if let Err(e) = client.snapshot().add_statistics(&snapshot).await { - println!("Could not POST snapshot download statistics: {:?}", e); + println!("Could not increment snapshot download statistics: {:?}", e); } let message = MessageBuilder::new() diff --git a/mithril-client/src/aggregator_client.rs b/mithril-client/src/aggregator_client.rs index 9cf71561385..1b296af20f9 100644 --- a/mithril-client/src/aggregator_client.rs +++ b/mithril-client/src/aggregator_client.rs @@ -69,8 +69,8 @@ pub enum AggregatorRequest { /// Lists the aggregator [snapshots][crate::Snapshot] ListSnapshots, - /// Increments Aggregator's download statistics - AddStatistics, + /// Increments the aggregator snapshot download statistics + IncrementSnapshotStatistic, } impl AggregatorRequest { @@ -91,7 +91,7 @@ impl AggregatorRequest { format!("artifact/snapshot/{}", digest) } AggregatorRequest::ListSnapshots => "artifact/snapshots".to_string(), - AggregatorRequest::AddStatistics => "statistics/snapshot".to_string(), + AggregatorRequest::IncrementSnapshotStatistic => "statistics/snapshot".to_string(), } } } @@ -106,7 +106,7 @@ pub trait AggregatorClient: Sync + Send { request: AggregatorRequest, ) -> Result; - /// Post information to the Aggregator, the URL is a relative path for a resource + /// Post information to the Aggregator async fn post_content( &self, request: AggregatorRequest, diff --git a/mithril-client/src/lib.rs b/mithril-client/src/lib.rs index 5a3f671072e..2d6460b4590 100644 --- a/mithril-client/src/lib.rs +++ b/mithril-client/src/lib.rs @@ -6,7 +6,7 @@ //! //! It handles the different types that can be queried to a Mithril aggregator: //! -//! - [Snapshot][snapshot_client] list, get, download tarball and add statistics. +//! - [Snapshot][snapshot_client] list, get, download tarball and record statistics. //! - [Mithril stake distribution][mithril_stake_distribution_client] list and get. //! - [Certificates][certificate_client] list, get, and chain validation. //! @@ -47,7 +47,7 @@ //! .await?; //! //! if let Err(e) = client.snapshot().add_statistics(&snapshot).await { -//! println!("Could not POST snapshot download statistics: {:?}", e); +//! println!("Could not increment snapshot download statistics: {:?}", e); //! } //! //! diff --git a/mithril-client/src/snapshot_client.rs b/mithril-client/src/snapshot_client.rs index 8ec44f6c243..2a1ca820672 100644 --- a/mithril-client/src/snapshot_client.rs +++ b/mithril-client/src/snapshot_client.rs @@ -66,7 +66,7 @@ //! # Add statistics //! **Note:** _Available on crate feature_ **fs** _only._ //! -//! Increments Aggregator's download statistics using the [ClientBuilder][crate::client::ClientBuilder]. +//! Increments the aggregator snapshot download statistics using the [ClientBuilder][crate::client::ClientBuilder]. //! //! ```no_run //! # async fn run() -> mithril_client::MithrilResult<()> { @@ -239,12 +239,12 @@ impl SnapshotClient { } } - /// Increments Aggregator's download statistics + /// Increments the aggregator snapshot download statistics pub async fn add_statistics(&self, snapshot: &Snapshot) -> MithrilResult<()> { let _response = self .aggregator_client .post_content( - AggregatorRequest::AddStatistics, + AggregatorRequest::IncrementSnapshotStatistic, &serde_json::to_string(snapshot)?, ) .await?; diff --git a/mithril-client/tests/extensions/fake.rs b/mithril-client/tests/extensions/fake.rs index 28aa047c8d0..a5766bb4b53 100644 --- a/mithril-client/tests/extensions/fake.rs +++ b/mithril-client/tests/extensions/fake.rs @@ -48,7 +48,6 @@ impl FakeAggregator { } pub async fn store_call_and_return_value( - _params: Vec, full_path: FullPath, calls: FakeAggregatorCalls, returned_value: String, diff --git a/mithril-client/tests/extensions/routes/certificate.rs b/mithril-client/tests/extensions/routes/certificate.rs index ad47f4ee438..0eba6216eb6 100644 --- a/mithril-client/tests/extensions/routes/certificate.rs +++ b/mithril-client/tests/extensions/routes/certificate.rs @@ -26,12 +26,7 @@ fn certificate_certificates( .and(warp::path::full().map(move |p| p)) .and(with_calls_middleware(calls.clone())) .and_then(move |fullpath, calls| { - FakeAggregator::store_call_and_return_value( - vec![], - fullpath, - calls, - returned_value.clone(), - ) + FakeAggregator::store_call_and_return_value(fullpath, calls, returned_value.clone()) }) } @@ -43,12 +38,7 @@ fn certificate_certificate_hash( warp::path!("certificate" / String) .and(warp::path::full().map(move |p| p)) .and(with_calls_middleware(calls.clone())) - .and_then(move |param, fullpath, calls| { - FakeAggregator::store_call_and_return_value( - vec![param], - fullpath, - calls, - returned_value.clone(), - ) + .and_then(move |_param, fullpath, calls| { + FakeAggregator::store_call_and_return_value(fullpath, calls, returned_value.clone()) }) } diff --git a/mithril-client/tests/extensions/routes/mithril_stake_distribution.rs b/mithril-client/tests/extensions/routes/mithril_stake_distribution.rs index a2260134325..3392d5bc95a 100644 --- a/mithril-client/tests/extensions/routes/mithril_stake_distribution.rs +++ b/mithril-client/tests/extensions/routes/mithril_stake_distribution.rs @@ -21,12 +21,7 @@ fn mithril_stake_distributions( .and(warp::path::full().map(move |p| p)) .and(with_calls_middleware(calls.clone())) .and_then(move |fullpath, calls| { - FakeAggregator::store_call_and_return_value( - vec![], - fullpath, - calls, - returned_value.clone(), - ) + FakeAggregator::store_call_and_return_value(fullpath, calls, returned_value.clone()) }) } @@ -38,12 +33,7 @@ fn mithril_stake_distribution_by_id( warp::path!("artifact" / "mithril-stake-distribution" / String) .and(warp::path::full().map(move |p| p)) .and(with_calls_middleware(calls.clone())) - .and_then(move |param, fullpath, calls| { - FakeAggregator::store_call_and_return_value( - vec![param], - fullpath, - calls, - returned_value.clone(), - ) + .and_then(move |_param, fullpath, calls| { + FakeAggregator::store_call_and_return_value(fullpath, calls, returned_value.clone()) }) } diff --git a/mithril-client/tests/extensions/routes/snapshot.rs b/mithril-client/tests/extensions/routes/snapshot.rs index b1fce732805..a2d1db7b1cd 100644 --- a/mithril-client/tests/extensions/routes/snapshot.rs +++ b/mithril-client/tests/extensions/routes/snapshot.rs @@ -26,12 +26,7 @@ fn snapshots( .and(warp::path::full().map(move |p| p)) .and(with_calls_middleware(calls.clone())) .and_then(move |fullpath, calls| { - FakeAggregator::store_call_and_return_value( - vec![], - fullpath, - calls, - returned_value.clone(), - ) + FakeAggregator::store_call_and_return_value(fullpath, calls, returned_value.clone()) }) } @@ -43,10 +38,9 @@ fn snapshot_by_id( warp::path!("artifact" / "snapshot" / String) .and(warp::path::full().map(move |p| p)) .and(with_calls_middleware(calls.clone())) - .and_then(move |param, fullpath, calls| { + .and_then(move |_param, fullpath, calls| { let data = returned_value.read().unwrap(); FakeAggregator::store_call_and_return_value( - vec![param], fullpath, calls, serde_json::to_string(&data.clone()).unwrap(), diff --git a/mithril-client/tests/extensions/routes/statistics.rs b/mithril-client/tests/extensions/routes/statistics.rs index e8b6ce52375..739df4ef447 100644 --- a/mithril-client/tests/extensions/routes/statistics.rs +++ b/mithril-client/tests/extensions/routes/statistics.rs @@ -16,6 +16,6 @@ fn add_statistics( .and(warp::path::full().map(move |p| p)) .and(with_calls_middleware(calls.clone())) .and_then(move |fullpath, calls| { - FakeAggregator::store_call_and_return_value(vec![], fullpath, calls, "".to_string()) + FakeAggregator::store_call_and_return_value(fullpath, calls, "".to_string()) }) } diff --git a/mithril-client/tests/snapshot_list_get_show_download_verify.rs b/mithril-client/tests/snapshot_list_get_show_download_verify.rs index c0ed4111000..729c443d89b 100644 --- a/mithril-client/tests/snapshot_list_get_show_download_verify.rs +++ b/mithril-client/tests/snapshot_list_get_show_download_verify.rs @@ -101,7 +101,10 @@ async fn snapshot_list_get_show_download_verify() { .expect("add_statistics should not fail"); assert_eq!( fake_aggregator.get_last_call().await, - Some(format!("/{}", AggregatorRequest::AddStatistics.route())) + Some(format!( + "/{}", + AggregatorRequest::IncrementSnapshotStatistic.route() + )) ); let message = MessageBuilder::new() From 32891c9dce287d9bfafbb422d5fd7e4b6c11086b Mon Sep 17 00:00:00 2001 From: Damien LACHAUME / PALO-IT Date: Wed, 20 Dec 2023 18:38:03 +0100 Subject: [PATCH 11/13] PR review: move http request body into `AggregatorRequest` --- mithril-client/src/aggregator_client.rs | 26 +++++++++++++++---- mithril-client/src/snapshot_client.rs | 7 +++-- .../snapshot_list_get_show_download_verify.rs | 5 +++- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/mithril-client/src/aggregator_client.rs b/mithril-client/src/aggregator_client.rs index 1b296af20f9..ecaa22173e7 100644 --- a/mithril-client/src/aggregator_client.rs +++ b/mithril-client/src/aggregator_client.rs @@ -70,7 +70,10 @@ pub enum AggregatorRequest { ListSnapshots, /// Increments the aggregator snapshot download statistics - IncrementSnapshotStatistic, + IncrementSnapshotStatistic { + /// Snapshot as HTTP request body + snapshot: String, + }, } impl AggregatorRequest { @@ -91,7 +94,19 @@ impl AggregatorRequest { format!("artifact/snapshot/{}", digest) } AggregatorRequest::ListSnapshots => "artifact/snapshots".to_string(), - AggregatorRequest::IncrementSnapshotStatistic => "statistics/snapshot".to_string(), + AggregatorRequest::IncrementSnapshotStatistic { snapshot: _ } => { + "statistics/snapshot".to_string() + } + } + } + + /// Get the request body to send to the aggregator + pub fn get_body(&self) -> Option { + match self { + AggregatorRequest::IncrementSnapshotStatistic { snapshot } => { + Some(snapshot.to_string()) + } + _ => None, } } } @@ -110,7 +125,6 @@ pub trait AggregatorClient: Sync + Send { async fn post_content( &self, request: AggregatorRequest, - content: &str, ) -> Result; } @@ -312,10 +326,12 @@ impl AggregatorClient for AggregatorHTTPClient { async fn post_content( &self, request: AggregatorRequest, - content: &str, ) -> Result { let response = self - .post(self.get_url_for_route(&request.route())?, content) + .post( + self.get_url_for_route(&request.route())?, + &request.get_body().unwrap_or_default(), + ) .await?; response.text().await.map_err(|e| { diff --git a/mithril-client/src/snapshot_client.rs b/mithril-client/src/snapshot_client.rs index 2a1ca820672..3bdf11e5711 100644 --- a/mithril-client/src/snapshot_client.rs +++ b/mithril-client/src/snapshot_client.rs @@ -243,10 +243,9 @@ impl SnapshotClient { pub async fn add_statistics(&self, snapshot: &Snapshot) -> MithrilResult<()> { let _response = self .aggregator_client - .post_content( - AggregatorRequest::IncrementSnapshotStatistic, - &serde_json::to_string(snapshot)?, - ) + .post_content(AggregatorRequest::IncrementSnapshotStatistic { + snapshot: serde_json::to_string(snapshot)?, + }) .await?; Ok(()) diff --git a/mithril-client/tests/snapshot_list_get_show_download_verify.rs b/mithril-client/tests/snapshot_list_get_show_download_verify.rs index 729c443d89b..6adfe1477f8 100644 --- a/mithril-client/tests/snapshot_list_get_show_download_verify.rs +++ b/mithril-client/tests/snapshot_list_get_show_download_verify.rs @@ -103,7 +103,10 @@ async fn snapshot_list_get_show_download_verify() { fake_aggregator.get_last_call().await, Some(format!( "/{}", - AggregatorRequest::IncrementSnapshotStatistic.route() + AggregatorRequest::IncrementSnapshotStatistic { + snapshot: "whatever".to_string() + } + .route() )) ); From af2ed8e4caa10236b149296b60f1666b11945498 Mon Sep 17 00:00:00 2001 From: Damien LACHAUME / PALO-IT Date: Wed, 20 Dec 2023 18:49:48 +0100 Subject: [PATCH 12/13] PR review: fix in `mithril-client-cli` - use `Self` instead of `SnapshotDownloadCommand` - fix typos - remove unused dependencies --- Cargo.lock | 1 - mithril-client-cli/Cargo.toml | 1 - .../src/commands/snapshot/download.rs | 93 ++++++++----------- 3 files changed, 41 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 95f84e5ae6a..781c6b788df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3296,7 +3296,6 @@ dependencies = [ "mithril-common", "openssl", "openssl-probe", - "semver", "serde", "serde_json", "slog", diff --git a/mithril-client-cli/Cargo.toml b/mithril-client-cli/Cargo.toml index ea7adc22a4d..09c4063e3a2 100644 --- a/mithril-client-cli/Cargo.toml +++ b/mithril-client-cli/Cargo.toml @@ -36,7 +36,6 @@ mithril-client = { path = "../mithril-client", features = ["fs"] } mithril-common = { path = "../mithril-common", features = ["full"] } openssl = { version = "0.10.57", features = ["vendored"], optional = true } openssl-probe = { version = "0.1.5", optional = true } -semver = "1.0.19" serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.107" slog = { version = "2.7.0", features = [ diff --git a/mithril-client-cli/src/commands/snapshot/download.rs b/mithril-client-cli/src/commands/snapshot/download.rs index 9cf4afe2e86..4a3ee91559d 100644 --- a/mithril-client-cli/src/commands/snapshot/download.rs +++ b/mithril-client-cli/src/commands/snapshot/download.rs @@ -92,39 +92,35 @@ impl SnapshotDownloadCommand { .await? .with_context(|| format!("Can not get the snapshot for digest: '{}'", self.digest))?; - SnapshotDownloadCommand::check_local_disk_info( - 1, - &progress_printer, - &db_dir, - &snapshot_message, - )?; + Self::check_local_disk_info(1, &progress_printer, &db_dir, &snapshot_message)?; - let certificate = SnapshotDownloadCommand::fetching_certificate_and_verifying_chain( + let certificate = Self::fetch_certificate_and_verifying_chain( 2, &progress_printer, &client, - &snapshot_message, + &snapshot_message.certificate_hash, ) .await?; - SnapshotDownloadCommand::downloading_and_unpacking_snapshot( + Self::download_and_unpack_snapshot( 3, &progress_printer, &client, &snapshot_message, &db_dir, ) - .await?; + .await + .with_context(|| { + format!( + "Can not get download and unpack snapshot for digest: '{}'", + self.digest + ) + })?; - let message = SnapshotDownloadCommand::computing_snapshot_digest( - 4, - &progress_printer, - &certificate, - &db_dir, - ) - .await?; + let message = + Self::compute_snapshot_message(4, &progress_printer, &certificate, &db_dir).await?; - SnapshotDownloadCommand::verifying_snapshot_signature( + Self::verify_snapshot_signature( 5, &progress_printer, &certificate, @@ -133,7 +129,7 @@ impl SnapshotDownloadCommand { ) .await?; - SnapshotDownloadCommand::log_download_information(&db_dir, &snapshot_message, self.json)?; + Self::log_download_information(&db_dir, &snapshot_message, self.json)?; Ok(()) } @@ -142,13 +138,13 @@ impl SnapshotDownloadCommand { step_number: u16, progress_printer: &ProgressPrinter, db_dir: &PathBuf, - snapshot_message: &Snapshot, + snapshot: &Snapshot, ) -> StdResult<()> { progress_printer.report_step(step_number, "Checking local disk info…")?; if let Err(e) = SnapshotUnpacker::check_prerequisites( db_dir, - snapshot_message.size, - snapshot_message.compression_algorithm.unwrap_or_default(), + snapshot.size, + snapshot.compression_algorithm.unwrap_or_default(), ) { progress_printer .report_step(step_number, &SnapshotUtils::check_disk_space_error(e)?)?; @@ -164,11 +160,11 @@ impl SnapshotDownloadCommand { Ok(()) } - async fn fetching_certificate_and_verifying_chain( + async fn fetch_certificate_and_verifying_chain( step_number: u16, progress_printer: &ProgressPrinter, client: &Client, - snapshot_message: &Snapshot, + certificate_hash: &str, ) -> StdResult { progress_printer.report_step( step_number, @@ -176,40 +172,31 @@ impl SnapshotDownloadCommand { )?; let certificate = client .certificate() - .verify_chain(&snapshot_message.certificate_hash) + .verify_chain(certificate_hash) .await .with_context(|| { format!( "Can not verify the certificate chain from certificate_hash: '{}'", - snapshot_message.certificate_hash + certificate_hash ) })?; Ok(certificate) } - async fn downloading_and_unpacking_snapshot( + async fn download_and_unpack_snapshot( step_number: u16, progress_printer: &ProgressPrinter, client: &Client, - snapshot_message: &Snapshot, + snapshot: &Snapshot, db_dir: &Path, ) -> StdResult<()> { progress_printer.report_step(step_number, "Downloading and unpacking the snapshot…")?; - client - .snapshot() - .download_unpack(snapshot_message, db_dir) - .await - .with_context(|| { - format!( - "Snapshot Service can not download and verify the snapshot for digest: '{}'", - snapshot_message.digest - ) - })?; + client.snapshot().download_unpack(snapshot, db_dir).await?; // The snapshot download does not fail if the statistic call fails. // It would be nice to implement tests to verify the behavior of `add_statistics` - if let Err(e) = client.snapshot().add_statistics(snapshot_message).await { + if let Err(e) = client.snapshot().add_statistics(snapshot).await { warn!("Could not increment snapshot download statistics: {e:?}"); } @@ -224,13 +211,13 @@ impl SnapshotDownloadCommand { Ok(()) } - async fn computing_snapshot_digest( + async fn compute_snapshot_message( step_number: u16, progress_printer: &ProgressPrinter, certificate: &MithrilCertificate, db_dir: &Path, ) -> StdResult { - progress_printer.report_step(step_number, "Computing the snapshot digest…")?; + progress_printer.report_step(step_number, "Computing the snapshot message")?; let message = SnapshotUtils::wait_spinner( progress_printer, MessageBuilder::new().compute_snapshot_message(certificate, db_dir), @@ -246,12 +233,12 @@ impl SnapshotDownloadCommand { Ok(message) } - async fn verifying_snapshot_signature( + async fn verify_snapshot_signature( step_number: u16, progress_printer: &ProgressPrinter, certificate: &MithrilCertificate, message: &ProtocolMessage, - snapshot_message: &Snapshot, + snapshot: &Snapshot, ) -> StdResult<()> { progress_printer.report_step(step_number, "Verifying the snapshot signature…")?; if !certificate.match_message(message) { @@ -259,7 +246,7 @@ impl SnapshotDownloadCommand { return Err(anyhow!( "Certificate verification failed (snapshot digest = '{}').", - snapshot_message.digest.clone() + snapshot.digest.clone() )); } @@ -268,7 +255,7 @@ impl SnapshotDownloadCommand { fn log_download_information( db_dir: &Path, - snapshot_message: &Snapshot, + snapshot: &Snapshot, json_output: bool, ) -> StdResult<()> { let canonicalized_filepath = &db_dir.canonicalize().with_context(|| { @@ -285,6 +272,10 @@ impl SnapshotDownloadCommand { canonicalized_filepath.display() ); } else { + let cardano_node_version = snapshot + .cardano_node_version + .clone() + .unwrap_or("latest".to_string()); println!( r###"Snapshot '{}' has been unpacked and successfully checked against Mithril multi-signature contained in the certificate. @@ -292,17 +283,15 @@ impl SnapshotDownloadCommand { If you are using Cardano Docker image, you can restore a Cardano Node with: - docker run -v cardano-node-ipc:/ipc -v cardano-node-data:/data --mount type=bind,source="{}",target=/data/db/ -e NETWORK={} inputoutput/cardano-node:8.1.2 + docker run -v cardano-node-ipc:/ipc -v cardano-node-data:/data --mount type=bind,source="{}",target=/data/db/ -e NETWORK={} inputoutput/cardano-node:{} "###, - snapshot_message.digest, + snapshot.digest, db_dir.display(), - snapshot_message - .cardano_node_version - .clone() - .unwrap_or("latest".to_string()), + cardano_node_version, canonicalized_filepath.display(), - snapshot_message.beacon.network, + snapshot.beacon.network, + cardano_node_version ); } From 0b9ccf2aca7a1fa221b574193c0ff4b9e6d58d20 Mon Sep 17 00:00:00 2001 From: Damien LACHAUME / PALO-IT Date: Thu, 21 Dec 2023 11:31:48 +0100 Subject: [PATCH 13/13] Update crates versions --- Cargo.lock | 6 +++--- examples/client-snapshot/Cargo.toml | 2 +- mithril-client-cli/Cargo.toml | 2 +- mithril-client/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 781c6b788df..d7a011aff18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -883,7 +883,7 @@ dependencies = [ [[package]] name = "client-snapshot" -version = "0.1.2" +version = "0.1.3" dependencies = [ "anyhow", "async-trait", @@ -3247,7 +3247,7 @@ dependencies = [ [[package]] name = "mithril-client" -version = "0.5.13" +version = "0.5.14" dependencies = [ "anyhow", "async-recursion", @@ -3280,7 +3280,7 @@ dependencies = [ [[package]] name = "mithril-client-cli" -version = "0.5.12" +version = "0.5.13" dependencies = [ "anyhow", "async-trait", diff --git a/examples/client-snapshot/Cargo.toml b/examples/client-snapshot/Cargo.toml index decf3363acb..924825193ef 100644 --- a/examples/client-snapshot/Cargo.toml +++ b/examples/client-snapshot/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "client-snapshot" description = "Mithril client snapshot example" -version = "0.1.2" +version = "0.1.3" authors = ["dev@iohk.io", "mithril-dev@iohk.io"] documentation = "https://mithril.network/doc" edition = "2021" diff --git a/mithril-client-cli/Cargo.toml b/mithril-client-cli/Cargo.toml index 09c4063e3a2..182c894f0cf 100644 --- a/mithril-client-cli/Cargo.toml +++ b/mithril-client-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-client-cli" -version = "0.5.12" +version = "0.5.13" description = "A Mithril Client" authors = { workspace = true } edition = { workspace = true } diff --git a/mithril-client/Cargo.toml b/mithril-client/Cargo.toml index 27f4111fa16..d5f1e7bf5f9 100644 --- a/mithril-client/Cargo.toml +++ b/mithril-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-client" -version = "0.5.13" +version = "0.5.14" description = "Mithril client library" authors = { workspace = true } edition = { workspace = true }