diff --git a/Cargo.lock b/Cargo.lock index 075a5abb5a9..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,10 +3280,9 @@ dependencies = [ [[package]] name = "mithril-client-cli" -version = "0.5.12" +version = "0.5.13" dependencies = [ "anyhow", - "async-recursion", "async-trait", "chrono", "clap", @@ -3297,8 +3296,6 @@ dependencies = [ "mithril-common", "openssl", "openssl-probe", - "reqwest", - "semver", "serde", "serde_json", "slog", @@ -3308,7 +3305,6 @@ dependencies = [ "slog-term", "thiserror", "tokio", - "warp", ] [[package]] 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..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 and download tarball. +- [**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. @@ -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 increment 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 increment snapshot download statistics: {:?}", e); + } let message = MessageBuilder::new() .compute_snapshot_message(&certificate, target_directory) 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/examples/client-snapshot/README.md b/examples/client-snapshot/README.md index dc585e7ebe4..17d72616c28 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 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 64a60a4aa92..49ff194acba 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 increment snapshot download statistics: {:?}", e); + } + println!("Computing snapshot '{}' message ...", snapshot.digest); let message = wait_spinner( &progress_bar, diff --git a/mithril-client-cli/Cargo.toml b/mithril-client-cli/Cargo.toml index 9af9c9368ab..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 } @@ -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,8 +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" slog = { version = "2.7.0", features = [ @@ -54,7 +51,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..4a3ee91559d 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,58 +92,112 @@ 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( + Self::check_local_disk_info(1, &progress_printer, &db_dir, &snapshot_message)?; + + let certificate = Self::fetch_certificate_and_verifying_chain( + 2, + &progress_printer, + &client, + &snapshot_message.certificate_hash, + ) + .await?; + + Self::download_and_unpack_snapshot( + 3, + &progress_printer, + &client, + &snapshot_message, &db_dir, - snapshot_message.size, - snapshot_message.compression_algorithm.unwrap_or_default(), + ) + .await + .with_context(|| { + format!( + "Can not get download and unpack snapshot for digest: '{}'", + self.digest + ) + })?; + + let message = + Self::compute_snapshot_message(4, &progress_printer, &certificate, &db_dir).await?; + + Self::verify_snapshot_signature( + 5, + &progress_printer, + &certificate, + &message, + &snapshot_message, + ) + .await?; + + Self::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: &Snapshot, + ) -> StdResult<()> { + progress_printer.report_step(step_number, "Checking local disk info…")?; + if let Err(e) = SnapshotUnpacker::check_prerequisites( + db_dir, + snapshot.size, + snapshot.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 fetch_certificate_and_verifying_chain( + step_number: u16, + progress_printer: &ProgressPrinter, + client: &Client, + certificate_hash: &str, + ) -> StdResult { progress_printer.report_step( - 2, + step_number, "Fetching the certificate and verifying the certificate chain…", )?; 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 ) })?; - progress_printer.report_step(3, "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: '{}'", - self.digest - ) - })?; + Ok(certificate) + } + + async fn download_and_unpack_snapshot( + step_number: u16, + progress_printer: &ProgressPrinter, + client: &Client, + snapshot: &Snapshot, + db_dir: &Path, + ) -> StdResult<()> { + progress_printer.report_step(step_number, "Downloading and unpacking the snapshot…")?; + 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) = SnapshotUtils::add_statistics( - ¶ms.require("aggregator_endpoint")?, - &snapshot_message, - ) - .await - { - warn!("Could not POST snapshot download statistics: {e:?}"); + if let Err(e) = client.snapshot().add_statistics(snapshot).await { + warn!("Could not increment snapshot download statistics: {e:?}"); } // Append 'clean' file to speedup node bootstrap @@ -152,29 +208,56 @@ impl SnapshotDownloadCommand { ); }; - progress_printer.report_step(4, "Computing the snapshot digest…")?; + Ok(()) + } + + 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 message")?; 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 verify_snapshot_signature( + step_number: u16, + progress_printer: &ProgressPrinter, + certificate: &MithrilCertificate, + message: &ProtocolMessage, + snapshot: &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!( "Certificate verification failed (snapshot digest = '{}').", - snapshot_message.digest.clone() + snapshot.digest.clone() )); } + Ok(()) + } + + fn log_download_information( + db_dir: &Path, + snapshot: &Snapshot, + json_output: bool, + ) -> StdResult<()> { let canonicalized_filepath = &db_dir.canonicalize().with_context(|| { format!( "Could not get canonicalized filepath of '{}'", @@ -182,30 +265,33 @@ impl SnapshotDownloadCommand { ) })?; - if self.json { + if json_output { println!( r#"{{"timestamp": "{}", "db_directory": "{}"}}"#, Utc::now().to_rfc3339(), 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. - -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:{} + + "###, + snapshot.digest, db_dir.display(), - snapshot_message - .cardano_node_version - .unwrap_or("latest".to_string()), + cardano_node_version, canonicalized_filepath.display(), - snapshot_message.beacon.network, + snapshot.beacon.network, + cardano_node_version ); } 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/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..e3830564c28 100644 --- a/mithril-client-cli/src/lib.rs +++ b/mithril-client-cli/src/lib.rs @@ -1,17 +1,2 @@ pub mod configuration; -pub mod http_client; 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, 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() { 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 } diff --git a/mithril-client/README.md b/mithril-client/README.md index a144d5228f2..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 and download tarball. + * Snapshot: list, get, download tarball and record 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 increment snapshot download statistics: {:?}", e); + } let message = MessageBuilder::new() .compute_snapshot_message(&certificate, target_directory) diff --git a/mithril-client/src/aggregator_client.rs b/mithril-client/src/aggregator_client.rs index 5ae014c21d0..ecaa22173e7 100644 --- a/mithril-client/src/aggregator_client.rs +++ b/mithril-client/src/aggregator_client.rs @@ -68,6 +68,12 @@ pub enum AggregatorRequest { }, /// Lists the aggregator [snapshots][crate::Snapshot] ListSnapshots, + + /// Increments the aggregator snapshot download statistics + IncrementSnapshotStatistic { + /// Snapshot as HTTP request body + snapshot: String, + }, } impl AggregatorRequest { @@ -88,6 +94,19 @@ impl AggregatorRequest { format!("artifact/snapshot/{}", digest) } AggregatorRequest::ListSnapshots => "artifact/snapshots".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, } } } @@ -101,6 +120,12 @@ pub trait AggregatorClient: Sync + Send { &self, request: AggregatorRequest, ) -> Result; + + /// Post information to the Aggregator + async fn post_content( + &self, + request: AggregatorRequest, + ) -> Result; } /// Responsible of HTTP transport and API version check. @@ -208,6 +233,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 +322,24 @@ impl AggregatorClient for AggregatorHTTPClient { ))) }) } + + async fn post_content( + &self, + request: AggregatorRequest, + ) -> Result { + let response = self + .post( + self.get_url_for_route(&request.route())?, + &request.get_body().unwrap_or_default(), + ) + .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/lib.rs b/mithril-client/src/lib.rs index 2579e15db51..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 and download tarball. +//! - [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. //! @@ -46,6 +46,11 @@ //! .download_unpack(&snapshot, &target_directory) //! .await?; //! +//! if let Err(e) = client.snapshot().add_statistics(&snapshot).await { +//! println!("Could not increment 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 93fd57315c6..3bdf11e5711 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 the aggregator snapshot 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")] @@ -154,65 +180,75 @@ 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 the aggregator snapshot download statistics + pub async fn add_statistics(&self, snapshot: &Snapshot) -> MithrilResult<()> { + let _response = self + .aggregator_client + .post_content(AggregatorRequest::IncrementSnapshotStatistic { + snapshot: serde_json::to_string(snapshot)?, + }) + .await?; + + Ok(()) } } 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 b9d59df426e..a5766bb4b53 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, @@ -5,10 +7,13 @@ 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; +pub type FakeAggregatorCalls = Arc>>; pub struct FakeCertificateVerifier; @@ -21,10 +26,40 @@ 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() + } + + pub async fn store_call_and_return_value( + 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 { @@ -57,17 +92,20 @@ impl FakeAggregator { .unwrap(); test_http_server( - warp::path!("artifact" / "mithril-stake-distributions") - .map(move || mithril_stake_distribution_list_json.clone()) - .or( - warp::path!("artifact" / "mithril-stake-distribution" / String) - .map(move |_hash| mithril_stake_distribution_json.clone()), - ) - .or(warp::path!("certificate" / String).map(move |_hash| 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, + )), ) } - 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() @@ -84,11 +122,11 @@ impl FakeAggregator { ) .unwrap(); - test_http_server( - warp::path!("certificates") - .map(move || 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, + )) } } @@ -108,6 +146,7 @@ mod file { impl FakeAggregator { #[cfg(feature = "fs")] pub async fn spawn_with_snapshot( + &self, snapshot_digest: &str, certificate_hash: &str, immutable_db: &DummyImmutableDb, @@ -155,31 +194,21 @@ mod file { .compute_hash(); 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| { - let data = snapshot_clone.read().unwrap(); - serde_json::to_string(&data.clone()).unwrap() - }), - ) - .or(warp::path!("certificate" / String).map(move |_hash| certificate_json.clone())); + 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::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 - })); + 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); 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..0eba6216eb6 --- /dev/null +++ b/mithril-client/tests/extensions/routes/certificate.rs @@ -0,0 +1,44 @@ +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(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(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..3392d5bc95a --- /dev/null +++ b/mithril-client/tests/extensions/routes/mithril_stake_distribution.rs @@ -0,0 +1,39 @@ +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(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(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..a2d1db7b1cd --- /dev/null +++ b/mithril-client/tests/extensions/routes/snapshot.rs @@ -0,0 +1,81 @@ +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(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( + 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..739df4ef447 --- /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(fullpath, calls, "".to_string()) + }) +} 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 3b7b6c5d54f..6adfe1477f8 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,12 +67,48 @@ 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"); + assert_eq!( + fake_aggregator.get_last_call().await, + Some(format!( + "/{}", + AggregatorRequest::IncrementSnapshotStatistic { + snapshot: "whatever".to_string() + } + .route() + )) + ); let message = MessageBuilder::new() .compute_snapshot_message(&certificate, &unpacked_dir)