diff --git a/Cargo.lock b/Cargo.lock index 6abe1e5a704..37547f55423 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4405,6 +4405,30 @@ dependencies = [ "tokio", ] +[[package]] +name = "mithril-protocol-config" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "http 1.3.1", + "httpmock", + "mithril-cardano-node-chain", + "mithril-cardano-node-internal-database", + "mithril-common", + "mithril-ticker", + "mockall", + "reqwest", + "semver", + "serde", + "serde_json", + "slog", + "slog-async", + "slog-term", + "thiserror 2.0.16", + "tokio", +] + [[package]] name = "mithril-relay" version = "0.1.52" @@ -4489,6 +4513,7 @@ dependencies = [ "mithril-era", "mithril-metric", "mithril-persistence", + "mithril-protocol-config", "mithril-signed-entity-lock", "mithril-signed-entity-preloader", "mithril-ticker", diff --git a/Cargo.toml b/Cargo.toml index fabdd2909de..4f13ee52158 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "internal/mithril-era", "internal/mithril-metric", "internal/mithril-persistence", + "internal/mithril-protocol-config", "internal/mithril-resource-pool", "internal/mithril-ticker", "internal/signed-entity/mithril-signed-entity-lock", diff --git a/Makefile b/Makefile index 3057609c4fc..a379c8e96a7 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ COMPONENTS = mithril-aggregator mithril-client mithril-client-cli mithril-client internal/mithril-build-script internal/mithril-cli-helper internal/mithril-doc \ internal/mithril-dmq \ internal/mithril-doc-derive internal/mithril-era internal/mithril-metric internal/mithril-persistence \ + internal/mithril-protocol-config \ internal/mithril-resource-pool internal/mithril-ticker \ internal/cardano-node/mithril-cardano-node-chain internal/cardano-node/mithril-cardano-node-internal-database \ internal/signed-entity/mithril-signed-entity-lock internal/signed-entity/mithril-signed-entity-preloader \ diff --git a/README.md b/README.md index 920bd1a3ec2..283cc97477f 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,8 @@ This repository consists of the following parts: - [**Mithril persistence**](./internal/mithril-persistence): the **persistence** library that is used by **Mithril network** nodes. + - [**Mithril protocol config**](./internal/mithril-protocol-config): mechanisms to read and check the **configuration parameters** of a **Mithril network**. + - [**Mithril resource pool**](./internal/mithril-resource-pool): a **resource pool** mechanism that is used by **Mithril network** nodes. - [**Mithril ticker**](./internal/mithril-ticker): a **ticker** mechanism that reads time information from the chain and is used by **Mithril network** nodes. diff --git a/internal/mithril-protocol-config/Cargo.toml b/internal/mithril-protocol-config/Cargo.toml new file mode 100644 index 00000000000..ba3a54d281f --- /dev/null +++ b/internal/mithril-protocol-config/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "mithril-protocol-config" +version = "0.1.0" +description = "Configuraton parameters of the mithril network" +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +repository = { workspace = true } + +[dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } +mithril-cardano-node-chain = { path = "../cardano-node/mithril-cardano-node-chain" } +mithril-cardano-node-internal-database = { path = "../cardano-node/mithril-cardano-node-internal-database" } +mithril-common = { path = "../../mithril-common" } +mithril-ticker = { path = "../mithril-ticker" } +reqwest = { workspace = true, features = [ + "default", + "stream", + "gzip", + "zstd", + "deflate", + "brotli" +] } +semver = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +slog = { workspace = true, features = [ + "max_level_trace", + "release_max_level_debug", +] } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] } + +[dev-dependencies] +#criterion = { version = "0.7.0", features = ["html_reports", "async_tokio"] } +http = "1.3.1" +httpmock = "0.7.0" +mockall = { workspace = true } +slog-async = { workspace = true } +slog-term = { workspace = true } diff --git a/internal/mithril-protocol-config/Makefile b/internal/mithril-protocol-config/Makefile new file mode 100644 index 00000000000..d66d6d9fefc --- /dev/null +++ b/internal/mithril-protocol-config/Makefile @@ -0,0 +1,19 @@ +.PHONY: all build test check doc + +CARGO = cargo + +all: test build + +build: + ${CARGO} build --release + +test: + ${CARGO} test + +check: + ${CARGO} check --release --all-features --all-targets + ${CARGO} clippy --release --all-features --all-targets + ${CARGO} fmt --check + +doc: + ${CARGO} doc --no-deps --open diff --git a/internal/mithril-protocol-config/README.md b/internal/mithril-protocol-config/README.md new file mode 100644 index 00000000000..b7ad44b1301 --- /dev/null +++ b/internal/mithril-protocol-config/README.md @@ -0,0 +1,5 @@ +# Mithril-protocol-config + +**This is a work in progress** 🛠 + +This crate provides mechanisms to read and check the configuration parameters of a Mithril network. diff --git a/internal/mithril-protocol-config/src/http_client/aggregator_client.rs b/internal/mithril-protocol-config/src/http_client/aggregator_client.rs new file mode 100644 index 00000000000..31f30f8f605 --- /dev/null +++ b/internal/mithril-protocol-config/src/http_client/aggregator_client.rs @@ -0,0 +1,777 @@ +use anyhow::anyhow; +use async_trait::async_trait; +use reqwest::header::{self, HeaderValue}; +use reqwest::{self, Client, Proxy, RequestBuilder, Response, StatusCode}; +use semver::Version; +use slog::{Logger, debug, error, warn}; +use std::{io, sync::Arc, time::Duration}; +use thiserror::Error; + +use mithril_common::{ + MITHRIL_API_VERSION_HEADER, MITHRIL_SIGNER_VERSION_HEADER, StdError, + api_version::APIVersionProvider, + entities::{ClientError, ServerError}, + logging::LoggerExtensions, + messages::{AggregatorFeaturesMessage, EpochSettingsMessage}, +}; + +const JSON_CONTENT_TYPE: HeaderValue = HeaderValue::from_static("application/json"); + +const API_VERSION_MISMATCH_WARNING_MESSAGE: &str = + "OpenAPI version may be incompatible, please update your Mithril node to the latest version."; + +/// Error structure for the Aggregator Client. +#[derive(Error, Debug)] +pub enum AggregatorClientError { + /// The aggregator host has returned a technical error. + #[error("remote server technical error")] + RemoteServerTechnical(#[source] StdError), + + /// The aggregator host responded it cannot fulfill our request. + #[error("remote server logical error")] + RemoteServerLogical(#[source] StdError), + + /// Could not reach aggregator. + #[error("remote server unreachable")] + RemoteServerUnreachable(#[source] StdError), + + /// Unhandled status code + #[error("unhandled status code: {0}, response text: {1}")] + UnhandledStatusCode(StatusCode, String), + + /// Could not parse response. + #[error("json parsing failed")] + JsonParseFailed(#[source] StdError), + + /// Mostly network errors. + #[error("Input/Output error")] + IOError(#[from] io::Error), + + /// HTTP client creation error + #[error("HTTP client creation failed")] + HTTPClientCreation(#[source] StdError), + + /// Proxy creation error + #[error("proxy creation failed")] + ProxyCreation(#[source] StdError), + + /// No signer registration round opened yet + #[error("a signer registration round is not opened yet, please try again later")] + RegistrationRoundNotYetOpened(#[source] StdError), +} + +impl AggregatorClientError { + /// Create an `AggregatorClientError` from a response. + /// + /// This method is meant to be used after handling domain-specific cases leaving only + /// 4xx or 5xx status codes. + /// Otherwise, it will return an `UnhandledStatusCode` error. + pub async fn from_response(response: Response) -> Self { + let error_code = response.status(); + + if error_code.is_client_error() { + let root_cause = Self::get_root_cause(response).await; + Self::RemoteServerLogical(anyhow!(root_cause)) + } else if error_code.is_server_error() { + let root_cause = Self::get_root_cause(response).await; + match error_code.as_u16() { + 550 => Self::RegistrationRoundNotYetOpened(anyhow!(root_cause)), + _ => Self::RemoteServerTechnical(anyhow!(root_cause)), + } + } else { + let response_text = response.text().await.unwrap_or_default(); + Self::UnhandledStatusCode(error_code, response_text) + } + } + + async fn get_root_cause(response: Response) -> String { + let error_code = response.status(); + let canonical_reason = error_code.canonical_reason().unwrap_or_default().to_lowercase(); + let is_json = response + .headers() + .get(header::CONTENT_TYPE) + .is_some_and(|ct| JSON_CONTENT_TYPE == ct); + + if is_json { + let json_value: serde_json::Value = response.json().await.unwrap_or_default(); + + if let Ok(client_error) = serde_json::from_value::(json_value.clone()) { + format!( + "{}: {}: {}", + canonical_reason, client_error.label, client_error.message + ) + } else if let Ok(server_error) = + serde_json::from_value::(json_value.clone()) + { + format!("{}: {}", canonical_reason, server_error.message) + } else if json_value.is_null() { + canonical_reason.to_string() + } else { + format!("{canonical_reason}: {json_value}") + } + } else { + let response_text = response.text().await.unwrap_or_default(); + format!("{canonical_reason}: {response_text}") + } + } +} + +/// Trait for mocking and testing a `AggregatorClient` +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait AggregatorClient: Sync + Send { + /// Retrieves epoch settings from the aggregator + async fn retrieve_epoch_settings( + &self, + ) -> Result, AggregatorClientError>; + + /// Retrieves aggregator features message from the aggregator + async fn retrieve_aggregator_features( + &self, + ) -> Result; +} + +/// AggregatorHTTPClient is a http client for an aggregator +pub struct AggregatorHTTPClient { + aggregator_endpoint: String, + relay_endpoint: Option, + api_version_provider: Arc, + timeout_duration: Option, + logger: Logger, +} + +impl AggregatorHTTPClient { + /// AggregatorHTTPClient factory + pub fn new( + aggregator_endpoint: String, + relay_endpoint: Option, + api_version_provider: Arc, + timeout_duration: Option, + logger: Logger, + ) -> Self { + let logger = logger.new_with_component_name::(); + debug!(logger, "New AggregatorHTTPClient created"); + Self { + aggregator_endpoint, + relay_endpoint, + api_version_provider, + timeout_duration, + logger, + } + } + + fn prepare_http_client(&self) -> Result { + let client = match &self.relay_endpoint { + Some(relay_endpoint) => Client::builder() + .proxy( + Proxy::all(relay_endpoint) + .map_err(|e| AggregatorClientError::ProxyCreation(anyhow!(e)))?, + ) + .build() + .map_err(|e| AggregatorClientError::HTTPClientCreation(anyhow!(e)))?, + None => Client::new(), + }; + + Ok(client) + } + + /// Forge a client request adding protocol version in the headers. + pub fn prepare_request_builder(&self, request_builder: RequestBuilder) -> RequestBuilder { + let request_builder = request_builder + .header( + MITHRIL_API_VERSION_HEADER, + self.api_version_provider + .compute_current_version() + .unwrap() + .to_string(), + ) + .header(MITHRIL_SIGNER_VERSION_HEADER, env!("CARGO_PKG_VERSION")); + + if let Some(duration) = self.timeout_duration { + request_builder.timeout(duration) + } else { + request_builder + } + } + + /// Check API version mismatch and log a warning if the aggregator's version is more recent. + fn warn_if_api_version_mismatch(&self, response: &Response) { + let aggregator_version = response + .headers() + .get(MITHRIL_API_VERSION_HEADER) + .and_then(|v| v.to_str().ok()) + .and_then(|s| Version::parse(s).ok()); + + let signer_version = self.api_version_provider.compute_current_version(); + + match (aggregator_version, signer_version) { + (Some(aggregator), Ok(signer)) if signer < aggregator => { + warn!(self.logger, "{}", API_VERSION_MISMATCH_WARNING_MESSAGE; + "aggregator_version" => %aggregator, + "signer_version" => %signer, + ); + } + (Some(_), Err(error)) => { + error!( + self.logger, + "Failed to compute the current signer API version"; + "error" => error.to_string() + ); + } + _ => {} + } + } +} + +#[async_trait] +impl AggregatorClient for AggregatorHTTPClient { + async fn retrieve_epoch_settings( + &self, + ) -> Result, AggregatorClientError> { + debug!(self.logger, "Retrieve epoch settings"); + let url = format!("{}/epoch-settings", self.aggregator_endpoint); + let response = self + .prepare_request_builder(self.prepare_http_client()?.get(url.clone())) + .send() + .await; + + match response { + Ok(response) => match response.status() { + StatusCode::OK => { + self.warn_if_api_version_mismatch(&response); + match response.json::().await { + Ok(message) => Ok(Some(message)), + Err(err) => Err(AggregatorClientError::JsonParseFailed(anyhow!(err))), + } + } + _ => Err(AggregatorClientError::from_response(response).await), + }, + Err(err) => Err(AggregatorClientError::RemoteServerUnreachable(anyhow!(err))), + } + } + + async fn retrieve_aggregator_features( + &self, + ) -> Result { + debug!(self.logger, "Retrieve aggregator features message"); + let url = format!("{}/", self.aggregator_endpoint); + let response = self + .prepare_request_builder(self.prepare_http_client()?.get(url.clone())) + .send() + .await; + + match response { + Ok(response) => match response.status() { + StatusCode::OK => { + self.warn_if_api_version_mismatch(&response); + + Ok(response + .json::() + .await + .map_err(|e| AggregatorClientError::JsonParseFailed(anyhow!(e)))?) + } + _ => Err(AggregatorClientError::from_response(response).await), + }, + Err(err) => Err(AggregatorClientError::RemoteServerUnreachable(anyhow!(err))), + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use http::response::Builder as HttpResponseBuilder; + use httpmock::prelude::*; + use semver::Version; + use serde_json::json; + + use mithril_common::test::{ + double::{Dummy, DummyApiVersionDiscriminantSource}, + logging::MemoryDrainForTestInspector, + }; + + use crate::test_tools::TestLogger; + + use super::*; + + macro_rules! assert_is_error { + ($error:expr, $error_type:pat) => { + assert!( + matches!($error, $error_type), + "Expected {} error, got '{:?}'.", + stringify!($error_type), + $error + ); + }; + } + + fn setup_client>(server_url: U) -> AggregatorHTTPClient { + let discriminant_source = DummyApiVersionDiscriminantSource::new("dummy"); + let api_version_provider = APIVersionProvider::new(Arc::new(discriminant_source)); + + AggregatorHTTPClient::new( + server_url.into(), + None, + Arc::new(api_version_provider), + None, + TestLogger::stdout(), + ) + } + + fn setup_server_and_client() -> (MockServer, AggregatorHTTPClient) { + let server = MockServer::start(); + let aggregator_endpoint = server.url(""); + let client = setup_client(&aggregator_endpoint); + + (server, client) + } + + fn set_returning_500(server: &MockServer) { + server.mock(|_, then| { + then.status(500).body("an error occurred"); + }); + } + + fn set_unparsable_json(server: &MockServer) { + server.mock(|_, then| { + then.status(200).body("this is not a json"); + }); + } + + fn build_text_response>(status_code: StatusCode, body: T) -> Response { + HttpResponseBuilder::new() + .status(status_code) + .body(body.into()) + .unwrap() + .into() + } + + fn build_json_response(status_code: StatusCode, body: &T) -> Response { + HttpResponseBuilder::new() + .status(status_code) + .header(header::CONTENT_TYPE, JSON_CONTENT_TYPE) + .body(serde_json::to_string(&body).unwrap()) + .unwrap() + .into() + } + + macro_rules! assert_error_text_contains { + ($error: expr, $expect_contains: expr) => { + let error = &$error; + assert!( + error.contains($expect_contains), + "Expected error message to contain '{}'\ngot '{error:?}'", + $expect_contains, + ); + }; + } + + mod epoch_settings { + use super::*; + + #[tokio::test] + async fn test_epoch_settings_ok_200() { + let (server, client) = setup_server_and_client(); + let epoch_settings_expected = EpochSettingsMessage::dummy(); + let _server_mock = server.mock(|when, then| { + when.path("/epoch-settings"); + then.status(200).body(json!(epoch_settings_expected).to_string()); + }); + + let epoch_settings = client.retrieve_epoch_settings().await; + epoch_settings.as_ref().expect("unexpected error"); + assert_eq!(epoch_settings_expected, epoch_settings.unwrap().unwrap()); + } + + #[tokio::test] + async fn test_epoch_settings_ko_500() { + let (server, client) = setup_server_and_client(); + let _server_mock = server.mock(|when, then| { + when.path("/epoch-settings"); + then.status(500).body("an error occurred"); + }); + + match client.retrieve_epoch_settings().await.unwrap_err() { + AggregatorClientError::RemoteServerTechnical(_) => (), + e => panic!("Expected Aggregator::RemoteServerTechnical error, got '{e:?}'."), + }; + } + + #[tokio::test] + async fn test_epoch_settings_timeout() { + let (server, mut client) = setup_server_and_client(); + client.timeout_duration = Some(Duration::from_millis(10)); + let _server_mock = server.mock(|when, then| { + when.path("/epoch-settings"); + then.delay(Duration::from_millis(100)); + }); + + let error = client + .retrieve_epoch_settings() + .await + .expect_err("retrieve_epoch_settings should fail"); + + assert!( + matches!(error, AggregatorClientError::RemoteServerUnreachable(_)), + "unexpected error type: {error:?}" + ); + } + } + + mod aggregator_features { + use super::*; + + #[tokio::test] + async fn test_aggregator_features_ok_200() { + let (server, client) = setup_server_and_client(); + let message_expected = AggregatorFeaturesMessage::dummy(); + let _server_mock = server.mock(|when, then| { + when.path("/"); + then.status(200).body(json!(message_expected).to_string()); + }); + + let message = client.retrieve_aggregator_features().await.unwrap(); + + assert_eq!(message_expected, message); + } + + #[tokio::test] + async fn test_aggregator_features_ko_500() { + let (server, client) = setup_server_and_client(); + set_returning_500(&server); + + let error = client.retrieve_aggregator_features().await.unwrap_err(); + + assert_is_error!(error, AggregatorClientError::RemoteServerTechnical(_)); + } + + #[tokio::test] + async fn test_aggregator_features_ko_json_serialization() { + let (server, client) = setup_server_and_client(); + set_unparsable_json(&server); + + let error = client.retrieve_aggregator_features().await.unwrap_err(); + + assert_is_error!(error, AggregatorClientError::JsonParseFailed(_)); + } + + #[tokio::test] + async fn test_aggregator_features_timeout() { + let (server, mut client) = setup_server_and_client(); + client.timeout_duration = Some(Duration::from_millis(10)); + let _server_mock = server.mock(|when, then| { + when.path("/"); + then.delay(Duration::from_millis(100)); + }); + + let error = client.retrieve_aggregator_features().await.unwrap_err(); + + assert_is_error!(error, AggregatorClientError::RemoteServerUnreachable(_)); + } + } + + #[tokio::test] + async fn test_4xx_errors_are_handled_as_remote_server_logical() { + let response = build_text_response(StatusCode::BAD_REQUEST, "error text"); + let handled_error = AggregatorClientError::from_response(response).await; + + assert!( + matches!( + handled_error, + AggregatorClientError::RemoteServerLogical(..) + ), + "Expected error to be RemoteServerLogical\ngot '{handled_error:?}'", + ); + } + + #[tokio::test] + async fn test_5xx_errors_are_handled_as_remote_server_technical() { + let response = build_text_response(StatusCode::INTERNAL_SERVER_ERROR, "error text"); + let handled_error = AggregatorClientError::from_response(response).await; + + assert!( + matches!( + handled_error, + AggregatorClientError::RemoteServerTechnical(..) + ), + "Expected error to be RemoteServerLogical\ngot '{handled_error:?}'", + ); + } + + #[tokio::test] + async fn test_550_error_is_handled_as_registration_round_not_yet_opened() { + let response = build_text_response(StatusCode::from_u16(550).unwrap(), "Not yet available"); + let handled_error = AggregatorClientError::from_response(response).await; + + assert!( + matches!( + handled_error, + AggregatorClientError::RegistrationRoundNotYetOpened(..) + ), + "Expected error to be RegistrationRoundNotYetOpened\ngot '{handled_error:?}'", + ); + } + + #[tokio::test] + async fn test_non_4xx_or_5xx_errors_are_handled_as_unhandled_status_code_and_contains_response_text() + { + let response = build_text_response(StatusCode::OK, "ok text"); + let handled_error = AggregatorClientError::from_response(response).await; + + assert!( + matches!( + handled_error, + AggregatorClientError::UnhandledStatusCode(..) if format!("{handled_error:?}").contains("ok text") + ), + "Expected error to be UnhandledStatusCode with 'ok text' in error text\ngot '{handled_error:?}'", + ); + } + + #[tokio::test] + async fn test_root_cause_of_non_json_response_contains_response_plain_text() { + let error_text = "An error occurred; please try again later."; + let response = build_text_response(StatusCode::EXPECTATION_FAILED, error_text); + + assert_error_text_contains!( + AggregatorClientError::get_root_cause(response).await, + "expectation failed: An error occurred; please try again later." + ); + } + + #[tokio::test] + async fn test_root_cause_of_json_formatted_client_error_response_contains_error_label_and_message() + { + let client_error = ClientError::new("label", "message"); + let response = build_json_response(StatusCode::BAD_REQUEST, &client_error); + + assert_error_text_contains!( + AggregatorClientError::get_root_cause(response).await, + "bad request: label: message" + ); + } + + #[tokio::test] + async fn test_root_cause_of_json_formatted_server_error_response_contains_error_label_and_message() + { + let server_error = ServerError::new("message"); + let response = build_json_response(StatusCode::BAD_REQUEST, &server_error); + + assert_error_text_contains!( + AggregatorClientError::get_root_cause(response).await, + "bad request: message" + ); + } + + #[tokio::test] + async fn test_root_cause_of_unknown_formatted_json_response_contains_json_key_value_pairs() { + let response = build_json_response( + StatusCode::INTERNAL_SERVER_ERROR, + &json!({ "second": "unknown", "first": "foreign" }), + ); + + assert_error_text_contains!( + AggregatorClientError::get_root_cause(response).await, + r#"internal server error: {"first":"foreign","second":"unknown"}"# + ); + } + + #[tokio::test] + async fn test_root_cause_with_invalid_json_response_still_contains_response_status_name() { + let response = HttpResponseBuilder::new() + .status(StatusCode::BAD_REQUEST) + .header(header::CONTENT_TYPE, JSON_CONTENT_TYPE) + .body(r#"{"invalid":"unexpected dot", "key": "value".}"#) + .unwrap() + .into(); + + let root_cause = AggregatorClientError::get_root_cause(response).await; + + assert_error_text_contains!(root_cause, "bad request"); + assert!( + !root_cause.contains("bad request: "), + "Expected error message should not contain additional information \ngot '{root_cause:?}'" + ); + } + + mod warn_if_api_version_mismatch { + use mithril_common::test::api_version_extensions::ApiVersionProviderTestExtension; + + use super::*; + + fn version_provider_with_open_api_version>( + version: V, + ) -> APIVersionProvider { + let mut version_provider = version_provider_without_open_api_version(); + let mut open_api_versions = HashMap::new(); + open_api_versions.insert( + "openapi.yaml".to_string(), + Version::parse(&version.into()).unwrap(), + ); + version_provider.update_open_api_versions(open_api_versions); + + version_provider + } + + fn version_provider_without_open_api_version() -> APIVersionProvider { + let mut version_provider = + APIVersionProvider::new(Arc::new(DummyApiVersionDiscriminantSource::new("dummy"))); + version_provider.update_open_api_versions(HashMap::new()); + + version_provider + } + + fn build_fake_response_with_header, V: Into>( + key: K, + value: V, + ) -> Response { + HttpResponseBuilder::new() + .header(key.into(), value.into()) + .body("whatever") + .unwrap() + .into() + } + + fn assert_api_version_warning_logged, S: Into>( + log_inspector: &MemoryDrainForTestInspector, + aggregator_version: A, + signer_version: S, + ) { + assert!(log_inspector.contains_log(API_VERSION_MISMATCH_WARNING_MESSAGE)); + assert!( + log_inspector + .contains_log(&format!("aggregator_version={}", aggregator_version.into())) + ); + assert!( + log_inspector.contains_log(&format!("signer_version={}", signer_version.into())) + ); + } + + #[test] + fn test_logs_warning_when_aggregator_api_version_is_newer() { + let aggregator_version = "2.0.0"; + let signer_version = "1.0.0"; + let (logger, log_inspector) = TestLogger::memory(); + let version_provider = version_provider_with_open_api_version(signer_version); + let mut client = setup_client("whatever"); + client.api_version_provider = Arc::new(version_provider); + client.logger = logger; + let response = + build_fake_response_with_header(MITHRIL_API_VERSION_HEADER, aggregator_version); + + assert!( + Version::parse(aggregator_version).unwrap() + > Version::parse(signer_version).unwrap() + ); + + client.warn_if_api_version_mismatch(&response); + + assert_api_version_warning_logged(&log_inspector, aggregator_version, signer_version); + } + + #[test] + fn test_no_warning_logged_when_versions_match() { + let version = "1.0.0"; + let (logger, log_inspector) = TestLogger::memory(); + let version_provider = version_provider_with_open_api_version(version); + let mut client = setup_client("whatever"); + client.api_version_provider = Arc::new(version_provider); + client.logger = logger; + let response = build_fake_response_with_header(MITHRIL_API_VERSION_HEADER, version); + + client.warn_if_api_version_mismatch(&response); + + assert!(!log_inspector.contains_log(API_VERSION_MISMATCH_WARNING_MESSAGE)); + } + + #[test] + fn test_no_warning_logged_when_aggregator_api_version_is_older() { + let aggregator_version = "1.0.0"; + let signer_version = "2.0.0"; + let (logger, log_inspector) = TestLogger::memory(); + let version_provider = version_provider_with_open_api_version(signer_version); + let mut client = setup_client("whatever"); + client.api_version_provider = Arc::new(version_provider); + client.logger = logger; + let response = + build_fake_response_with_header(MITHRIL_API_VERSION_HEADER, aggregator_version); + + assert!( + Version::parse(aggregator_version).unwrap() + < Version::parse(signer_version).unwrap() + ); + + client.warn_if_api_version_mismatch(&response); + + assert!(!log_inspector.contains_log(API_VERSION_MISMATCH_WARNING_MESSAGE)); + } + + #[test] + fn test_does_not_log_or_fail_when_header_is_missing() { + let (logger, log_inspector) = TestLogger::memory(); + let mut client = setup_client("whatever"); + client.logger = logger; + let response = + build_fake_response_with_header("NotMithrilAPIVersionHeader", "whatever"); + + client.warn_if_api_version_mismatch(&response); + + assert!(!log_inspector.contains_log(API_VERSION_MISMATCH_WARNING_MESSAGE)); + } + + #[test] + fn test_does_not_log_or_fail_when_header_is_not_a_version() { + let (logger, log_inspector) = TestLogger::memory(); + let mut client = setup_client("whatever"); + client.logger = logger; + let response = + build_fake_response_with_header(MITHRIL_API_VERSION_HEADER, "not_a_version"); + + client.warn_if_api_version_mismatch(&response); + + assert!(!log_inspector.contains_log(API_VERSION_MISMATCH_WARNING_MESSAGE)); + } + + #[test] + fn test_logs_error_when_signer_version_cannot_be_computed() { + let (logger, log_inspector) = TestLogger::memory(); + let version_provider = version_provider_without_open_api_version(); + let mut client = setup_client("whatever"); + client.api_version_provider = Arc::new(version_provider); + client.logger = logger; + let response = build_fake_response_with_header(MITHRIL_API_VERSION_HEADER, "1.0.0"); + + client.warn_if_api_version_mismatch(&response); + + assert!(!log_inspector.contains_log(API_VERSION_MISMATCH_WARNING_MESSAGE)); + } + + #[tokio::test] + async fn test_epoch_settings_ok_200_log_warning_if_api_version_mismatch() { + let aggregator_version = "2.0.0"; + let signer_version = "1.0.0"; + let (server, mut client) = setup_server_and_client(); + let (logger, log_inspector) = TestLogger::memory(); + let version_provider = version_provider_with_open_api_version(signer_version); + client.api_version_provider = Arc::new(version_provider); + client.logger = logger; + + let epoch_settings_expected = EpochSettingsMessage::dummy(); + let _server_mock = server.mock(|when, then| { + when.path("/epoch-settings"); + then.status(200) + .header(MITHRIL_API_VERSION_HEADER, aggregator_version) + .body(json!(epoch_settings_expected).to_string()); + }); + + assert!( + Version::parse(aggregator_version).unwrap() + > Version::parse(signer_version).unwrap() + ); + + client.retrieve_epoch_settings().await.unwrap(); + + assert_api_version_warning_logged(&log_inspector, aggregator_version, signer_version); + } + } +} diff --git a/internal/mithril-protocol-config/src/http_client/http_impl.rs b/internal/mithril-protocol-config/src/http_client/http_impl.rs new file mode 100644 index 00000000000..6f1b2adcb23 --- /dev/null +++ b/internal/mithril-protocol-config/src/http_client/http_impl.rs @@ -0,0 +1,65 @@ +use anyhow::anyhow; +use std::{collections::HashMap, sync::Arc, time::Duration}; + +use crate::{ + HTTP_REQUEST_TIMEOUT_DURATION, + http_client::aggregator_client::{AggregatorClient, AggregatorHTTPClient}, + interface::MithrilNetworkConfigurationProvider, + model::{MithrilNetworkConfiguration, SignedEntityTypeConfiguration}, +}; +use async_trait::async_trait; +use mithril_common::api_version::APIVersionProvider; +use mithril_common::{StdResult, entities::SignedEntityTypeDiscriminants}; + +pub struct HttpMithrilNetworkConfigurationProvider { + aggregator_client: AggregatorHTTPClient, +} + +impl HttpMithrilNetworkConfigurationProvider { + pub fn new( + aggregator_endpoint: String, + relay_endpoint: Option, + api_version_provider: Arc, + logger: slog::Logger, + ) -> Self { + let aggregator_client = AggregatorHTTPClient::new( + aggregator_endpoint.clone(), + relay_endpoint.clone(), + api_version_provider.clone(), + Some(Duration::from_millis(HTTP_REQUEST_TIMEOUT_DURATION)), + logger.clone(), + ); + + Self { aggregator_client } + } +} + +#[async_trait] +impl MithrilNetworkConfigurationProvider for HttpMithrilNetworkConfigurationProvider { + async fn get(&self) -> StdResult { + let Some(epoch_settings) = self.aggregator_client.retrieve_epoch_settings().await? else { + return Err(anyhow!("Failed to retrieve epoch settings")); + }; + + let aggregator_features = self.aggregator_client.retrieve_aggregator_features().await?; + let available_signed_entity_types = aggregator_features.capabilities.signed_entity_types; + + let mut signed_entity_types_config = HashMap::new(); + signed_entity_types_config.insert( + SignedEntityTypeDiscriminants::CardanoTransactions, + SignedEntityTypeConfiguration::CardanoTransactions( + epoch_settings.cardano_transactions_signing_config.ok_or_else(|| { + anyhow!("Cardano transactions signing config is missing in epoch settings") + })?, + ), + ); + + Ok(MithrilNetworkConfiguration { + epoch: epoch_settings.epoch, + signer_registration_protocol_parameters: epoch_settings + .signer_registration_protocol_parameters, + available_signed_entity_types, + signed_entity_types_config, + }) + } +} diff --git a/internal/mithril-protocol-config/src/http_client/mod.rs b/internal/mithril-protocol-config/src/http_client/mod.rs new file mode 100644 index 00000000000..40fd6cc26c7 --- /dev/null +++ b/internal/mithril-protocol-config/src/http_client/mod.rs @@ -0,0 +1,2 @@ +mod aggregator_client; +pub mod http_impl; diff --git a/internal/mithril-protocol-config/src/interface.rs b/internal/mithril-protocol-config/src/interface.rs new file mode 100644 index 00000000000..e6eb8f38eb9 --- /dev/null +++ b/internal/mithril-protocol-config/src/interface.rs @@ -0,0 +1,11 @@ +use async_trait::async_trait; +use mithril_common::StdResult; + +use crate::model::MithrilNetworkConfiguration; + +/// Trait to provide the current Mithril network configuration. +#[async_trait] +pub trait MithrilNetworkConfigurationProvider: Sync + Send { + /// Get the Mithril network configuration for the current epoch. + async fn get(&self) -> StdResult; +} diff --git a/internal/mithril-protocol-config/src/lib.rs b/internal/mithril-protocol-config/src/lib.rs new file mode 100644 index 00000000000..d6f7f25acf7 --- /dev/null +++ b/internal/mithril-protocol-config/src/lib.rs @@ -0,0 +1,11 @@ +/// HTTP request timeout duration in milliseconds +const HTTP_REQUEST_TIMEOUT_DURATION: u64 = 30000; +pub mod http_client; +pub mod interface; +pub mod model; +pub mod test; + +#[cfg(test)] +pub(crate) mod test_tools { + mithril_common::define_test_logger!(); +} diff --git a/internal/mithril-protocol-config/src/model.rs b/internal/mithril-protocol-config/src/model.rs new file mode 100644 index 00000000000..93f0d6f87ef --- /dev/null +++ b/internal/mithril-protocol-config/src/model.rs @@ -0,0 +1,100 @@ +use std::collections::{BTreeSet, HashMap}; + +use mithril_common::entities::{ + CardanoTransactionsSigningConfig, Epoch, ProtocolParameters, SignedEntityTypeDiscriminants, +}; + +#[derive(PartialEq, Clone, Debug)] +pub enum SignedEntityTypeConfiguration { + /// Cardano Transactions + CardanoTransactions(CardanoTransactionsSigningConfig), +} + +/// A Mithril network configuration +#[derive(PartialEq, Clone, Debug)] +pub struct MithrilNetworkConfiguration { + /// Epoch + pub epoch: Epoch, + + /// Cryptographic protocol parameters (`k`, `m` and `phi_f`) + pub signer_registration_protocol_parameters: ProtocolParameters, + + /// List of available types of certifications (`CardanoDatabase`, `CardanoTransactions`, `CardanoStakeDistribution`, ...) + pub available_signed_entity_types: BTreeSet, + + /// Custom configurations for signed entity types (e.g. `cardano_transactions_signing_config` for `CardanoTransactions`) + pub signed_entity_types_config: + HashMap, +} + +impl MithrilNetworkConfiguration { + /// Get the Cardano Transactions signing configuration + pub fn get_cardano_transactions_signing_config( + &self, + ) -> Option { + match self + .signed_entity_types_config + .get(&SignedEntityTypeDiscriminants::CardanoTransactions) + { + Some(SignedEntityTypeConfiguration::CardanoTransactions(config)) => { + Some(config.clone()) + } + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use mithril_common::entities::{ + BlockNumber, CardanoTransactionsSigningConfig, SignedEntityTypeDiscriminants, + }; + + use crate::model::{MithrilNetworkConfiguration, SignedEntityTypeConfiguration}; + + fn default() -> MithrilNetworkConfiguration { + MithrilNetworkConfiguration { + epoch: Default::default(), + signer_registration_protocol_parameters: Default::default(), + available_signed_entity_types: Default::default(), + signed_entity_types_config: Default::default(), + } + } + + #[test] + fn test_get_cardano_transactions_signing_config_should_return_config_if_cardano_transactions_exist() + { + let config = MithrilNetworkConfiguration { + signed_entity_types_config: HashMap::from([( + SignedEntityTypeDiscriminants::CardanoTransactions, + SignedEntityTypeConfiguration::CardanoTransactions( + CardanoTransactionsSigningConfig { + security_parameter: BlockNumber(42), + step: BlockNumber(26), + }, + ), + )]), + ..default() + }; + + let result = config.get_cardano_transactions_signing_config(); + assert!(result.is_some()); + let unwrapped_result = result.unwrap(); + assert_eq!(unwrapped_result.security_parameter, BlockNumber(42)); + assert_eq!(unwrapped_result.step, BlockNumber(26)); + } + + #[test] + fn test_get_cardano_transactions_signing_config_should_return_none_if_there_is_no_cardano_transactions() + { + let config = MithrilNetworkConfiguration { + signed_entity_types_config: HashMap::new(), + ..default() + }; + + let result = config.get_cardano_transactions_signing_config(); + assert!(result.is_none()); + } +} diff --git a/internal/mithril-protocol-config/src/test/double/dummies.rs b/internal/mithril-protocol-config/src/test/double/dummies.rs new file mode 100644 index 00000000000..775b80d6691 --- /dev/null +++ b/internal/mithril-protocol-config/src/test/double/dummies.rs @@ -0,0 +1,35 @@ +use std::collections::{BTreeSet, HashMap}; + +use mithril_common::{ + entities::{CardanoTransactionsSigningConfig, SignedEntityTypeDiscriminants}, + test::double::{Dummy, fake_data}, +}; + +use crate::model::{MithrilNetworkConfiguration, SignedEntityTypeConfiguration}; + +impl Dummy for MithrilNetworkConfiguration { + /// Return a dummy [MithrilNetworkConfiguration] (test-only). + fn dummy() -> Self { + let beacon = fake_data::beacon(); + + let signer_registration_protocol_parameters = fake_data::protocol_parameters(); + + let mut available_signed_entity_types = BTreeSet::new(); + available_signed_entity_types.insert(SignedEntityTypeDiscriminants::CardanoTransactions); + + let mut signed_entity_types_config = HashMap::new(); + signed_entity_types_config.insert( + SignedEntityTypeDiscriminants::CardanoTransactions, + SignedEntityTypeConfiguration::CardanoTransactions( + CardanoTransactionsSigningConfig::dummy(), + ), + ); + + Self { + epoch: beacon.epoch, + signer_registration_protocol_parameters, + available_signed_entity_types, + signed_entity_types_config, + } + } +} diff --git a/internal/mithril-protocol-config/src/test/double/mithril_network_configuration_provider.rs b/internal/mithril-protocol-config/src/test/double/mithril_network_configuration_provider.rs new file mode 100644 index 00000000000..afa62c9ee02 --- /dev/null +++ b/internal/mithril-protocol-config/src/test/double/mithril_network_configuration_provider.rs @@ -0,0 +1,168 @@ +use std::{ + collections::{BTreeSet, HashMap}, + sync::Arc, +}; + +use tokio::sync::RwLock; + +use crate::{ + interface::MithrilNetworkConfigurationProvider, + model::{MithrilNetworkConfiguration, SignedEntityTypeConfiguration}, +}; +use anyhow::Error; +use async_trait::async_trait; +use mithril_common::{ + StdResult, + entities::{ProtocolParameters, SignedEntityTypeDiscriminants, TimePoint}, +}; +use mithril_ticker::{MithrilTickerService, TickerService}; + +/// A fake [MithrilNetworkConfigurationProvider] that return [MithrilNetworkConfiguration] +pub struct FakeMithrilNetworkConfigurationProvider { + pub signer_registration_protocol_parameters: ProtocolParameters, + + pub available_signed_entity_types: RwLock>, + + pub signed_entity_types_config: + HashMap, + + ticker_service: Arc, +} + +impl FakeMithrilNetworkConfigurationProvider { + pub fn new( + signer_registration_protocol_parameters: ProtocolParameters, + available_signed_entity_types: BTreeSet, + signed_entity_types_config: HashMap< + SignedEntityTypeDiscriminants, + SignedEntityTypeConfiguration, + >, + ticker_service: Arc, + ) -> Self { + Self { + signer_registration_protocol_parameters, + available_signed_entity_types: RwLock::new(available_signed_entity_types), + signed_entity_types_config, + ticker_service, + } + } + + async fn get_time_point(&self) -> Result { + let time_point = self.ticker_service.get_current_time_point().await?; + + Ok(time_point) + } + + pub async fn change_allowed_discriminants( + &self, + discriminants: &BTreeSet, + ) { + let mut available_signed_entity_types = self.available_signed_entity_types.write().await; + *available_signed_entity_types = discriminants.clone(); + } +} + +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] +impl MithrilNetworkConfigurationProvider for FakeMithrilNetworkConfigurationProvider { + async fn get(&self) -> StdResult { + let time_point = self.get_time_point().await?; + + let available_signed_entity_types = self.available_signed_entity_types.read().await; + + Ok(MithrilNetworkConfiguration { + epoch: time_point.epoch, + signer_registration_protocol_parameters: self + .signer_registration_protocol_parameters + .clone(), + available_signed_entity_types: available_signed_entity_types.clone(), + signed_entity_types_config: self.signed_entity_types_config.clone(), + }) + } +} + +#[cfg(test)] +mod tests { + use std::{ + collections::{BTreeSet, HashMap}, + sync::Arc, + }; + + use mithril_common::{ + entities::{ + BlockNumber, CardanoTransactionsSigningConfig, ChainPoint, Epoch, ProtocolParameters, + SignedEntityTypeDiscriminants, TimePoint, + }, + test::double::Dummy, + }; + use mithril_ticker::MithrilTickerService; + + use crate::{ + interface::MithrilNetworkConfigurationProvider, model::SignedEntityTypeConfiguration, + test::double::mithril_network_configuration_provider::FakeMithrilNetworkConfigurationProvider, + }; + use mithril_cardano_node_chain::test::double::FakeChainObserver; + use mithril_cardano_node_internal_database::test::double::DumbImmutableFileObserver; + + async fn ticker_service() -> Arc { + let immutable_observer = Arc::new(DumbImmutableFileObserver::new()); + immutable_observer.shall_return(Some(1)).await; + let chain_observer = Arc::new(FakeChainObserver::new(Some(TimePoint { + epoch: Epoch(1), + immutable_file_number: 1, + chain_point: ChainPoint::dummy(), + }))); + + Arc::new(MithrilTickerService::new( + chain_observer.clone(), + immutable_observer.clone(), + )) + } + + #[tokio::test] + async fn test_get() { + let signer_registration_protocol_parameters = ProtocolParameters { + k: 2, + m: 3, + phi_f: 0.5, + }; + let available_signed_entity_types = BTreeSet::from([ + SignedEntityTypeDiscriminants::MithrilStakeDistribution, + SignedEntityTypeDiscriminants::CardanoTransactions, + ]); + let signed_entity_types_config = HashMap::from([( + SignedEntityTypeDiscriminants::CardanoTransactions, + SignedEntityTypeConfiguration::CardanoTransactions(CardanoTransactionsSigningConfig { + security_parameter: BlockNumber(12), + step: BlockNumber(10), + }), + )]); + + let mithril_network_configuration_provider = FakeMithrilNetworkConfigurationProvider::new( + signer_registration_protocol_parameters.clone(), + available_signed_entity_types.clone(), + signed_entity_types_config.clone(), + ticker_service().await, + ); + + let actual_config = mithril_network_configuration_provider.get().await.unwrap(); + + assert_eq!(actual_config.epoch, Epoch(1)); + assert_eq!( + actual_config.signer_registration_protocol_parameters, + ProtocolParameters { + k: 2, + m: 3, + phi_f: 0.5 + } + ); + assert_eq!( + actual_config.available_signed_entity_types, + available_signed_entity_types + ); + assert_eq!( + actual_config.signed_entity_types_config, + signed_entity_types_config + ); + } +} diff --git a/internal/mithril-protocol-config/src/test/double/mod.rs b/internal/mithril-protocol-config/src/test/double/mod.rs new file mode 100644 index 00000000000..834deaf031b --- /dev/null +++ b/internal/mithril-protocol-config/src/test/double/mod.rs @@ -0,0 +1,2 @@ +mod dummies; +pub mod mithril_network_configuration_provider; diff --git a/internal/mithril-protocol-config/src/test/mod.rs b/internal/mithril-protocol-config/src/test/mod.rs new file mode 100644 index 00000000000..90278e1483d --- /dev/null +++ b/internal/mithril-protocol-config/src/test/mod.rs @@ -0,0 +1 @@ +pub mod double; diff --git a/mithril-common/src/entities/protocol_parameters.rs b/mithril-common/src/entities/protocol_parameters.rs index 346c51445f2..58177aa23d2 100644 --- a/mithril-common/src/entities/protocol_parameters.rs +++ b/mithril-common/src/entities/protocol_parameters.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; /// Protocol cryptographic parameters -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct ProtocolParameters { /// Quorum parameter pub k: u64, diff --git a/mithril-common/src/entities/signed_entity_type.rs b/mithril-common/src/entities/signed_entity_type.rs index 397c8c0057e..62a0a385999 100644 --- a/mithril-common/src/entities/signed_entity_type.rs +++ b/mithril-common/src/entities/signed_entity_type.rs @@ -49,6 +49,7 @@ const ENTITY_TYPE_CARDANO_DATABASE: usize = 4; PartialOrd, Ord, EnumIter, + Hash, ))] pub enum SignedEntityType { /// Mithril stake distribution diff --git a/mithril-signer/Cargo.toml b/mithril-signer/Cargo.toml index 6beed3eb73c..2c062226b3e 100644 --- a/mithril-signer/Cargo.toml +++ b/mithril-signer/Cargo.toml @@ -32,6 +32,7 @@ mithril-doc = { path = "../internal/mithril-doc" } mithril-era = { path = "../internal/mithril-era" } mithril-metric = { path = "../internal/mithril-metric" } mithril-persistence = { path = "../internal/mithril-persistence" } +mithril-protocol-config = { path = "../internal/mithril-protocol-config" } mithril-signed-entity-lock = { path = "../internal/signed-entity/mithril-signed-entity-lock" } mithril-signed-entity-preloader = { path = "../internal/signed-entity/mithril-signed-entity-preloader" } mithril-ticker = { path = "../internal/mithril-ticker" } diff --git a/mithril-signer/src/dependency_injection/builder.rs b/mithril-signer/src/dependency_injection/builder.rs index 2a737d04244..32be1b7c7ed 100644 --- a/mithril-signer/src/dependency_injection/builder.rs +++ b/mithril-signer/src/dependency_injection/builder.rs @@ -41,6 +41,11 @@ use mithril_persistence::database::repository::CardanoTransactionRepository; use mithril_persistence::database::{ApplicationNodeType, SqlMigration}; use mithril_persistence::sqlite::{ConnectionBuilder, SqliteConnection, SqliteConnectionPool}; +use mithril_protocol_config::{ + http_client::http_impl::HttpMithrilNetworkConfigurationProvider, + interface::MithrilNetworkConfigurationProvider, +}; + #[cfg(feature = "future_dmq")] use mithril_dmq::{DmqMessageBuilder, DmqPublisherClientPallas}; @@ -373,8 +378,16 @@ impl<'a> DependenciesBuilder<'a> { self.root_logger(), )); let metrics_service = Arc::new(MetricsService::new(self.root_logger())?); - let preloader_activation = - CardanoTransactionsPreloaderActivationSigner::new(aggregator_client.clone()); + let network_configuration_service: Arc = + Arc::new(HttpMithrilNetworkConfigurationProvider::new( + self.config.aggregator_endpoint.clone(), + self.config.relay_endpoint.clone(), + api_version_provider.clone(), + self.root_logger(), + )); + let preloader_activation = CardanoTransactionsPreloaderActivationSigner::new( + network_configuration_service.clone(), + ); let cardano_transactions_preloader = Arc::new(CardanoTransactionsPreloader::new( signed_entity_type_lock.clone(), preloader_transactions_importer, @@ -522,6 +535,7 @@ impl<'a> DependenciesBuilder<'a> { epoch_service, certifier, kes_signer, + network_configuration_service, }; Ok(services) diff --git a/mithril-signer/src/dependency_injection/containers.rs b/mithril-signer/src/dependency_injection/containers.rs index 7f76d16d69b..ce74c456a38 100644 --- a/mithril-signer/src/dependency_injection/containers.rs +++ b/mithril-signer/src/dependency_injection/containers.rs @@ -9,6 +9,7 @@ use mithril_cardano_node_chain::chain_observer::ChainObserver; use mithril_cardano_node_internal_database::digesters::ImmutableDigester; use mithril_era::{EraChecker, EraReader}; use mithril_persistence::store::StakeStorer; +use mithril_protocol_config::interface::MithrilNetworkConfigurationProvider; use mithril_signed_entity_lock::SignedEntityTypeLock; use mithril_signed_entity_preloader::CardanoTransactionsPreloader; use mithril_ticker::TickerService; @@ -26,6 +27,7 @@ type DigesterService = Arc; type SingleSignerService = Arc; type TimePointProviderService = Arc; type ProtocolInitializerStoreService = Arc; +type MithrilNetworkConfigurationService = Arc; /// EpochServiceWrapper wraps a [EpochService] pub type EpochServiceWrapper = Arc>; @@ -85,4 +87,7 @@ pub struct SignerDependencyContainer { /// Kes signer service pub kes_signer: Option>, + + /// Mithril network configuration service + pub network_configuration_service: MithrilNetworkConfigurationService, } diff --git a/mithril-signer/src/runtime/runner.rs b/mithril-signer/src/runtime/runner.rs index d259f181757..b2b8b7c98b9 100644 --- a/mithril-signer/src/runtime/runner.rs +++ b/mithril-signer/src/runtime/runner.rs @@ -10,6 +10,7 @@ use mithril_common::entities::{ Epoch, PartyId, ProtocolMessage, SignedEntityType, Signer, TimePoint, }; use mithril_common::logging::LoggerExtensions; +use mithril_protocol_config::model::MithrilNetworkConfiguration; use crate::Configuration; use crate::dependency_injection::SignerDependencyContainer; @@ -19,6 +20,9 @@ use crate::services::{EpochService, MithrilProtocolInitializerBuilder}; /// This trait is mainly intended for mocking. #[async_trait] pub trait Runner: Send + Sync { + ///Fetch the configuration parameters of a Mithril network + async fn get_mithril_network_configuration(&self) -> StdResult; + /// Fetch the current epoch settings if any. async fn get_epoch_settings(&self) -> StdResult>; @@ -38,7 +42,12 @@ pub trait Runner: Send + Sync { async fn can_sign_current_epoch(&self) -> StdResult; /// Register epoch information - async fn inform_epoch_settings(&self, epoch_settings: SignerEpochSettings) -> StdResult<()>; + async fn inform_epoch_settings( + &self, + mithril_network_configuration: MithrilNetworkConfiguration, + current_signer: Vec, + next_signer: Vec, + ) -> StdResult<()>; /// Create the message to be signed with the single signature. async fn compute_message( @@ -102,6 +111,12 @@ impl SignerRunner { #[cfg_attr(test, mockall::automock)] #[async_trait] impl Runner for SignerRunner { + async fn get_mithril_network_configuration(&self) -> StdResult { + debug!(self.logger, ">> get_mithril_network_configuration"); + + self.services.network_configuration_service.get().await + } + async fn get_epoch_settings(&self) -> StdResult> { debug!(self.logger, ">> get_epoch_settings"); @@ -240,25 +255,22 @@ impl Runner for SignerRunner { epoch_service.can_signer_sign_current_epoch(self.services.single_signer.get_party_id()) } - async fn inform_epoch_settings(&self, epoch_settings: SignerEpochSettings) -> StdResult<()> { + async fn inform_epoch_settings( + &self, + mithril_network_configuration: MithrilNetworkConfiguration, + current_signer: Vec, + next_signer: Vec, + ) -> StdResult<()> { debug!( self.logger, - ">> inform_epoch_settings(epoch:{})", epoch_settings.epoch + ">> inform_epoch_settings(epoch:{})", mithril_network_configuration.epoch ); - let aggregator_features = self - .services - .certificate_handler - .retrieve_aggregator_features() - .await?; self.services .epoch_service .write() .await - .inform_epoch_settings( - epoch_settings, - aggregator_features.capabilities.signed_entity_types, - ) + .inform_epoch_settings(mithril_network_configuration, current_signer, next_signer) .await } @@ -347,7 +359,6 @@ mod tests { api_version::APIVersionProvider, crypto_helper::{MKMap, MKMapNode, MKTreeNode, MKTreeStoreInMemory, MKTreeStorer}, entities::{BlockNumber, BlockRange, Epoch, SignedEntityTypeDiscriminants}, - messages::{AggregatorCapabilities, AggregatorFeaturesMessage}, signable_builder::{ BlockRangeRootRetriever, CardanoStakeDistributionSignableBuilder, CardanoTransactionsSignableBuilder, MithrilSignableBuilderService, @@ -359,6 +370,7 @@ mod tests { }, }; use mithril_era::{EraChecker, EraReader, adapters::EraReaderBootstrapAdapter}; + use mithril_protocol_config::test::double::mithril_network_configuration_provider::FakeMithrilNetworkConfigurationProvider; use mithril_signed_entity_lock::SignedEntityTypeLock; use mithril_signed_entity_preloader::{ CardanoTransactionsPreloader, CardanoTransactionsPreloaderActivation, @@ -514,6 +526,13 @@ mod tests { )); let kes_signer = None; + let network_configuration_service = Arc::new(FakeMithrilNetworkConfigurationProvider::new( + Default::default(), + Default::default(), + Default::default(), + ticker_service.clone(), + )); + SignerDependencyContainer { stake_store, certificate_handler: aggregator_client, @@ -533,6 +552,7 @@ mod tests { epoch_service, certifier, kes_signer, + network_configuration_service, } } @@ -623,14 +643,20 @@ mod tests { .unwrap(); let runner = init_runner(Some(services), None).await; - // inform epoch settings - let epoch_settings = SignerEpochSettings { + + let mithril_network_configuration = MithrilNetworkConfiguration { epoch: current_epoch, - current_signers: fixture.signers(), - next_signers: fixture.signers(), - ..SignerEpochSettings::dummy().clone() + ..MithrilNetworkConfiguration::dummy() }; - runner.inform_epoch_settings(epoch_settings).await.unwrap(); + + runner + .inform_epoch_settings( + mithril_network_configuration, + fixture.signers(), + fixture.signers(), + ) + .await + .unwrap(); runner .register_signer_to_aggregator() @@ -679,29 +705,31 @@ mod tests { } #[tokio::test] - async fn test_inform_epoch_setting_pass_allowed_discriminant_to_epoch_service() { + async fn test_inform_epoch_setting_pass_available_signed_entity_types_to_epoch_service() { let mut services = init_services().await; let certificate_handler = Arc::new(DumbAggregatorClient::default()); - certificate_handler - .set_aggregator_features(AggregatorFeaturesMessage { - capabilities: AggregatorCapabilities { - signed_entity_types: BTreeSet::from([ - SignedEntityTypeDiscriminants::MithrilStakeDistribution, - SignedEntityTypeDiscriminants::CardanoTransactions, - ]), - ..AggregatorFeaturesMessage::dummy().capabilities - }, - ..AggregatorFeaturesMessage::dummy() - }) - .await; + services.certificate_handler = certificate_handler; let runner = init_runner(Some(services), None).await; - let epoch_settings = SignerEpochSettings { + let mithril_network_configuration = MithrilNetworkConfiguration { epoch: Epoch(1), - ..SignerEpochSettings::dummy() + available_signed_entity_types: BTreeSet::from([ + SignedEntityTypeDiscriminants::MithrilStakeDistribution, + SignedEntityTypeDiscriminants::CardanoTransactions, + ]), + ..MithrilNetworkConfiguration::dummy() }; - runner.inform_epoch_settings(epoch_settings).await.unwrap(); + + // Signers + let signers = fake_data::signers(5); + let current_signers = signers[1..3].to_vec(); + let next_signers = signers[2..5].to_vec(); + + runner + .inform_epoch_settings(mithril_network_configuration, current_signers, next_signers) + .await + .unwrap(); let epoch_service = runner.services.epoch_service.read().await; let recorded_allowed_discriminants = epoch_service.allowed_discriminants().unwrap(); diff --git a/mithril-signer/src/runtime/state_machine.rs b/mithril-signer/src/runtime/state_machine.rs index aea825ddcae..dc9b1f964ad 100644 --- a/mithril-signer/src/runtime/state_machine.rs +++ b/mithril-signer/src/runtime/state_machine.rs @@ -1,20 +1,17 @@ use anyhow::Error; use chrono::Local; +use mithril_protocol_config::model::MithrilNetworkConfiguration; use slog::{Logger, debug, info}; use std::{fmt::Display, ops::Deref, sync::Arc, time::Duration}; use tokio::sync::Mutex; use mithril_common::{ crypto_helper::ProtocolInitializerError, - entities::{Epoch, TimePoint}, + entities::{Epoch, Signer, TimePoint}, logging::LoggerExtensions, }; -use crate::{ - MetricsService, - entities::{BeaconToSign, SignerEpochSettings}, - services::AggregatorClientError, -}; +use crate::{MetricsService, entities::BeaconToSign, services::AggregatorClientError}; use super::{Runner, RuntimeError}; @@ -167,7 +164,7 @@ impl StateMachine { "→ Epoch has changed, transiting to Unregistered" ); *state = self.transition_from_unregistered_to_unregistered(new_epoch).await?; - } else if let Some(epoch_settings) = self + } else if let Some(signer_registrations) = self .runner .get_epoch_settings() .await @@ -176,19 +173,33 @@ impl StateMachine { nested_error: Some(e), })? { - info!(self.logger, "→ Epoch settings found"); - if epoch_settings.epoch >= *epoch { + info!(self.logger, "→ Epoch Signer registrations found"); + let network_configuration = self + .runner + .get_mithril_network_configuration() + .await + .map_err(|e| RuntimeError::KeepState { + message: "could not retrieve mithril network configuration".to_string(), + nested_error: Some(e), + })?; + info!(self.logger, "→ Mithril network configuration found"); + + if network_configuration.epoch >= *epoch { info!(self.logger, "New Epoch found"); info!(self.logger, " ⋅ Transiting to Registered"); *state = self .transition_from_unregistered_to_one_of_registered_states( - epoch_settings, + network_configuration, + signer_registrations.current_signers, + signer_registrations.next_signers, ) .await?; } else { info!( - self.logger, " ⋅ Epoch settings found, but its epoch is behind the known epoch, waiting…"; - "epoch_settings" => ?epoch_settings, + self.logger, " ⋅ Signer settings and Network Configuration found, but its epoch is behind the known epoch, waiting…"; + "network_configuration" => ?network_configuration, + "current_singer" => ?signer_registrations.current_signers, + "next_signer" => ?signer_registrations.next_signers, "known_epoch" => ?epoch, ); } @@ -286,7 +297,9 @@ impl StateMachine { /// Launch the transition process from the `Unregistered` to `ReadyToSign` or `RegisteredNotAbleToSign` state. async fn transition_from_unregistered_to_one_of_registered_states( &self, - epoch_settings: SignerEpochSettings, + mithril_network_configuration: MithrilNetworkConfiguration, + current_signer: Vec, + next_signer: Vec, ) -> Result { self.metrics_service .get_signer_registration_total_since_startup_counter() @@ -302,7 +315,7 @@ impl StateMachine { })?; self.runner - .inform_epoch_settings(epoch_settings) + .inform_epoch_settings(mithril_network_configuration, current_signer, next_signer) .await .map_err(|e| RuntimeError::KeepState { message: format!( @@ -474,11 +487,12 @@ impl StateMachine { mod tests { use anyhow::anyhow; use chrono::DateTime; - use mockall::predicate; + use mithril_protocol_config::model::MithrilNetworkConfiguration; use mithril_common::entities::{ChainPoint, Epoch, ProtocolMessage, SignedEntityType}; use mithril_common::test::double::{Dummy, fake_data}; + use crate::SignerEpochSettings; use crate::runtime::runner::MockSignerRunner; use crate::services::AggregatorClientError; use crate::test_tools::TestLogger; @@ -528,7 +542,7 @@ mod tests { async fn unregistered_epoch_settings_behind_known_epoch() { let mut runner = MockSignerRunner::new(); let epoch_settings = SignerEpochSettings { - epoch: Epoch(3), + epoch: Epoch(999), // epoch is no longer read from get_epoch_settings anymore but from mithril network configuration registration_protocol_parameters: fake_data::protocol_parameters(), current_signers: vec![], next_signers: vec![], @@ -539,6 +553,17 @@ mod tests { .expect_get_epoch_settings() .once() .returning(move || Ok(Some(epoch_settings.to_owned()))); + runner + .expect_get_mithril_network_configuration() + .once() + .returning(|| { + Ok(MithrilNetworkConfiguration { + epoch: Epoch(3), + signer_registration_protocol_parameters: fake_data::protocol_parameters(), + available_signed_entity_types: Default::default(), + signed_entity_types_config: Default::default(), + }) + }); runner.expect_get_current_time_point().once().returning(|| { Ok(TimePoint { epoch: Epoch(4), @@ -567,11 +592,15 @@ mod tests { .once() .returning(|| Ok(Some(SignerEpochSettings::dummy()))); + runner + .expect_get_mithril_network_configuration() + .once() + .returning(|| Ok(MithrilNetworkConfiguration::dummy())); + runner .expect_inform_epoch_settings() - .with(predicate::eq(SignerEpochSettings::dummy())) .once() - .returning(|_| Ok(())); + .returning(|_, _, _| Ok(())); runner .expect_get_current_time_point() @@ -615,11 +644,15 @@ mod tests { .once() .returning(|| Ok(Some(SignerEpochSettings::dummy()))); + runner + .expect_get_mithril_network_configuration() + .once() + .returning(|| Ok(MithrilNetworkConfiguration::dummy())); + runner .expect_inform_epoch_settings() - .with(predicate::eq(SignerEpochSettings::dummy())) .once() - .returning(|_| Ok(())); + .returning(|_, _, _| Ok(())); runner .expect_get_current_time_point() @@ -667,11 +700,15 @@ mod tests { .once() .returning(|| Ok(Some(SignerEpochSettings::dummy()))); + runner + .expect_get_mithril_network_configuration() + .once() + .returning(|| Ok(MithrilNetworkConfiguration::dummy())); + runner .expect_inform_epoch_settings() - .with(predicate::eq(SignerEpochSettings::dummy())) .once() - .returning(|_| Ok(())); + .returning(|_, _, _| Ok(())); runner .expect_get_current_time_point() diff --git a/mithril-signer/src/services/aggregator_client.rs b/mithril-signer/src/services/aggregator_client.rs index 1ccd35c0c65..25e388a0208 100644 --- a/mithril-signer/src/services/aggregator_client.rs +++ b/mithril-signer/src/services/aggregator_client.rs @@ -399,14 +399,6 @@ pub(crate) mod dumb { pub async fn get_last_registered_signer(&self) -> Option { self.last_registered_signer.read().await.clone() } - - pub async fn set_aggregator_features( - &self, - aggregator_features: AggregatorFeaturesMessage, - ) { - let mut aggregator_features_writer = self.aggregator_features.write().await; - *aggregator_features_writer = aggregator_features; - } } impl Default for DumbAggregatorClient { diff --git a/mithril-signer/src/services/cardano_transactions/preloader_checker.rs b/mithril-signer/src/services/cardano_transactions/preloader_checker.rs index 5343eb171ea..5f08bf7f9ec 100644 --- a/mithril-signer/src/services/cardano_transactions/preloader_checker.rs +++ b/mithril-signer/src/services/cardano_transactions/preloader_checker.rs @@ -4,32 +4,34 @@ use anyhow::Context; use async_trait::async_trait; use mithril_common::{StdResult, entities::SignedEntityTypeDiscriminants}; +use mithril_protocol_config::interface::MithrilNetworkConfigurationProvider; use mithril_signed_entity_preloader::CardanoTransactionsPreloaderChecker; - -use crate::services::AggregatorClient; - /// CardanoTransactionsPreloaderActivationSigner pub struct CardanoTransactionsPreloaderActivationSigner { - aggregator_client: Arc, + network_configuration_provider: Arc, } impl CardanoTransactionsPreloaderActivationSigner { /// Create a new instance of `CardanoTransactionsPreloaderActivationSigner` - pub fn new(aggregator_client: Arc) -> Self { - Self { aggregator_client } + pub fn new( + network_configuration_provider: Arc, + ) -> Self { + Self { + network_configuration_provider, + } } } #[async_trait] impl CardanoTransactionsPreloaderChecker for CardanoTransactionsPreloaderActivationSigner { async fn is_activated(&self) -> StdResult { - let message = self - .aggregator_client - .retrieve_aggregator_features() + let configuration = self + .network_configuration_provider + .get() .await - .with_context(|| "An error occurred while calling the Aggregator")?; + .context("An error occurred while retrieving Mithril network configuration")?; - let activated_signed_entity_types = message.capabilities.signed_entity_types; + let activated_signed_entity_types = configuration.available_signed_entity_types; Ok(activated_signed_entity_types .contains(&SignedEntityTypeDiscriminants::CardanoTransactions)) @@ -39,32 +41,38 @@ impl CardanoTransactionsPreloaderChecker for CardanoTransactionsPreloaderActivat #[cfg(test)] mod tests { use anyhow::anyhow; + use mithril_common::{entities::SignedEntityTypeDiscriminants, test::double::Dummy}; + use mithril_protocol_config::model::MithrilNetworkConfiguration; + use mockall::mock; use std::collections::BTreeSet; - use mithril_common::{ - entities::SignedEntityTypeDiscriminants, messages::AggregatorFeaturesMessage, - test::double::Dummy, - }; + use super::*; - use crate::services::{AggregatorClientError, MockAggregatorClient}; + mock! { + pub MithrilNetworkConfigurationProvider {} - use super::*; + #[async_trait] + impl MithrilNetworkConfigurationProvider for MithrilNetworkConfigurationProvider { + async fn get(&self) -> StdResult; + } + } #[tokio::test] async fn preloader_activation_state_activate_preloader_when_cardano_transactions_not_in_aggregator_capabilities() { - let mut aggregator_client = MockAggregatorClient::new(); - aggregator_client - .expect_retrieve_aggregator_features() - .times(1) - .returning(|| { - let mut message = AggregatorFeaturesMessage::dummy(); - message.capabilities.signed_entity_types = - BTreeSet::from([SignedEntityTypeDiscriminants::MithrilStakeDistribution]); - Ok(message) - }); - let preloader = - CardanoTransactionsPreloaderActivationSigner::new(Arc::new(aggregator_client)); + let mut network_configuration_provider = MockMithrilNetworkConfigurationProvider::new(); + network_configuration_provider.expect_get().times(1).returning(|| { + Ok(MithrilNetworkConfiguration { + available_signed_entity_types: BTreeSet::from([ + SignedEntityTypeDiscriminants::MithrilStakeDistribution, + ]), + ..Dummy::dummy() + }) + }); + + let preloader = CardanoTransactionsPreloaderActivationSigner::new(Arc::new( + network_configuration_provider, + )); let is_activated = preloader.is_activated().await.unwrap(); @@ -74,18 +82,19 @@ mod tests { #[tokio::test] async fn preloader_activation_state_activate_preloader_when_cardano_transactions_in_aggregator_capabilities() { - let mut aggregator_client = MockAggregatorClient::new(); - aggregator_client - .expect_retrieve_aggregator_features() - .times(1) - .returning(|| { - let mut message = AggregatorFeaturesMessage::dummy(); - message.capabilities.signed_entity_types = - BTreeSet::from([SignedEntityTypeDiscriminants::CardanoTransactions]); - Ok(message) - }); - let preloader = - CardanoTransactionsPreloaderActivationSigner::new(Arc::new(aggregator_client)); + let mut network_configuration_provider = MockMithrilNetworkConfigurationProvider::new(); + network_configuration_provider.expect_get().times(1).returning(|| { + Ok(MithrilNetworkConfiguration { + available_signed_entity_types: BTreeSet::from([ + SignedEntityTypeDiscriminants::CardanoTransactions, + ]), + ..Dummy::dummy() + }) + }); + + let preloader = CardanoTransactionsPreloaderActivationSigner::new(Arc::new( + network_configuration_provider, + )); let is_activated = preloader.is_activated().await.unwrap(); @@ -94,17 +103,15 @@ mod tests { #[tokio::test] async fn preloader_activation_state_activate_preloader_when_aggregator_call_fails() { - let mut aggregator_client = MockAggregatorClient::new(); - aggregator_client - .expect_retrieve_aggregator_features() + let mut network_configuration_provider = MockMithrilNetworkConfigurationProvider::new(); + network_configuration_provider + .expect_get() .times(1) - .returning(|| { - Err(AggregatorClientError::RemoteServerTechnical(anyhow!( - "Aggregator call failed" - ))) - }); - let preloader = - CardanoTransactionsPreloaderActivationSigner::new(Arc::new(aggregator_client)); + .returning(|| Err(anyhow!("Aggregator call failure"))); + + let preloader = CardanoTransactionsPreloaderActivationSigner::new(Arc::new( + network_configuration_provider, + )); preloader .is_activated() diff --git a/mithril-signer/src/services/epoch_service.rs b/mithril-signer/src/services/epoch_service.rs index 4f13e45937f..c1fb77bcd35 100644 --- a/mithril-signer/src/services/epoch_service.rs +++ b/mithril-signer/src/services/epoch_service.rs @@ -1,5 +1,6 @@ use anyhow::anyhow; use async_trait::async_trait; +use mithril_protocol_config::model::MithrilNetworkConfiguration; use slog::{Logger, debug, trace, warn}; use std::collections::BTreeSet; use std::sync::Arc; @@ -7,7 +8,6 @@ use thiserror::Error; use crate::RunnerError; use crate::dependency_injection::EpochServiceWrapper; -use crate::entities::SignerEpochSettings; use crate::services::SignedEntityConfigProvider; use crate::store::ProtocolInitializerStorer; use mithril_common::StdResult; @@ -36,8 +36,9 @@ pub trait EpochService: Sync + Send { /// internal state for the new epoch. async fn inform_epoch_settings( &mut self, - epoch_settings: SignerEpochSettings, - allowed_discriminants: BTreeSet, + mithril_network_configuration: MithrilNetworkConfiguration, + current_signers: Vec, + next_signers: Vec, ) -> StdResult<()>; /// Get the current epoch for which the data stored in this service are computed. @@ -168,25 +169,38 @@ impl MithrilEpochService { impl EpochService for MithrilEpochService { async fn inform_epoch_settings( &mut self, - epoch_settings: SignerEpochSettings, - allowed_discriminants: BTreeSet, + mithril_network_configuration: MithrilNetworkConfiguration, + current_signers: Vec, + next_signers: Vec, ) -> StdResult<()> { - debug!(self.logger, ">> inform_epoch_settings"; "epoch_settings" => ?epoch_settings); + debug!(self.logger, ">> inform_epoch_settings"; "mithril_network_configuration" => ?mithril_network_configuration, "current_signers" => ?current_signers, "next_signers" => ?next_signers); + + let epoch = mithril_network_configuration.epoch; + + let registration_protocol_parameters = mithril_network_configuration + .signer_registration_protocol_parameters + .clone(); - let epoch = epoch_settings.epoch; let protocol_initializer = self .protocol_initializer_store .get_protocol_initializer(epoch.offset_to_signer_retrieval_epoch()?) .await?; + let allowed_discriminants = + mithril_network_configuration.available_signed_entity_types.clone(); + + let cardano_transactions_signing_config = mithril_network_configuration + .get_cardano_transactions_signing_config() + .clone(); + self.epoch_data = Some(EpochData { epoch, - registration_protocol_parameters: epoch_settings.registration_protocol_parameters, + registration_protocol_parameters, protocol_initializer, - current_signers: epoch_settings.current_signers, - next_signers: epoch_settings.next_signers, + current_signers, + next_signers, allowed_discriminants, - cardano_transactions_signing_config: epoch_settings.cardano_transactions_signing_config, + cardano_transactions_signing_config, }); Ok(()) @@ -363,8 +377,9 @@ pub(crate) mod mock_epoch_service { impl EpochService for EpochServiceImpl { async fn inform_epoch_settings( &mut self, - epoch_settings: SignerEpochSettings, - allowed_discriminants: BTreeSet, + mithril_network_configuration: MithrilNetworkConfiguration, + current_signers: Vec, + next_signers: Vec, ) -> StdResult<()>; fn epoch_of_current_data(&self) -> StdResult; @@ -405,6 +420,8 @@ pub(crate) mod mock_epoch_service { #[cfg(test)] mod tests { + use mithril_protocol_config::model::SignedEntityTypeConfiguration; + use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; @@ -416,7 +433,6 @@ mod tests { use crate::database::repository::{ProtocolInitializerRepository, StakePoolStore}; use crate::database::test_helper::main_db_connection; - use crate::entities::SignerEpochSettings; use crate::services::MithrilProtocolInitializerBuilder; use crate::test_tools::TestLogger; @@ -453,27 +469,34 @@ mod tests { { let fixtures = MithrilFixtureBuilder::default().with_signers(10).build(); let protocol_initializer = fixtures.signers_fixture()[0].protocol_initializer.to_owned(); - let epoch = Epoch(12); - let signers = fixtures.signers(); let connection = Arc::new(main_db_connection().unwrap()); let stake_store = Arc::new(StakePoolStore::new(connection.clone(), None)); let protocol_initializer_store = Arc::new(ProtocolInitializerRepository::new(connection, None)); - let epoch_settings = SignerEpochSettings { + let epoch = Epoch(12); + let mithril_network_configuration = MithrilNetworkConfiguration { epoch, - current_signers: signers[..5].to_vec(), - ..SignerEpochSettings::dummy().clone() + available_signed_entity_types: BTreeSet::new(), + ..MithrilNetworkConfiguration::dummy().clone() }; + let signers = fixtures.signers(); + let current_signers = signers[..5].to_vec(); + let next_signers = signers[2..5].to_vec(); + let mut service = MithrilEpochService::new( stake_store, protocol_initializer_store, TestLogger::stdout(), ); service - .inform_epoch_settings(epoch_settings.clone(), BTreeSet::new()) + .inform_epoch_settings( + mithril_network_configuration.clone(), + current_signers.clone(), + next_signers.clone(), + ) .await .unwrap(); @@ -627,9 +650,7 @@ mod tests { #[tokio::test] async fn test_data_are_available_after_register_epoch_settings_call() { - let epoch = Epoch(12); // Signers and stake distribution - let signers = fake_data::signers(10); // Init stores let connection = Arc::new(main_db_connection().unwrap()); @@ -637,14 +658,19 @@ mod tests { let protocol_initializer_store = Arc::new(ProtocolInitializerRepository::new(connection, None)); - // Epoch settings - let epoch_settings = SignerEpochSettings { + let epoch = Epoch(12); + let mithril_network_configuration = MithrilNetworkConfiguration { epoch, - current_signers: signers[2..5].to_vec(), - next_signers: signers[3..7].to_vec(), - ..SignerEpochSettings::dummy().clone() + available_signed_entity_types: BTreeSet::from([ + SignedEntityTypeDiscriminants::CardanoImmutableFilesFull, + ]), + ..MithrilNetworkConfiguration::dummy().clone() }; + let signers = fake_data::signers(10); + let current_signers = signers[2..5].to_vec(); + let next_signers = signers[3..7].to_vec(); + // Build service and register epoch settings let mut service = MithrilEpochService::new( stake_store, @@ -654,8 +680,9 @@ mod tests { service .inform_epoch_settings( - epoch_settings.clone(), - BTreeSet::from([SignedEntityTypeDiscriminants::CardanoImmutableFilesFull]), + mithril_network_configuration.clone(), + current_signers.clone(), + next_signers.clone(), ) .await .unwrap(); @@ -663,23 +690,23 @@ mod tests { // Check current_signers { let current_signers = service.current_signers().unwrap(); - let expected_current_signers = epoch_settings.current_signers.clone(); + let expected_current_signers = current_signers.clone(); assert_eq!(expected_current_signers, *current_signers); } // Check next_signers { let next_signers = service.next_signers().unwrap(); - let expected_next_signers = epoch_settings.next_signers.clone(); + let expected_next_signers = next_signers.clone(); assert_eq!(expected_next_signers, *next_signers); } // Check other data assert_eq!( - epoch_settings.epoch, + mithril_network_configuration.epoch, service.epoch_of_current_data().unwrap() ); assert_eq!( - epoch_settings.registration_protocol_parameters, + mithril_network_configuration.signer_registration_protocol_parameters, *service.registration_protocol_parameters().unwrap() ); assert!( @@ -695,7 +722,7 @@ mod tests { // Check cardano_transactions_signing_config assert_eq!( - epoch_settings.cardano_transactions_signing_config, + mithril_network_configuration.get_cardano_transactions_signing_config(), *service.cardano_transactions_signing_config().unwrap() ); } @@ -739,14 +766,16 @@ mod tests { let protocol_initializer_store = Arc::new(ProtocolInitializerRepository::new(connection, None)); - // Epoch settings - let epoch_settings = SignerEpochSettings { + //MithrilNetworkConfiguration + let mithril_network_configuration = MithrilNetworkConfiguration { epoch, - current_signers: signers[2..5].to_vec(), - next_signers: signers[3..7].to_vec(), - ..SignerEpochSettings::dummy().clone() + available_signed_entity_types: BTreeSet::new(), + ..MithrilNetworkConfiguration::dummy().clone() }; + let current_signers = signers[2..5].to_vec(); + let next_signers = signers[3..7].to_vec(); + // Build service and register epoch settings let mut service = MithrilEpochService::new( stake_store, @@ -754,16 +783,20 @@ mod tests { TestLogger::stdout(), ); service - .inform_epoch_settings(epoch_settings.clone(), BTreeSet::new()) + .inform_epoch_settings( + mithril_network_configuration, + current_signers.clone(), + next_signers.clone(), + ) .await .unwrap(); // Check current signers with stake { - let current_signers = service.current_signers_with_stake().await.unwrap(); + let actual_current_signers = service.current_signers_with_stake().await.unwrap(); - assert_eq!(epoch_settings.current_signers.len(), current_signers.len()); - for signer in current_signers { + assert_eq!(current_signers.len(), actual_current_signers.len()); + for signer in actual_current_signers { let expected_stake = stake_distribution.get(&signer.party_id).unwrap(); assert_eq!(expected_stake, &signer.stake); } @@ -771,10 +804,10 @@ mod tests { // Check next signers with stake { - let next_signers = service.next_signers_with_stake().await.unwrap(); + let actual_next_signers = service.next_signers_with_stake().await.unwrap(); - assert_eq!(epoch_settings.next_signers.len(), next_signers.len()); - for signer in next_signers { + assert_eq!(next_signers.len(), actual_next_signers.len()); + for signer in actual_next_signers { let expected_stake = next_stake_distribution.get(&signer.party_id).unwrap(); assert_eq!(expected_stake, &signer.stake); } @@ -802,12 +835,15 @@ mod tests { protocol_initializer_store, TestLogger::stdout(), ); - let epoch_settings = SignerEpochSettings { + + let mithril_network_configuration = MithrilNetworkConfiguration { epoch, - ..SignerEpochSettings::dummy().clone() + available_signed_entity_types: BTreeSet::new(), + ..MithrilNetworkConfiguration::dummy().clone() }; + service - .inform_epoch_settings(epoch_settings, BTreeSet::new()) + .inform_epoch_settings(mithril_network_configuration, Vec::new(), Vec::new()) .await .unwrap(); @@ -841,18 +877,22 @@ mod tests { } // Fail after `inform_epoch_settings` if `cardano_transactions_signing_config` is not set { - let allowed_discriminants = - BTreeSet::from([SignedEntityTypeDiscriminants::CardanoImmutableFilesFull]); + // Signers + let signers = fake_data::signers(5); + let current_signers = signers[1..3].to_vec(); + let next_signers = signers[2..5].to_vec(); epoch_service .write() .await .inform_epoch_settings( - SignerEpochSettings { - cardano_transactions_signing_config: None, - ..SignerEpochSettings::dummy() + MithrilNetworkConfiguration { + available_signed_entity_types: BTreeSet::new(), + signed_entity_types_config: HashMap::new(), + ..MithrilNetworkConfiguration::dummy() }, - allowed_discriminants.clone(), + current_signers, + next_signers, ) .await .unwrap(); @@ -866,17 +906,31 @@ mod tests { { let allowed_discriminants = BTreeSet::from([SignedEntityTypeDiscriminants::CardanoImmutableFilesFull]); + + let mut signed_entity_types_config = HashMap::new(); + signed_entity_types_config.insert( + SignedEntityTypeDiscriminants::CardanoTransactions, + SignedEntityTypeConfiguration::CardanoTransactions( + CardanoTransactionsSigningConfig::dummy(), + ), + ); + + // Signers + let signers = fake_data::signers(5); + let current_signers = signers[1..3].to_vec(); + let next_signers = signers[2..5].to_vec(); + epoch_service .write() .await .inform_epoch_settings( - SignerEpochSettings { - cardano_transactions_signing_config: Some( - CardanoTransactionsSigningConfig::dummy(), - ), - ..SignerEpochSettings::dummy() + MithrilNetworkConfiguration { + available_signed_entity_types: allowed_discriminants.clone(), + signed_entity_types_config, + ..MithrilNetworkConfiguration::dummy() }, - allowed_discriminants.clone(), + current_signers, + next_signers, ) .await .unwrap(); diff --git a/mithril-signer/tests/test_extensions/certificate_handler.rs b/mithril-signer/tests/test_extensions/certificate_handler.rs index c24f1d4da54..9bdb4b85b84 100644 --- a/mithril-signer/tests/test_extensions/certificate_handler.rs +++ b/mithril-signer/tests/test_extensions/certificate_handler.rs @@ -231,6 +231,7 @@ mod tests { #[tokio::test] async fn retrieve_epoch_settings() { + //TODO split in two tests, to test only signers from retrieve_epoch_settings and CardanoTransactionsSigningConfig in a mithril_network_configuration_provider test ? let (chain_observer, fake_aggregator) = init().await; let fake_signers = fake_data::signers(3); let epoch = chain_observer.get_current_epoch().await.unwrap().unwrap(); @@ -298,6 +299,7 @@ mod tests { #[tokio::test] async fn retrieve_aggregator_features() { + //TODO aggregator features are now retrieved with mithril_network_configuration_provider, adapt or remove test and implementation ? let (_chain_observer, fake_aggregator) = init().await; { diff --git a/mithril-signer/tests/test_extensions/state_machine_tester.rs b/mithril-signer/tests/test_extensions/state_machine_tester.rs index 43dd22662b4..c8de9253996 100644 --- a/mithril-signer/tests/test_extensions/state_machine_tester.rs +++ b/mithril-signer/tests/test_extensions/state_machine_tester.rs @@ -1,11 +1,15 @@ #![allow(dead_code)] use anyhow::anyhow; use mithril_metric::{MetricCollector, MetricsServiceExporter}; +use mithril_protocol_config::{ + model::SignedEntityTypeConfiguration, + test::double::mithril_network_configuration_provider::FakeMithrilNetworkConfigurationProvider, +}; use prometheus_parse::Value; use slog::Drain; use slog_scope::debug; use std::{ - collections::{BTreeMap, BTreeSet}, + collections::{BTreeMap, BTreeSet, HashMap}, fmt::Debug, ops::RangeInclusive, path::Path, @@ -39,7 +43,7 @@ use mithril_common::{ MithrilSignableBuilderService, MithrilStakeDistributionSignableBuilder, SignableBuilderServiceDependencies, }, - test::double::Dummy, + test::double::{Dummy, fake_data}, }; use mithril_era::{EraChecker, EraMarker, EraReader, adapters::EraReaderDummyAdapter}; use mithril_persistence::{ @@ -89,6 +93,7 @@ pub struct StateMachineTester { immutable_observer: Arc, chain_observer: Arc, certificate_handler: Arc, + network_configuration_service: Arc, protocol_initializer_store: Arc, stake_store: Arc, era_checker: Arc, @@ -169,6 +174,18 @@ impl StateMachineTester { }, ticker_service.clone(), )); + let network_configuration_service = Arc::new(FakeMithrilNetworkConfigurationProvider::new( + fake_data::protocol_parameters(), + SignedEntityTypeDiscriminants::all(), + HashMap::from([( + SignedEntityTypeDiscriminants::CardanoTransactions, + SignedEntityTypeConfiguration::CardanoTransactions( + cardano_transactions_signing_config.clone(), + ), + )]), + ticker_service.clone(), + )); + let digester = Arc::new(DumbImmutableDigester::default().with_digest("DIGEST")); let protocol_initializer_store = Arc::new(ProtocolInitializerRepository::new( sqlite_connection.clone(), @@ -313,6 +330,7 @@ impl StateMachineTester { epoch_service, certifier, kes_signer, + network_configuration_service: network_configuration_service.clone(), }; // set up stake distribution chain_observer.set_signers(signers_with_stake.to_owned()).await; @@ -332,6 +350,7 @@ impl StateMachineTester { immutable_observer, chain_observer, certificate_handler, + network_configuration_service, protocol_initializer_store, stake_store, era_checker, @@ -457,7 +476,7 @@ impl StateMachineTester { &mut self, discriminants: &[SignedEntityTypeDiscriminants], ) -> &mut Self { - self.certificate_handler + self.network_configuration_service .change_allowed_discriminants(&BTreeSet::from_iter(discriminants.iter().cloned())) .await; self