From 3c7ca2177b2b876cfa6b9dc89013b7c65e9278f2 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Tue, 28 Oct 2025 17:01:37 +0100 Subject: [PATCH 01/31] chore(aggregator-discovery): scaffold new crate --- .github/workflows/ci.yml | 2 +- Cargo.lock | 17 +++++++++ Cargo.toml | 1 + Makefile | 38 ++++++++++++++----- README.md | 7 +++- .../mithril-aggregator-discovery/Cargo.toml | 30 +++++++++++++++ .../mithril-aggregator-discovery/Makefile | 19 ++++++++++ .../mithril-aggregator-discovery/README.md | 3 ++ .../mithril-aggregator-discovery/src/lib.rs | 2 + 9 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 internal/mithril-aggregator-discovery/Cargo.toml create mode 100644 internal/mithril-aggregator-discovery/Makefile create mode 100644 internal/mithril-aggregator-discovery/README.md create mode 100644 internal/mithril-aggregator-discovery/src/lib.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5fcb1f14210..c6efd987de8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -843,7 +843,7 @@ jobs: # the same name (we only want to document those anyway) cargo doc --no-deps --lib -p mithril-stm -p mithril-common \ -p mithril-cardano-node-chain -p mithril-cardano-node-internal-database \ - -p mithril-aggregator-client -p mithril-build-script -p mithril-cli-helper \ + -p mithril-aggregator-client -p mithril-aggregator-discovery -p mithril-build-script -p mithril-cli-helper \ -p mithril-dmq -p mithril-doc -p mithril-doc-derive \ -p mithril-era -p mithril-metric -p mithril-persistence -p mithril-resource-pool \ -p mithril-ticker -p mithril-signed-entity-lock -p mithril-signed-entity-preloader \ diff --git a/Cargo.lock b/Cargo.lock index e41d565b5fa..cd501c2ce12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3801,6 +3801,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "mithril-aggregator-discovery" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "mithril-common", + "mockall", + "serde", + "slog", + "slog-async", + "slog-scope", + "slog-term", + "thiserror 2.0.17", + "tokio", +] + [[package]] name = "mithril-aggregator-fake" version = "0.4.15" diff --git a/Cargo.toml b/Cargo.toml index 5f699538b64..6130d00239b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "internal/cardano-node/mithril-cardano-node-chain", "internal/cardano-node/mithril-cardano-node-internal-database", "internal/mithril-aggregator-client", + "internal/mithril-aggregator-discovery", "internal/mithril-build-script", "internal/mithril-cli-helper", "internal/mithril-dmq", diff --git a/Makefile b/Makefile index a379c8e96a7..8c12ce5464f 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,33 @@ -COMPONENTS = mithril-aggregator mithril-client mithril-client-cli mithril-client-wasm \ - mithril-common mithril-relay mithril-signer mithril-stm \ - internal/mithril-build-script internal/mithril-cli-helper internal/mithril-doc \ +COMPONENTS = demo/protocol-demo \ + internal/cardano-node/mithril-cardano-node-chain \ + internal/cardano-node/mithril-cardano-node-internal-database \ + internal/mithril-aggregator-client \ + internal/mithril-aggregator-discovery \ + internal/mithril-build-script \ + internal/mithril-cli-helper \ internal/mithril-dmq \ - internal/mithril-doc-derive internal/mithril-era internal/mithril-metric internal/mithril-persistence \ + internal/mithril-doc \ + 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 \ - internal/tests/mithril-api-spec internal/tests/mithril-test-http-server \ - demo/protocol-demo \ - mithril-test-lab/mithril-aggregator-fake mithril-test-lab/mithril-end-to-end + internal/mithril-resource-pool \ + internal/mithril-ticker \ + internal/signed-entity/mithril-signed-entity-lock \ + internal/signed-entity/mithril-signed-entity-preloader \ + internal/tests/mithril-api-spec \ + internal/tests/mithril-test-http-server \ + mithril-aggregator \ + mithril-client \ + mithril-client-cli \ + mithril-client-wasm \ + mithril-common \ + mithril-relay \ + mithril-signer \ + mithril-stm \ + mithril-test-lab/mithril-aggregator-fake \ + mithril-test-lab/mithril-end-to-end GOALS := $(or $(MAKECMDGOALS),all) NON_COMPONENT_GOALS := check-format format diff --git a/README.md b/README.md index 283cc97477f..5d0e9b5f950 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,10 @@ This repository consists of the following parts: - [**Mithril signer**](./mithril-signer): the node of the **Mithril network** responsible for producing individual signatures that are collected and aggregated by the **Mithril aggregator**. - [**Internal**](./internal): the shared tools and API used by **Mithril** crates. - - [**Mithril aggregator client**](./internal/mithril-aggregator-client): a client to request data from a Mithril Aggregator, used by **Mithril network** nodes and client library. + + - [**Mithril aggregator client**](./internal/mithril-aggregator-client): a client to request data from a Mithril aggregator, used by **Mithril network** nodes and client library. + + - [**Mithril aggregator discovery**](./internal/mithril-aggregator-discovery): mechanisms to discover available Mithril aggregator, used by **Mithril network** nodes and client library. - [**Mithril build script**](./internal/mithril-build-script): a toolbox for Mithril crates that uses a build script phase. @@ -113,11 +116,13 @@ This repository consists of the following parts: - [**Mithril signed entity prealoader**](./internal/signed-entity/mithril-signed-entity-preloader): a **preload** mechanism for the Cardano transaction signed entity, used by **Mithril network** nodes. - [**tests**](./internal/tests): shared testing tools used by **Mithril** crates. + - [**Mithril api spec**](./internal/tests/mithril-api-spec): toolset to verify conformity of http routes against an Open Api specification, used by **Mithril network** nodes. - [**Mithril test http server**](internal/tests/mithril-test-http-server): provides a test http server, used by **Mithril network** nodes. - [**Mithril test lab**](./mithril-test-lab): the suite of tools that allow us to test and stress the **Mithril** protocol implementations. + - [**Mithril devnet**](./mithril-test-lab/mithril-devnet): the private **Mithril/Cardano network** used to scaffold a **Mithril network** on top of a **Cardano network**. - [**Mithril end to end**](./mithril-test-lab/mithril-end-to-end): the tool used to run test scenarios against a **Mithril devnet**. diff --git a/internal/mithril-aggregator-discovery/Cargo.toml b/internal/mithril-aggregator-discovery/Cargo.toml new file mode 100644 index 00000000000..b44f7032571 --- /dev/null +++ b/internal/mithril-aggregator-discovery/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "mithril-aggregator-discovery" +description = "Mechanisms to discover aggregator available in a Mithril network." +version = "0.1.0" +authors.workspace = true +documentation.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true +include = ["**/*.rs", "Cargo.toml", "README.md", ".gitignore"] + +[lib] +crate-type = ["lib", "cdylib", "staticlib"] + +[dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } +mithril-common = { path = "../../mithril-common" } +serde = { workspace = true } +slog = { workspace = true } +slog-scope = "4.4.0" +thiserror = { workspace = true } +tokio = { workspace = true, features = ["sync"] } + +[dev-dependencies] +mockall = { workspace = true } +slog-async = { workspace = true } +slog-term = { workspace = true } +tokio = { workspace = true, features = ["macros"] } diff --git a/internal/mithril-aggregator-discovery/Makefile b/internal/mithril-aggregator-discovery/Makefile new file mode 100644 index 00000000000..e503264d97d --- /dev/null +++ b/internal/mithril-aggregator-discovery/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 \ No newline at end of file diff --git a/internal/mithril-aggregator-discovery/README.md b/internal/mithril-aggregator-discovery/README.md new file mode 100644 index 00000000000..56a780b6020 --- /dev/null +++ b/internal/mithril-aggregator-discovery/README.md @@ -0,0 +1,3 @@ +# Mithril-aggregator-discovery + +This crate provides mechanisms to discover aggregators in a Mithril network. diff --git a/internal/mithril-aggregator-discovery/src/lib.rs b/internal/mithril-aggregator-discovery/src/lib.rs new file mode 100644 index 00000000000..df8b84b112f --- /dev/null +++ b/internal/mithril-aggregator-discovery/src/lib.rs @@ -0,0 +1,2 @@ +#![warn(missing_docs)] +//! This crate provides mechanisms to discover aggregators in a Mithril network. From ca036314e4dab65f5795540ad18b722d5a393a51 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Wed, 29 Oct 2025 18:18:30 +0100 Subject: [PATCH 02/31] feat(aggregator-discovery): add models for aggregator endpoint --- .../mithril-aggregator-discovery/src/lib.rs | 2 + .../mithril-aggregator-discovery/src/model.rs | 42 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 internal/mithril-aggregator-discovery/src/model.rs diff --git a/internal/mithril-aggregator-discovery/src/lib.rs b/internal/mithril-aggregator-discovery/src/lib.rs index df8b84b112f..a31d78e83fc 100644 --- a/internal/mithril-aggregator-discovery/src/lib.rs +++ b/internal/mithril-aggregator-discovery/src/lib.rs @@ -1,2 +1,4 @@ #![warn(missing_docs)] //! This crate provides mechanisms to discover aggregators in a Mithril network. +mod model; +pub use model::{AggregatorEndpoint, MithrilNetwork}; diff --git a/internal/mithril-aggregator-discovery/src/model.rs b/internal/mithril-aggregator-discovery/src/model.rs new file mode 100644 index 00000000000..4628b5f345b --- /dev/null +++ b/internal/mithril-aggregator-discovery/src/model.rs @@ -0,0 +1,42 @@ +use mithril_common::StdResult; + +/// Representation of a Mithril network +// TODO: to move to mithril common +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MithrilNetwork(String); + +impl MithrilNetwork { + /// Create a new MithrilNetwork instance + pub fn new(name: String) -> Self { + Self(name) + } + + /// Create a dummy MithrilNetwork instance for testing purposes + pub fn dummy() -> Self { + Self("dummy".to_string()) + } +} + +/// Representation of an aggregator endpoint +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AggregatorEndpoint { + url: String, +} + +impl AggregatorEndpoint { + /// Create a new AggregatorEndpoint instance + pub fn new(url: String) -> Self { + Self { url } + } + + /// Retrieve the capabilities of the aggregator + pub fn capabilities(&self) -> StdResult<()> { + todo!("Implement capabilities retrieval") + } +} + +impl From for String { + fn from(endpoint: AggregatorEndpoint) -> Self { + endpoint.url + } +} From 08f508e7ffc06beb4b86d8799058bfaddcb156ac Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Wed, 29 Oct 2025 18:20:15 +0100 Subject: [PATCH 03/31] feat(aggregator-discovery): introduce 'AggregatorDiscoverer' trait --- .../src/interface.rs | 18 ++++++++++++++++++ .../mithril-aggregator-discovery/src/lib.rs | 4 ++++ 2 files changed, 22 insertions(+) create mode 100644 internal/mithril-aggregator-discovery/src/interface.rs diff --git a/internal/mithril-aggregator-discovery/src/interface.rs b/internal/mithril-aggregator-discovery/src/interface.rs new file mode 100644 index 00000000000..26cfa20f4a4 --- /dev/null +++ b/internal/mithril-aggregator-discovery/src/interface.rs @@ -0,0 +1,18 @@ +//! Interface definition for Mithril Protocol Configuration provider. + +use mithril_common::StdResult; + +use crate::model::{AggregatorEndpoint, MithrilNetwork}; + +/// An aggregator discoverer. +#[cfg_attr(test, mockall::automock)] +#[async_trait::async_trait] +pub trait AggregatorDiscoverer: Sync + Send { + /// Get an iterator over a list of available aggregators in a Mithril network. + /// + /// Note: there is no guarantee that the returned aggregators is sorted, complete or up-to-date. + async fn get_available_aggregators( + &self, + network: MithrilNetwork, + ) -> StdResult>; +} diff --git a/internal/mithril-aggregator-discovery/src/lib.rs b/internal/mithril-aggregator-discovery/src/lib.rs index a31d78e83fc..fa1b98d9a35 100644 --- a/internal/mithril-aggregator-discovery/src/lib.rs +++ b/internal/mithril-aggregator-discovery/src/lib.rs @@ -1,4 +1,8 @@ #![warn(missing_docs)] //! This crate provides mechanisms to discover aggregators in a Mithril network. + +mod interface; mod model; + +pub use interface::AggregatorDiscoverer; pub use model::{AggregatorEndpoint, MithrilNetwork}; From fc55ee67b62ee6c9d2438551fd3aa4d745fbfc54 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Wed, 29 Oct 2025 18:20:37 +0100 Subject: [PATCH 04/31] feat(aggregator-discovery): create test double for 'AggregatorDiscoverer' trait --- .../mithril-aggregator-discovery/src/lib.rs | 1 + .../src/test/double/discoverer.rs | 76 +++++++++++++++++++ .../src/test/double/mod.rs | 7 ++ .../src/test/mod.rs | 7 ++ 4 files changed, 91 insertions(+) create mode 100644 internal/mithril-aggregator-discovery/src/test/double/discoverer.rs create mode 100644 internal/mithril-aggregator-discovery/src/test/double/mod.rs create mode 100644 internal/mithril-aggregator-discovery/src/test/mod.rs diff --git a/internal/mithril-aggregator-discovery/src/lib.rs b/internal/mithril-aggregator-discovery/src/lib.rs index fa1b98d9a35..9d24e399ae1 100644 --- a/internal/mithril-aggregator-discovery/src/lib.rs +++ b/internal/mithril-aggregator-discovery/src/lib.rs @@ -3,6 +3,7 @@ mod interface; mod model; +pub mod test; pub use interface::AggregatorDiscoverer; pub use model::{AggregatorEndpoint, MithrilNetwork}; diff --git a/internal/mithril-aggregator-discovery/src/test/double/discoverer.rs b/internal/mithril-aggregator-discovery/src/test/double/discoverer.rs new file mode 100644 index 00000000000..5a334638bc9 --- /dev/null +++ b/internal/mithril-aggregator-discovery/src/test/double/discoverer.rs @@ -0,0 +1,76 @@ +use std::collections::VecDeque; + +use tokio::sync::Mutex; + +use mithril_common::StdResult; + +use crate::{AggregatorDiscoverer, AggregatorEndpoint, MithrilNetwork}; + +type AggregatorListReturn = StdResult>; + +/// A fake implementation of the [AggregatorDiscoverer] trait for testing purposes. +pub struct AggregatorDiscovererFake { + results: Mutex>, +} + +impl AggregatorDiscovererFake { + /// Creates a new `AggregatorDiscovererFake` instance with the provided results. + pub fn new(results: Vec) -> Self { + Self { + results: Mutex::new(VecDeque::from(results)), + } + } +} + +#[async_trait::async_trait] +impl AggregatorDiscoverer for AggregatorDiscovererFake { + async fn get_available_aggregators( + &self, + _network: MithrilNetwork, + ) -> StdResult> { + let mut results = self.results.lock().await; + + let endpoints = results.pop_front().ok_or_else(|| { + anyhow::anyhow!("No more results available in AggregatorDiscovererFake") + })??; + + Ok(endpoints) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[tokio::test] + async fn get_available_aggregators_success() { + let consumer = AggregatorDiscovererFake::new(vec![ + Ok(vec![AggregatorEndpoint::new("test-1".to_string())]), + Ok(vec![AggregatorEndpoint::new("test-2".to_string())]), + ]); + + let messages = consumer + .get_available_aggregators(MithrilNetwork::dummy()) + .await + .unwrap(); + + assert_eq!( + vec![AggregatorEndpoint::new("test-1".to_string())], + messages + ); + } + + #[tokio::test] + async fn consume_messages_failure() { + let consumer = AggregatorDiscovererFake::new(vec![ + Err(anyhow::anyhow!("Test error")), + Ok(vec![AggregatorEndpoint::new("test-2".to_string())]), + ]); + + consumer + .get_available_aggregators(MithrilNetwork::dummy()) + .await + .expect_err("AggregatorDiscovererFake should return an error"); + } +} diff --git a/internal/mithril-aggregator-discovery/src/test/double/mod.rs b/internal/mithril-aggregator-discovery/src/test/double/mod.rs new file mode 100644 index 00000000000..31b02b52c2d --- /dev/null +++ b/internal/mithril-aggregator-discovery/src/test/double/mod.rs @@ -0,0 +1,7 @@ +//! Test doubles +//! +//! Enable unit testing with controlled inputs and predictable behavior. + +mod discoverer; + +pub use discoverer::*; diff --git a/internal/mithril-aggregator-discovery/src/test/mod.rs b/internal/mithril-aggregator-discovery/src/test/mod.rs new file mode 100644 index 00000000000..4be2cc86b8b --- /dev/null +++ b/internal/mithril-aggregator-discovery/src/test/mod.rs @@ -0,0 +1,7 @@ +//! Test utilities. +//! +//! ⚠ Do not use in production code ⚠ +//! +//! This module provides in particular test doubles for the traits defined in this crate. + +pub mod double; From 8d45b0a8d971678ca94f83acdf37e2e565342dbb Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Thu, 30 Oct 2025 14:05:21 +0100 Subject: [PATCH 05/31] feat(aggregator-discovery): add 'HttpConfigAggregatorDiscoverer' Implementation for 'AggregatorDiscoverer' trait. --- Cargo.lock | 3 + .../mithril-aggregator-discovery/Cargo.toml | 9 + .../src/http_config.rs | 196 ++++++++++++++++++ .../mithril-aggregator-discovery/src/lib.rs | 3 + .../mithril-aggregator-discovery/src/model.rs | 5 + mithril-client/src/aggregator_discovery.rs | 0 6 files changed, 216 insertions(+) create mode 100644 internal/mithril-aggregator-discovery/src/http_config.rs create mode 100644 mithril-client/src/aggregator_discovery.rs diff --git a/Cargo.lock b/Cargo.lock index cd501c2ce12..440a33aae10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3807,9 +3807,12 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "httpmock", "mithril-common", "mockall", + "reqwest", "serde", + "serde_json", "slog", "slog-async", "slog-scope", diff --git a/internal/mithril-aggregator-discovery/Cargo.toml b/internal/mithril-aggregator-discovery/Cargo.toml index b44f7032571..29698509571 100644 --- a/internal/mithril-aggregator-discovery/Cargo.toml +++ b/internal/mithril-aggregator-discovery/Cargo.toml @@ -17,7 +17,15 @@ crate-type = ["lib", "cdylib", "staticlib"] anyhow = { workspace = true } async-trait = { workspace = true } mithril-common = { path = "../../mithril-common" } +reqwest = { workspace = true, features = [ + "default", + "gzip", + "zstd", + "deflate", + "brotli" +] } serde = { workspace = true } +serde_json = { workspace = true } slog = { workspace = true } slog-scope = "4.4.0" thiserror = { workspace = true } @@ -25,6 +33,7 @@ tokio = { workspace = true, features = ["sync"] } [dev-dependencies] mockall = { workspace = true } +httpmock = "0.8.1" slog-async = { workspace = true } slog-term = { workspace = true } tokio = { workspace = true, features = ["macros"] } diff --git a/internal/mithril-aggregator-discovery/src/http_config.rs b/internal/mithril-aggregator-discovery/src/http_config.rs new file mode 100644 index 00000000000..3d6893bbef5 --- /dev/null +++ b/internal/mithril-aggregator-discovery/src/http_config.rs @@ -0,0 +1,196 @@ +use std::collections::HashMap; + +use anyhow::Context; +use reqwest::Client; +use serde::{Deserialize, Serialize}; + +use mithril_common::StdResult; + +use crate::{AggregatorDiscoverer, AggregatorEndpoint, MithrilNetwork}; + +const DEFAULT_REMOTE_NETWORKS_CONFIG_URL: &str = + "https://raw.githubusercontent.com/input-output-hk/mithril/main/networks.json"; + +/// Representation of the networks configuration file. +#[derive(Debug, Clone, Serialize, Deserialize)] +struct NetworksConfigMessage { + #[serde(flatten)] + pub networks: HashMap, +} + +/// Representation of a network environment in the networks configuration file. +#[derive(Debug, Clone, Serialize, Deserialize)] +struct NetworkEnvironmentMessage { + #[serde(rename = "mithril-networks")] + pub mithril_networks: Vec>, +} + +/// Representation of a Mithril network in the networks configuration file. +#[derive(Debug, Clone, Serialize, Deserialize)] +struct MithrilNetworkMessage { + pub aggregators: Vec, +} + +/// Representation of an aggregator in the networks configuration file. +#[derive(Debug, Clone, Serialize, Deserialize)] +struct AggregatorMessage { + pub url: String, +} + +/// An implementation of the [AggregatorDiscoverer] trait which discovers aggregators from remote networks configuration. +/// +/// The reference file is the `networks.json` file hosted in the Mithril GitHub repository. +pub struct HttpConfigAggregatorDiscoverer { + configuration_file_url: String, +} + +impl HttpConfigAggregatorDiscoverer { + /// Creates a new `HttpConfigAggregatorDiscoverer` instance with the provided results. + pub fn new(configuration_file_url: &str) -> Self { + Self { + configuration_file_url: configuration_file_url.to_string(), + } + } + + /// Builds a reqwest HTTP client. + fn build_client(&self) -> StdResult { + let client_builder = Client::builder(); + let client = client_builder.build()?; + + Ok(client) + } +} + +impl Default for HttpConfigAggregatorDiscoverer { + fn default() -> Self { + Self::new(DEFAULT_REMOTE_NETWORKS_CONFIG_URL) + } +} + +#[async_trait::async_trait] +impl AggregatorDiscoverer for HttpConfigAggregatorDiscoverer { + async fn get_available_aggregators( + &self, + network: MithrilNetwork, + ) -> StdResult> { + let client = self.build_client()?; + let networks_configuration_response = client + .get(&self.configuration_file_url) + .send() + .await + .with_context(|| { + format!( + "AggregatorDiscovererHttpConfig failed retrieving configuration file from {}", + &self.configuration_file_url + ) + })? + .json::() + .await + .with_context(|| { + format!( + "AggregatorDiscovererHttpConfig failed parsing configuration file from {}", + &self.configuration_file_url + ) + })?; + + Ok(networks_configuration_response + .networks + .values() + .flat_map(|env| &env.mithril_networks) + .flat_map(|network_map| network_map.iter()) + .filter(|(name, _)| *name == network.name()) + .flat_map(|(_, network)| &network.aggregators) + .map(|aggregator_msg| AggregatorEndpoint::new(aggregator_msg.url.clone())) + .collect()) + } +} + +#[cfg(test)] +mod tests { + use httpmock::MockServer; + + use super::*; + + const TEST_NETWORKS_CONFIG_JSON_SUCCESS: &str = r#" + { + "devnet": { + "mithril-networks": [ + { + "release-devnet": { + "aggregators": [ + { "url": "https://release-devnet-aggregator1" }, + { "url": "https://release-devnet-aggregator2" } + ] + } + } + ] + }, + "testnet": { + "mithril-networks": [ + { + "preview-testnet": { + "aggregators": [ + { "url": "https://preview-testnet-aggregator1" }, + { "url": "https://preview-testnet-aggregator2" } + ] + } + } + ] + } + }"#; + + const TEST_NETWORKS_CONFIG_JSON_FAILURE: &str = r#" + { + {"} + }"#; + + fn create_server_and_discoverer(content: &str) -> (MockServer, HttpConfigAggregatorDiscoverer) { + let size = content.len() as u64; + let server = MockServer::start(); + server.mock(|when, then| { + when.method(httpmock::Method::GET).path("/networks.json"); + then.status(200) + .body(content) + .header(reqwest::header::CONTENT_LENGTH.as_str(), size.to_string()); + }); + let configuration_file_url = format!("{}{}", server.url("/"), "networks.json"); + let discoverer = HttpConfigAggregatorDiscoverer::new(&configuration_file_url); + + (server, discoverer) + } + + #[tokio::test] + async fn get_available_aggregators_success() { + let content = TEST_NETWORKS_CONFIG_JSON_SUCCESS; + let (_server, discoverer) = create_server_and_discoverer(content); + let aggregators = discoverer + .get_available_aggregators(MithrilNetwork::new("release-devnet".into())) + .await + .unwrap(); + + assert_eq!( + vec![ + AggregatorEndpoint::new("https://release-devnet-aggregator1".into()), + AggregatorEndpoint::new("https://release-devnet-aggregator2".into()), + ], + aggregators + ); + + let aggregators = discoverer + .get_available_aggregators(MithrilNetwork::new("unknown".into())) + .await + .unwrap(); + + assert!(aggregators.is_empty()); + } + + #[tokio::test] + async fn get_available_aggregators_failure() { + let content = TEST_NETWORKS_CONFIG_JSON_FAILURE; + let (_server, discoverer) = create_server_and_discoverer(content); + discoverer + .get_available_aggregators(MithrilNetwork::new("release-devnet".into())) + .await + .expect_err("The retrieval of the aggregators should fail"); + } +} diff --git a/internal/mithril-aggregator-discovery/src/lib.rs b/internal/mithril-aggregator-discovery/src/lib.rs index 9d24e399ae1..7d26882cfaf 100644 --- a/internal/mithril-aggregator-discovery/src/lib.rs +++ b/internal/mithril-aggregator-discovery/src/lib.rs @@ -1,9 +1,12 @@ #![warn(missing_docs)] //! This crate provides mechanisms to discover aggregators in a Mithril network. +mod http_config; mod interface; mod model; pub mod test; +pub use capabilities_discoverer::CapableAggregatorDiscoverer; +pub use http_config_discoverer::HttpConfigAggregatorDiscoverer; pub use interface::AggregatorDiscoverer; pub use model::{AggregatorEndpoint, MithrilNetwork}; diff --git a/internal/mithril-aggregator-discovery/src/model.rs b/internal/mithril-aggregator-discovery/src/model.rs index 4628b5f345b..6c2ed656cc6 100644 --- a/internal/mithril-aggregator-discovery/src/model.rs +++ b/internal/mithril-aggregator-discovery/src/model.rs @@ -15,6 +15,11 @@ impl MithrilNetwork { pub fn dummy() -> Self { Self("dummy".to_string()) } + + /// Retrieve the name of the Mithril network + pub fn name(&self) -> &str { + &self.0 + } } /// Representation of an aggregator endpoint diff --git a/mithril-client/src/aggregator_discovery.rs b/mithril-client/src/aggregator_discovery.rs new file mode 100644 index 00000000000..e69de29bb2d From ce1af80d9b4538d3e4ecb29e11aeaf1cfc725b31 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Mon, 10 Nov 2025 15:48:06 +0100 Subject: [PATCH 06/31] refactor(aggregator-discovery): 'AggregatorDiscoverer' returns an iterator --- ...tp_config.rs => http_config_discoverer.rs} | 24 +++++++++++-------- .../src/interface.rs | 2 +- .../mithril-aggregator-discovery/src/lib.rs | 2 +- .../src/test/double/discoverer.rs | 16 +++++++------ 4 files changed, 25 insertions(+), 19 deletions(-) rename internal/mithril-aggregator-discovery/src/{http_config.rs => http_config_discoverer.rs} (92%) diff --git a/internal/mithril-aggregator-discovery/src/http_config.rs b/internal/mithril-aggregator-discovery/src/http_config_discoverer.rs similarity index 92% rename from internal/mithril-aggregator-discovery/src/http_config.rs rename to internal/mithril-aggregator-discovery/src/http_config_discoverer.rs index 3d6893bbef5..d4d6ebb6c9f 100644 --- a/internal/mithril-aggregator-discovery/src/http_config.rs +++ b/internal/mithril-aggregator-discovery/src/http_config_discoverer.rs @@ -72,7 +72,7 @@ impl AggregatorDiscoverer for HttpConfigAggregatorDiscoverer { async fn get_available_aggregators( &self, network: MithrilNetwork, - ) -> StdResult> { + ) -> StdResult>> { let client = self.build_client()?; let networks_configuration_response = client .get(&self.configuration_file_url) @@ -92,8 +92,7 @@ impl AggregatorDiscoverer for HttpConfigAggregatorDiscoverer { &self.configuration_file_url ) })?; - - Ok(networks_configuration_response + let aggregator_endpoints = networks_configuration_response .networks .values() .flat_map(|env| &env.mithril_networks) @@ -101,7 +100,9 @@ impl AggregatorDiscoverer for HttpConfigAggregatorDiscoverer { .filter(|(name, _)| *name == network.name()) .flat_map(|(_, network)| &network.aggregators) .map(|aggregator_msg| AggregatorEndpoint::new(aggregator_msg.url.clone())) - .collect()) + .collect::>(); + + Ok(Box::new(aggregator_endpoints.into_iter())) } } @@ -173,24 +174,27 @@ mod tests { AggregatorEndpoint::new("https://release-devnet-aggregator1".into()), AggregatorEndpoint::new("https://release-devnet-aggregator2".into()), ], - aggregators + aggregators.collect::>() ); - let aggregators = discoverer + let mut aggregators = discoverer .get_available_aggregators(MithrilNetwork::new("unknown".into())) .await .unwrap(); - assert!(aggregators.is_empty()); + assert!(aggregators.next().is_none()); } #[tokio::test] async fn get_available_aggregators_failure() { let content = TEST_NETWORKS_CONFIG_JSON_FAILURE; let (_server, discoverer) = create_server_and_discoverer(content); - discoverer + let result = discoverer .get_available_aggregators(MithrilNetwork::new("release-devnet".into())) - .await - .expect_err("The retrieval of the aggregators should fail"); + .await; + assert!( + result.is_err(), + "The retrieval of the aggregators should fail" + ); } } diff --git a/internal/mithril-aggregator-discovery/src/interface.rs b/internal/mithril-aggregator-discovery/src/interface.rs index 26cfa20f4a4..61b725a12f1 100644 --- a/internal/mithril-aggregator-discovery/src/interface.rs +++ b/internal/mithril-aggregator-discovery/src/interface.rs @@ -14,5 +14,5 @@ pub trait AggregatorDiscoverer: Sync + Send { async fn get_available_aggregators( &self, network: MithrilNetwork, - ) -> StdResult>; + ) -> StdResult>>; } diff --git a/internal/mithril-aggregator-discovery/src/lib.rs b/internal/mithril-aggregator-discovery/src/lib.rs index 7d26882cfaf..d4870f4d9ef 100644 --- a/internal/mithril-aggregator-discovery/src/lib.rs +++ b/internal/mithril-aggregator-discovery/src/lib.rs @@ -1,7 +1,7 @@ #![warn(missing_docs)] //! This crate provides mechanisms to discover aggregators in a Mithril network. -mod http_config; +mod http_config_discoverer; mod interface; mod model; pub mod test; diff --git a/internal/mithril-aggregator-discovery/src/test/double/discoverer.rs b/internal/mithril-aggregator-discovery/src/test/double/discoverer.rs index 5a334638bc9..808fb441174 100644 --- a/internal/mithril-aggregator-discovery/src/test/double/discoverer.rs +++ b/internal/mithril-aggregator-discovery/src/test/double/discoverer.rs @@ -27,14 +27,14 @@ impl AggregatorDiscoverer for AggregatorDiscovererFake { async fn get_available_aggregators( &self, _network: MithrilNetwork, - ) -> StdResult> { + ) -> StdResult>> { let mut results = self.results.lock().await; let endpoints = results.pop_front().ok_or_else(|| { anyhow::anyhow!("No more results available in AggregatorDiscovererFake") })??; - Ok(endpoints) + Ok(Box::new(endpoints.into_iter())) } } @@ -57,7 +57,7 @@ mod tests { assert_eq!( vec![AggregatorEndpoint::new("test-1".to_string())], - messages + messages.collect::>() ); } @@ -68,9 +68,11 @@ mod tests { Ok(vec![AggregatorEndpoint::new("test-2".to_string())]), ]); - consumer - .get_available_aggregators(MithrilNetwork::dummy()) - .await - .expect_err("AggregatorDiscovererFake should return an error"); + let result = consumer.get_available_aggregators(MithrilNetwork::dummy()).await; + + assert!( + result.is_err(), + "AggregatorDiscovererFake should return an error" + ); } } From 8af317fce781fbc013b130256fff25837f5fb04d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Mon, 10 Nov 2025 16:16:51 +0100 Subject: [PATCH 07/31] feat(aggregator-discovery): add 'ShuffleAggregatorDiscoverer' decorator --- Cargo.lock | 1 + .../mithril-aggregator-discovery/Cargo.toml | 4 + .../mithril-aggregator-discovery/src/lib.rs | 4 + .../src/rand_discoverer.rs | 77 +++++++++++++++++++ 4 files changed, 86 insertions(+) create mode 100644 internal/mithril-aggregator-discovery/src/rand_discoverer.rs diff --git a/Cargo.lock b/Cargo.lock index 440a33aae10..79e6b426d50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3810,6 +3810,7 @@ dependencies = [ "httpmock", "mithril-common", "mockall", + "rand 0.9.2", "reqwest", "serde", "serde_json", diff --git a/internal/mithril-aggregator-discovery/Cargo.toml b/internal/mithril-aggregator-discovery/Cargo.toml index 29698509571..01fba13dde8 100644 --- a/internal/mithril-aggregator-discovery/Cargo.toml +++ b/internal/mithril-aggregator-discovery/Cargo.toml @@ -13,10 +13,14 @@ include = ["**/*.rs", "Cargo.toml", "README.md", ".gitignore"] [lib] crate-type = ["lib", "cdylib", "staticlib"] +[features] +rand = ["dep:rand"] + [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } mithril-common = { path = "../../mithril-common" } +rand = { version = "0.9.2", optional = true} reqwest = { workspace = true, features = [ "default", "gzip", diff --git a/internal/mithril-aggregator-discovery/src/lib.rs b/internal/mithril-aggregator-discovery/src/lib.rs index d4870f4d9ef..39e34a47ce0 100644 --- a/internal/mithril-aggregator-discovery/src/lib.rs +++ b/internal/mithril-aggregator-discovery/src/lib.rs @@ -4,9 +4,13 @@ mod http_config_discoverer; mod interface; mod model; +#[cfg(feature = "rand")] +mod rand_discoverer; pub mod test; pub use capabilities_discoverer::CapableAggregatorDiscoverer; pub use http_config_discoverer::HttpConfigAggregatorDiscoverer; pub use interface::AggregatorDiscoverer; pub use model::{AggregatorEndpoint, MithrilNetwork}; +#[cfg(feature = "rand")] +pub use rand_discoverer::ShuffleAggregatorDiscoverer; diff --git a/internal/mithril-aggregator-discovery/src/rand_discoverer.rs b/internal/mithril-aggregator-discovery/src/rand_discoverer.rs new file mode 100644 index 00000000000..a967ae8b127 --- /dev/null +++ b/internal/mithril-aggregator-discovery/src/rand_discoverer.rs @@ -0,0 +1,77 @@ +use std::sync::Arc; + +use rand::{Rng, seq::SliceRandom}; +use tokio::sync::Mutex; + +use mithril_common::StdResult; + +use crate::{AggregatorDiscoverer, AggregatorEndpoint, MithrilNetwork}; + +/// A discoverer that returns a random set of aggregators +pub struct ShuffleAggregatorDiscoverer { + random_generator: Arc>, + inner_discoverer: Arc, +} + +impl ShuffleAggregatorDiscoverer { + /// Creates a new `ShuffleAggregatorDiscoverer` instance with the provided inner discoverer. + pub fn new(inner_discoverer: Arc, random_generator: R) -> Self { + Self { + inner_discoverer, + random_generator: Arc::new(Mutex::new(random_generator)), + } + } +} + +#[async_trait::async_trait] +impl AggregatorDiscoverer for ShuffleAggregatorDiscoverer { + async fn get_available_aggregators( + &self, + network: MithrilNetwork, + ) -> StdResult>> { + let mut aggregators: Vec = self + .inner_discoverer + .get_available_aggregators(network) + .await? + .collect(); + let mut rng = self.random_generator.lock().await; + aggregators.shuffle(&mut *rng); + + Ok(Box::new(aggregators.into_iter())) + } +} + +#[cfg(test)] +mod tests { + use rand::{SeedableRng, rngs::StdRng}; + + use crate::test::double::AggregatorDiscovererFake; + + use super::*; + + #[tokio::test] + async fn shuffle_aggregator_discoverer() { + let inner_discoverer = AggregatorDiscovererFake::new(vec![Ok(vec![ + AggregatorEndpoint::new("https://release-devnet-aggregator1".to_string()), + AggregatorEndpoint::new("https://release-devnet-aggregator2".to_string()), + AggregatorEndpoint::new("https://release-devnet-aggregator3".to_string()), + ])]); + let seed = [0u8; 32]; + let rng = StdRng::from_seed(seed); + let discoverer = ShuffleAggregatorDiscoverer::new(Arc::new(inner_discoverer), rng); + + let aggregators = discoverer + .get_available_aggregators(MithrilNetwork::new("release-devnet".into())) + .await + .unwrap(); + + assert_eq!( + vec![ + AggregatorEndpoint::new("https://release-devnet-aggregator3".into()), + AggregatorEndpoint::new("https://release-devnet-aggregator2".into()), + AggregatorEndpoint::new("https://release-devnet-aggregator1".into()), + ], + aggregators.collect::>() + ); + } +} From fb84bad3b863d47b954154727611c19a30361228 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Mon, 10 Nov 2025 17:50:48 +0100 Subject: [PATCH 08/31] wip(aggregator-discovery): add 'CapableAggregatorDiscoverer' decorator --- Cargo.lock | 1 + .../mithril-aggregator-discovery/Cargo.toml | 1 + .../src/capabilities_discoverer.rs | 156 ++++++++++++++++++ .../mithril-aggregator-discovery/src/lib.rs | 1 + .../mithril-aggregator-discovery/src/model.rs | 12 +- 5 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs diff --git a/Cargo.lock b/Cargo.lock index 79e6b426d50..2d68dc867ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3808,6 +3808,7 @@ dependencies = [ "anyhow", "async-trait", "httpmock", + "mithril-aggregator-client", "mithril-common", "mockall", "rand 0.9.2", diff --git a/internal/mithril-aggregator-discovery/Cargo.toml b/internal/mithril-aggregator-discovery/Cargo.toml index 01fba13dde8..85cdb9862e1 100644 --- a/internal/mithril-aggregator-discovery/Cargo.toml +++ b/internal/mithril-aggregator-discovery/Cargo.toml @@ -20,6 +20,7 @@ rand = ["dep:rand"] anyhow = { workspace = true } async-trait = { workspace = true } mithril-common = { path = "../../mithril-common" } +mithril-aggregator-client = { path = "../mithril-aggregator-client" } rand = { version = "0.9.2", optional = true} reqwest = { workspace = true, features = [ "default", diff --git a/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs b/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs new file mode 100644 index 00000000000..8b72fd54d2a --- /dev/null +++ b/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs @@ -0,0 +1,156 @@ +use std::sync::Arc; + +use mithril_common::{StdResult, messages::AggregatorCapabilities}; + +use crate::{AggregatorDiscoverer, AggregatorEndpoint, MithrilNetwork}; + +/// An aggregator discoverer for specific capabilities. +pub struct CapableAggregatorDiscoverer { + capabilities: AggregatorCapabilities, + inner_discoverer: Arc, +} + +impl CapableAggregatorDiscoverer { + /// Creates a new `CapableAggregatorDiscoverer` instance with the provided capabilities. + pub fn new( + capabilities: AggregatorCapabilities, + inner_discoverer: Arc, + ) -> Self { + Self { + capabilities, + inner_discoverer, + } + } + + /// Check if the available capabilities match the required capabilities. + /// + /// Returns true if: + /// - The aggregate signature types are the same. + /// - All required signed entity types are included in the available signed entity types. + fn capabilities_match( + required: &AggregatorCapabilities, + available: &AggregatorCapabilities, + ) -> bool { + if available.aggregate_signature_type != required.aggregate_signature_type { + return false; + } + + let available_signed_entity_types = &available.signed_entity_types; + let required_signed_entity_types = &required.signed_entity_types; + + required_signed_entity_types + .iter() + .all(|req| available_signed_entity_types.contains(req)) + } +} + +/// An iterator over aggregator endpoints filtered by capabilities. +struct CapableAggregatorDiscovererIterator { + capabilities: AggregatorCapabilities, + inner_iterator: Box>, +} + +impl Iterator for CapableAggregatorDiscovererIterator { + type Item = AggregatorEndpoint; + + fn next(&mut self) -> Option { + for aggregator_endpoint in &mut self.inner_iterator { + let aggregator_capabilities = tokio::runtime::Handle::current() + .block_on(async { aggregator_endpoint.retrieve_capabilities().await }); + if CapableAggregatorDiscoverer::capabilities_match( + &self.capabilities, + &aggregator_capabilities.ok()?, + ) { + return Some(aggregator_endpoint); + } + } + + None + } +} + +#[async_trait::async_trait] +impl AggregatorDiscoverer for CapableAggregatorDiscoverer { + async fn get_available_aggregators( + &self, + network: MithrilNetwork, + ) -> StdResult>> { + let aggregator_endpoints = self.inner_discoverer.get_available_aggregators(network).await?; + + Ok(Box::new(CapableAggregatorDiscovererIterator { + capabilities: self.capabilities.clone(), + inner_iterator: aggregator_endpoints, + })) + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use mithril_common::{ + AggregateSignatureType, + entities::SignedEntityTypeDiscriminants::{ + CardanoDatabase, CardanoStakeDistribution, CardanoTransactions, + }, + }; + + use super::*; + + #[test] + fn capabilities_match_success() { + let required = AggregatorCapabilities { + aggregate_signature_type: AggregateSignatureType::Concatenation, + signed_entity_types: BTreeSet::from([CardanoTransactions, CardanoStakeDistribution]), + cardano_transactions_prover: None, + }; + + let available = AggregatorCapabilities { + aggregate_signature_type: AggregateSignatureType::Concatenation, + signed_entity_types: BTreeSet::from([ + CardanoTransactions, + CardanoStakeDistribution, + CardanoDatabase, + ]), + cardano_transactions_prover: None, + }; + + assert!(CapableAggregatorDiscoverer::capabilities_match( + &required, &available + )); + } + + #[test] + fn capabilities_match_failure() { + let required = AggregatorCapabilities { + aggregate_signature_type: AggregateSignatureType::Concatenation, + signed_entity_types: BTreeSet::from([CardanoTransactions, CardanoStakeDistribution]), + cardano_transactions_prover: None, + }; + + let available = AggregatorCapabilities { + aggregate_signature_type: AggregateSignatureType::Concatenation, + signed_entity_types: BTreeSet::from([CardanoTransactions]), + cardano_transactions_prover: None, + }; + + assert!(!CapableAggregatorDiscoverer::capabilities_match( + &required, &available + )); + } + + #[tokio::test] + async fn get_available_aggregators_success() { + todo!() + } + + #[tokio::test] + async fn get_available_aggregators_success_when_one_aggregator_capabilities_does_not_match() { + todo!() + } + + #[tokio::test] + async fn get_available_aggregators_success_when_one_aggregator_retruns_an_error() { + todo!() + } +} diff --git a/internal/mithril-aggregator-discovery/src/lib.rs b/internal/mithril-aggregator-discovery/src/lib.rs index 39e34a47ce0..fcce2204325 100644 --- a/internal/mithril-aggregator-discovery/src/lib.rs +++ b/internal/mithril-aggregator-discovery/src/lib.rs @@ -1,6 +1,7 @@ #![warn(missing_docs)] //! This crate provides mechanisms to discover aggregators in a Mithril network. +mod capabilities_discoverer; mod http_config_discoverer; mod interface; mod model; diff --git a/internal/mithril-aggregator-discovery/src/model.rs b/internal/mithril-aggregator-discovery/src/model.rs index 6c2ed656cc6..4fa0805baf1 100644 --- a/internal/mithril-aggregator-discovery/src/model.rs +++ b/internal/mithril-aggregator-discovery/src/model.rs @@ -1,4 +1,5 @@ -use mithril_common::StdResult; +use mithril_aggregator_client::{AggregatorHttpClient, query::GetAggregatorFeaturesQuery}; +use mithril_common::{StdResult, messages::AggregatorCapabilities}; /// Representation of a Mithril network // TODO: to move to mithril common @@ -35,8 +36,13 @@ impl AggregatorEndpoint { } /// Retrieve the capabilities of the aggregator - pub fn capabilities(&self) -> StdResult<()> { - todo!("Implement capabilities retrieval") + pub async fn retrieve_capabilities(&self) -> StdResult { + let aggregator_client = AggregatorHttpClient::builder(self.url.clone()).build()?; + + Ok(aggregator_client + .send(GetAggregatorFeaturesQuery::current()) + .await? + .capabilities) } } From d48ab7df540eec6983fd6058d93fce7a561da58f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Fri, 14 Nov 2025 12:42:25 +0100 Subject: [PATCH 09/31] fix(aggregator-discovery): 'CapableAggregatorDiscovererIterator' implementation of 'Iterator' trait --- .../mithril-aggregator-discovery/Cargo.toml | 2 +- .../src/capabilities_discoverer.rs | 22 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/internal/mithril-aggregator-discovery/Cargo.toml b/internal/mithril-aggregator-discovery/Cargo.toml index 85cdb9862e1..176c42249f9 100644 --- a/internal/mithril-aggregator-discovery/Cargo.toml +++ b/internal/mithril-aggregator-discovery/Cargo.toml @@ -34,7 +34,7 @@ serde_json = { workspace = true } slog = { workspace = true } slog-scope = "4.4.0" thiserror = { workspace = true } -tokio = { workspace = true, features = ["sync"] } +tokio = { workspace = true, features = ["sync", "rt-multi-thread"] } [dev-dependencies] mockall = { workspace = true } diff --git a/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs b/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs index 8b72fd54d2a..8d3253ef2bf 100644 --- a/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs +++ b/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs @@ -54,14 +54,20 @@ impl Iterator for CapableAggregatorDiscovererIterator { type Item = AggregatorEndpoint; fn next(&mut self) -> Option { - for aggregator_endpoint in &mut self.inner_iterator { - let aggregator_capabilities = tokio::runtime::Handle::current() - .block_on(async { aggregator_endpoint.retrieve_capabilities().await }); - if CapableAggregatorDiscoverer::capabilities_match( - &self.capabilities, - &aggregator_capabilities.ok()?, - ) { - return Some(aggregator_endpoint); + while let Some(aggregator_endpoint) = self.inner_iterator.next() { + let aggregator_endpoint_clone = aggregator_endpoint.clone(); + let aggregator_capabilities = tokio::task::block_in_place(move || { + tokio::runtime::Handle::current().block_on(async move { + aggregator_endpoint_clone.retrieve_capabilities().await + }) + }); + if let Ok(aggregator_capabilities) = aggregator_capabilities { + if CapableAggregatorDiscoverer::capabilities_match( + &self.capabilities, + &aggregator_capabilities, + ) { + return Some(aggregator_endpoint); + } } } From 26642f356c0df78113b9d6636431d82536cf3c50 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Fri, 14 Nov 2025 16:14:36 +0100 Subject: [PATCH 10/31] test(aggregator-discovery): add tests for 'CapableAggregatorDiscovererIterator' --- .../src/capabilities_discoverer.rs | 204 +++++++++++++++++- 1 file changed, 196 insertions(+), 8 deletions(-) diff --git a/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs b/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs index 8d3253ef2bf..538074c3124 100644 --- a/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs +++ b/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs @@ -94,15 +94,29 @@ impl AggregatorDiscoverer for CapableAggregatorDiscoverer { mod tests { use std::collections::BTreeSet; + use httpmock::MockServer; + use serde_json::json; + use mithril_common::{ AggregateSignatureType, entities::SignedEntityTypeDiscriminants::{ CardanoDatabase, CardanoStakeDistribution, CardanoTransactions, }, + messages::AggregatorFeaturesMessage, }; use super::*; + fn create_aggregator_features_message( + capabilities: AggregatorCapabilities, + ) -> AggregatorFeaturesMessage { + AggregatorFeaturesMessage { + open_api_version: "1.0.0".to_string(), + documentation_url: "https://docs".to_string(), + capabilities, + } + } + #[test] fn capabilities_match_success() { let required = AggregatorCapabilities { @@ -145,18 +159,192 @@ mod tests { )); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn get_available_aggregators_success() { - todo!() + let capabilities = AggregatorCapabilities { + aggregate_signature_type: AggregateSignatureType::Concatenation, + signed_entity_types: BTreeSet::from([CardanoStakeDistribution, CardanoTransactions]), + cardano_transactions_prover: None, + }; + let aggregator_server = MockServer::start(); + let aggregator_server_mock = aggregator_server.mock(|when, then| { + when.path("/"); + then.status(200) + .body(json!(create_aggregator_features_message(capabilities)).to_string()); + }); + let discoverer = CapableAggregatorDiscoverer::new( + AggregatorCapabilities { + aggregate_signature_type: AggregateSignatureType::Concatenation, + signed_entity_types: BTreeSet::from([CardanoTransactions]), + cardano_transactions_prover: None, + }, + Arc::new(crate::test::double::AggregatorDiscovererFake::new(vec![ + Ok(vec![AggregatorEndpoint::new(aggregator_server.url("/"))]), + ])), + ); + + let mut aggregators = discoverer + .get_available_aggregators(MithrilNetwork::new("release-devnet".into())) + .await + .unwrap(); + + let next_aggregator = aggregators.next(); + aggregator_server_mock.assert(); + assert_eq!( + Some(AggregatorEndpoint::new(aggregator_server.url("/"))), + next_aggregator + ); } - #[tokio::test] - async fn get_available_aggregators_success_when_one_aggregator_capabilities_does_not_match() { - todo!() + #[tokio::test(flavor = "multi_thread")] + async fn get_available_aggregators_succeeds_when_aggregator_capabilities_do_not_match() { + let capabilities = AggregatorCapabilities { + aggregate_signature_type: AggregateSignatureType::Concatenation, + signed_entity_types: BTreeSet::from([CardanoTransactions]), + cardano_transactions_prover: None, + }; + let aggregator_server = MockServer::start(); + let aggregator_server_mock = aggregator_server.mock(|when, then| { + when.path("/"); + then.status(200) + .body(json!(create_aggregator_features_message(capabilities)).to_string()); + }); + let discoverer = CapableAggregatorDiscoverer::new( + AggregatorCapabilities { + aggregate_signature_type: AggregateSignatureType::Concatenation, + signed_entity_types: BTreeSet::from([CardanoDatabase]), + cardano_transactions_prover: None, + }, + Arc::new(crate::test::double::AggregatorDiscovererFake::new(vec![ + Ok(vec![AggregatorEndpoint::new(aggregator_server.url("/"))]), + ])), + ); + + let mut aggregators = discoverer + .get_available_aggregators(MithrilNetwork::new("release-devnet".into())) + .await + .unwrap(); + + let next_aggregator = aggregators.next(); + aggregator_server_mock.assert(); + assert!(next_aggregator.is_none()); } - #[tokio::test] - async fn get_available_aggregators_success_when_one_aggregator_retruns_an_error() { - todo!() + #[tokio::test(flavor = "multi_thread")] + async fn get_available_aggregators_succeeds_when_one_aggregator_returns_an_error() { + let aggregator_server_1 = MockServer::start(); + let aggregator_server_mock_1 = aggregator_server_1.mock(|when, then| { + when.path("/"); + then.status(500); + }); + let capabilities_2 = AggregatorCapabilities { + aggregate_signature_type: AggregateSignatureType::Concatenation, + signed_entity_types: BTreeSet::from([CardanoStakeDistribution, CardanoDatabase]), + cardano_transactions_prover: None, + }; + let aggregator_server_2 = MockServer::start(); + let aggregator_server_mock_2 = aggregator_server_2.mock(|when, then| { + when.path("/"); + then.status(200) + .body(json!(create_aggregator_features_message(capabilities_2)).to_string()); + }); + let discoverer = CapableAggregatorDiscoverer::new( + AggregatorCapabilities { + aggregate_signature_type: AggregateSignatureType::Concatenation, + signed_entity_types: BTreeSet::from([CardanoDatabase]), + cardano_transactions_prover: None, + }, + Arc::new(crate::test::double::AggregatorDiscovererFake::new(vec![ + Ok(vec![ + AggregatorEndpoint::new(aggregator_server_1.url("/")), + AggregatorEndpoint::new(aggregator_server_2.url("/")), + ]), + ])), + ); + + let mut aggregators = discoverer + .get_available_aggregators(MithrilNetwork::new("release-devnet".into())) + .await + .unwrap(); + + let next_aggregator = aggregators.next(); + aggregator_server_mock_1.assert(); + aggregator_server_mock_2.assert(); + assert_eq!( + Some(AggregatorEndpoint::new(aggregator_server_2.url("/"))), + next_aggregator + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn get_available_aggregators_succeeds_and_makes_minimum_calls_to_aggregators() { + let aggregator_server_1 = MockServer::start(); + let aggregator_server_mock_1 = aggregator_server_1.mock(|when, then| { + when.path("/"); + then.status(500); + }); + let capabilities_2 = AggregatorCapabilities { + aggregate_signature_type: AggregateSignatureType::Concatenation, + signed_entity_types: BTreeSet::from([CardanoStakeDistribution]), + cardano_transactions_prover: None, + }; + let aggregator_server_2 = MockServer::start(); + let aggregator_server_mock_2 = aggregator_server_2.mock(|when, then| { + when.path("/"); + then.status(200) + .body(json!(create_aggregator_features_message(capabilities_2)).to_string()); + }); + let capabilities_3 = AggregatorCapabilities { + aggregate_signature_type: AggregateSignatureType::Concatenation, + signed_entity_types: BTreeSet::from([CardanoDatabase]), + cardano_transactions_prover: None, + }; + let aggregator_server_3 = MockServer::start(); + let aggregator_server_mock_3 = aggregator_server_3.mock(|when, then| { + when.path("/"); + then.status(200) + .body(json!(create_aggregator_features_message(capabilities_3)).to_string()); + }); + let capabilities_4 = AggregatorCapabilities { + aggregate_signature_type: AggregateSignatureType::Concatenation, + signed_entity_types: BTreeSet::from([CardanoDatabase]), + cardano_transactions_prover: None, + }; + let aggregator_server_4 = MockServer::start(); + let aggregator_server_mock_4 = aggregator_server_4.mock(|when, then| { + when.path("/"); + then.status(200) + .body(json!(create_aggregator_features_message(capabilities_4)).to_string()); + }); + let discoverer = CapableAggregatorDiscoverer::new( + AggregatorCapabilities { + aggregate_signature_type: AggregateSignatureType::Concatenation, + signed_entity_types: BTreeSet::from([CardanoDatabase]), + cardano_transactions_prover: None, + }, + Arc::new(crate::test::double::AggregatorDiscovererFake::new(vec![ + Ok(vec![ + AggregatorEndpoint::new(aggregator_server_1.url("/")), + AggregatorEndpoint::new(aggregator_server_2.url("/")), + AggregatorEndpoint::new(aggregator_server_3.url("/")), + AggregatorEndpoint::new(aggregator_server_4.url("/")), + ]), + ])), + ); + + let mut aggregators = discoverer + .get_available_aggregators(MithrilNetwork::new("release-devnet".into())) + .await + .unwrap(); + + let next_aggregator = aggregators.next(); + aggregator_server_mock_1.assert(); + aggregator_server_mock_2.assert(); + aggregator_server_mock_3.assert(); + assert_eq!(0, aggregator_server_mock_4.calls()); + assert_eq!( + Some(AggregatorEndpoint::new(aggregator_server_3.url("/"))), + next_aggregator + ); } } From 7f9e154abdd390a19826871aa5845126f680b9aa Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Fri, 14 Nov 2025 17:08:26 +0100 Subject: [PATCH 11/31] refactor(client): prepare client builder for aggregator discovery --- mithril-client/src/client.rs | 95 +++++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 34 deletions(-) diff --git a/mithril-client/src/client.rs b/mithril-client/src/client.rs index 83304c80dc9..898f8fb61ae 100644 --- a/mithril-client/src/client.rs +++ b/mithril-client/src/client.rs @@ -1,6 +1,7 @@ use anyhow::{Context, anyhow}; #[cfg(feature = "fs")] use chrono::Utc; +use mithril_common::messages::AggregatorCapabilities; use reqwest::Url; use serde::{Deserialize, Serialize}; use slog::{Logger, o}; @@ -40,6 +41,20 @@ const fn one_week_in_seconds() -> u32 { 604800 } +/// The type of discovery to use to find the aggregator to connect to. +pub enum AggregatorDiscoveryType { + /// Automatically discover the aggregator. + Automatic, + /// Use a specific URL to connect to the aggregator. + Url(String), +} + +/// The genesis verification key. +pub enum GenesisVerificationKey { + /// The verification key is provided as a JSON Hex-encoded string. + JsonHex(String), +} + /// Options that can be used to configure the client. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ClientOptions { @@ -153,8 +168,9 @@ impl Client { /// Builder than can be used to create a [Client] easily or with custom dependencies. pub struct ClientBuilder { - aggregator_endpoint: Option, - genesis_verification_key: String, + aggregator_discovery: AggregatorDiscoveryType, + aggregator_capabilities: Option, + genesis_verification_key: Option, origin_tag: Option, client_type: Option, #[cfg(feature = "fs")] @@ -175,35 +191,17 @@ impl ClientBuilder { /// Constructs a new `ClientBuilder` that fetches data from the aggregator at the given /// endpoint and with the given genesis verification key. pub fn aggregator(endpoint: &str, genesis_verification_key: &str) -> ClientBuilder { - Self { - aggregator_endpoint: Some(endpoint.to_string()), - genesis_verification_key: genesis_verification_key.to_string(), - origin_tag: None, - client_type: None, - #[cfg(feature = "fs")] - ancillary_verification_key: None, - aggregator_client: None, - certificate_verifier: None, - #[cfg(feature = "fs")] - http_file_downloader: None, - #[cfg(feature = "unstable")] - certificate_verifier_cache: None, - era_fetcher: None, - logger: None, - feedback_receivers: vec![], - options: ClientOptions::default(), - } + Self::new(AggregatorDiscoveryType::Url(endpoint.to_string())).with_genesis_verification_key( + GenesisVerificationKey::JsonHex(genesis_verification_key.to_string()), + ) } /// Constructs a new `ClientBuilder` without any dependency set. - /// - /// Use [ClientBuilder::aggregator] if you don't need to set a custom [AggregatorClient] - /// to request data from the aggregator. - #[deprecated(since = "0.12.33", note = "Will be removed in 0.13.0")] - pub fn new(genesis_verification_key: &str) -> ClientBuilder { + pub fn new(aggregator_discovery: AggregatorDiscoveryType) -> ClientBuilder { Self { - aggregator_endpoint: None, - genesis_verification_key: genesis_verification_key.to_string(), + aggregator_discovery, + aggregator_capabilities: None, + genesis_verification_key: None, origin_tag: None, client_type: None, #[cfg(feature = "fs")] @@ -221,6 +219,23 @@ impl ClientBuilder { } } + /// Sets the genesis verification key to use when verifying certificates. + pub fn with_genesis_verification_key( + mut self, + genesis_verification_key: GenesisVerificationKey, + ) -> ClientBuilder { + self.genesis_verification_key = Some(genesis_verification_key); + + self + } + + /// Sets the aggregator capabilities expected to be matched by the aggregator with which the client will interact. + pub fn with_capabilities(mut self, capabilities: AggregatorCapabilities) -> ClientBuilder { + self.aggregator_capabilities = Some(capabilities); + + self + } + /// Returns a `Client` that uses the dependencies provided to this `ClientBuilder`. /// /// The builder will try to create the missing dependencies using default implementations @@ -231,6 +246,16 @@ impl ClientBuilder { .clone() .unwrap_or_else(|| Logger::root(slog::Discard, o!())); + let genesis_verification_key = match self.genesis_verification_key { + Some(GenesisVerificationKey::JsonHex(ref key)) => key, + None => { + return Err(anyhow!( + "The genesis verification key must be provided to build the client with the 'with_genesis_verification_key' function" + ) + .into()); + } + }; + let feedback_sender = FeedbackSender::new(&self.feedback_receivers); let aggregator_client = match self.aggregator_client { @@ -249,7 +274,7 @@ impl ClientBuilder { None => Arc::new( MithrilCertificateVerifier::new( aggregator_client.clone(), - &self.genesis_verification_key, + genesis_verification_key, feedback_sender.clone(), #[cfg(feature = "unstable")] self.certificate_verifier_cache, @@ -341,12 +366,14 @@ impl ClientBuilder { &self, logger: Logger, ) -> Result { - let endpoint = self - .aggregator_endpoint.as_ref() - .ok_or(anyhow!("No aggregator endpoint set: \ - You must either provide an aggregator endpoint or your own AggregatorClient implementation"))?; - let endpoint_url = Url::parse(endpoint).with_context(|| { - format!("Invalid aggregator endpoint, it must be a correctly formed url: '{endpoint}'") + let aggregator_endpoint = match self.aggregator_discovery { + AggregatorDiscoveryType::Url(ref url) => url.clone(), + AggregatorDiscoveryType::Automatic => { + todo!("Implement automatic aggregator discovery") + } + }; + let endpoint_url = Url::parse(&aggregator_endpoint).with_context(|| { + format!("Invalid aggregator endpoint, it must be a correctly formed url: '{aggregator_endpoint}'") })?; let headers = self.compute_http_headers(); From 0cdef1cb869dc45b63e9a9392f2e5e0a53019a91 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Mon, 17 Nov 2025 14:57:53 +0100 Subject: [PATCH 12/31] fix(aggregator-discovery): remove unneeded 'crate-type' in 'Cargo.toml' --- internal/mithril-aggregator-discovery/Cargo.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/mithril-aggregator-discovery/Cargo.toml b/internal/mithril-aggregator-discovery/Cargo.toml index 176c42249f9..af02ec4613b 100644 --- a/internal/mithril-aggregator-discovery/Cargo.toml +++ b/internal/mithril-aggregator-discovery/Cargo.toml @@ -10,9 +10,6 @@ license.workspace = true repository.workspace = true include = ["**/*.rs", "Cargo.toml", "README.md", ".gitignore"] -[lib] -crate-type = ["lib", "cdylib", "staticlib"] - [features] rand = ["dep:rand"] From 938daf855871fcc491857e05129061a9523dd628 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Tue, 18 Nov 2025 10:23:55 +0100 Subject: [PATCH 13/31] refactor(aggregator-discovery): remove the 'rand' feature --- internal/mithril-aggregator-discovery/Cargo.toml | 5 +---- internal/mithril-aggregator-discovery/src/lib.rs | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/internal/mithril-aggregator-discovery/Cargo.toml b/internal/mithril-aggregator-discovery/Cargo.toml index af02ec4613b..f6106fc4f47 100644 --- a/internal/mithril-aggregator-discovery/Cargo.toml +++ b/internal/mithril-aggregator-discovery/Cargo.toml @@ -10,15 +10,12 @@ license.workspace = true repository.workspace = true include = ["**/*.rs", "Cargo.toml", "README.md", ".gitignore"] -[features] -rand = ["dep:rand"] - [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } mithril-common = { path = "../../mithril-common" } mithril-aggregator-client = { path = "../mithril-aggregator-client" } -rand = { version = "0.9.2", optional = true} +rand = { version = "0.9.2"} reqwest = { workspace = true, features = [ "default", "gzip", diff --git a/internal/mithril-aggregator-discovery/src/lib.rs b/internal/mithril-aggregator-discovery/src/lib.rs index fcce2204325..2eeeab5edab 100644 --- a/internal/mithril-aggregator-discovery/src/lib.rs +++ b/internal/mithril-aggregator-discovery/src/lib.rs @@ -5,7 +5,6 @@ mod capabilities_discoverer; mod http_config_discoverer; mod interface; mod model; -#[cfg(feature = "rand")] mod rand_discoverer; pub mod test; @@ -13,5 +12,4 @@ pub use capabilities_discoverer::CapableAggregatorDiscoverer; pub use http_config_discoverer::HttpConfigAggregatorDiscoverer; pub use interface::AggregatorDiscoverer; pub use model::{AggregatorEndpoint, MithrilNetwork}; -#[cfg(feature = "rand")] pub use rand_discoverer::ShuffleAggregatorDiscoverer; From a9a03582163d4aa7f562640ee1884a6df4cbe197 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Mon, 17 Nov 2025 15:48:23 +0100 Subject: [PATCH 14/31] feat(client): implement basic aggregator discoverer --- Cargo.lock | 1 + mithril-client/Cargo.toml | 1 + mithril-client/src/client.rs | 44 ++++++++++++++++++++++++++++-------- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d68dc867ff..0683c520c04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3944,6 +3944,7 @@ dependencies = [ "hex", "http", "httpmock", + "mithril-aggregator-discovery", "mithril-cardano-node-internal-database", "mithril-common", "mockall", diff --git a/mithril-client/Cargo.toml b/mithril-client/Cargo.toml index 4d4a4c7eaca..379ef92a99f 100644 --- a/mithril-client/Cargo.toml +++ b/mithril-client/Cargo.toml @@ -59,6 +59,7 @@ flate2 = { version = "1.1.4", optional = true } flume = { version = "0.11.1", optional = true } futures = "0.3.31" mithril-common = { path = "../mithril-common", version = ">=0.6", default-features = false } +mithril-aggregator-discovery = { path = "../internal/mithril-aggregator-discovery" } reqwest = { workspace = true, default-features = false, features = [ "charset", "http2", diff --git a/mithril-client/src/client.rs b/mithril-client/src/client.rs index 898f8fb61ae..7539ffc10a3 100644 --- a/mithril-client/src/client.rs +++ b/mithril-client/src/client.rs @@ -1,6 +1,9 @@ use anyhow::{Context, anyhow}; #[cfg(feature = "fs")] use chrono::Utc; +use mithril_aggregator_discovery::{ + AggregatorDiscoverer, HttpConfigAggregatorDiscoverer, MithrilNetwork, +}; use mithril_common::messages::AggregatorCapabilities; use reqwest::Url; use serde::{Deserialize, Serialize}; @@ -44,7 +47,7 @@ const fn one_week_in_seconds() -> u32 { /// The type of discovery to use to find the aggregator to connect to. pub enum AggregatorDiscoveryType { /// Automatically discover the aggregator. - Automatic, + Automatic(MithrilNetwork), /// Use a specific URL to connect to the aggregator. Url(String), } @@ -170,6 +173,7 @@ impl Client { pub struct ClientBuilder { aggregator_discovery: AggregatorDiscoveryType, aggregator_capabilities: Option, + aggregator_discoverer: Option>, genesis_verification_key: Option, origin_tag: Option, client_type: Option, @@ -191,9 +195,11 @@ impl ClientBuilder { /// Constructs a new `ClientBuilder` that fetches data from the aggregator at the given /// endpoint and with the given genesis verification key. pub fn aggregator(endpoint: &str, genesis_verification_key: &str) -> ClientBuilder { - Self::new(AggregatorDiscoveryType::Url(endpoint.to_string())).with_genesis_verification_key( - GenesisVerificationKey::JsonHex(genesis_verification_key.to_string()), - ) + Self::new(AggregatorDiscoveryType::Url(endpoint.to_string())) + .with_genesis_verification_key(GenesisVerificationKey::JsonHex( + genesis_verification_key.to_string(), + )) + .with_aggregator_discoverer(Arc::new(HttpConfigAggregatorDiscoverer::default())) } /// Constructs a new `ClientBuilder` without any dependency set. @@ -201,6 +207,7 @@ impl ClientBuilder { Self { aggregator_discovery, aggregator_capabilities: None, + aggregator_discoverer: None, genesis_verification_key: None, origin_tag: None, client_type: None, @@ -236,6 +243,16 @@ impl ClientBuilder { self } + /// Sets the aggregator discoverer to use to find the aggregator endpoint when in automatic discovery. + pub fn with_aggregator_discoverer( + mut self, + discoverer: Arc, + ) -> ClientBuilder { + self.aggregator_discoverer = Some(discoverer); + + self + } + /// Returns a `Client` that uses the dependencies provided to this `ClientBuilder`. /// /// The builder will try to create the missing dependencies using default implementations @@ -259,7 +276,7 @@ impl ClientBuilder { let feedback_sender = FeedbackSender::new(&self.feedback_receivers); let aggregator_client = match self.aggregator_client { - None => Arc::new(self.build_aggregator_client(logger.clone())?), + None => Arc::new(self.build_aggregator_client(logger.clone()).await?), Some(client) => client, }; @@ -362,15 +379,24 @@ impl ClientBuilder { }) } - fn build_aggregator_client( + async fn build_aggregator_client( &self, logger: Logger, ) -> Result { let aggregator_endpoint = match self.aggregator_discovery { AggregatorDiscoveryType::Url(ref url) => url.clone(), - AggregatorDiscoveryType::Automatic => { - todo!("Implement automatic aggregator discovery") - } + AggregatorDiscoveryType::Automatic(ref network) => match &self.aggregator_discoverer { + Some(discoverer) => discoverer + .get_available_aggregators(network.to_owned()) + .await + .with_context(|| "Discovering aggregator endpoint failed")? + .next() + .unwrap() + .into(), + None => { + return Err(anyhow!("The aggregator discoverer must be provided to build the client with automatic discovery using the 'with_aggregator_discoverer' function").into()); + } + }, }; let endpoint_url = Url::parse(&aggregator_endpoint).with_context(|| { format!("Invalid aggregator endpoint, it must be a correctly formed url: '{aggregator_endpoint}'") From aa6b3b67b9f0b62475e4780378629c5d47ea13f7 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Mon, 17 Nov 2025 16:54:54 +0100 Subject: [PATCH 15/31] feat(aggregator-discovery): implement aggregator discovery in client builder --- mithril-client/src/client.rs | 67 ++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/mithril-client/src/client.rs b/mithril-client/src/client.rs index 7539ffc10a3..f06850c2f15 100644 --- a/mithril-client/src/client.rs +++ b/mithril-client/src/client.rs @@ -1,17 +1,19 @@ use anyhow::{Context, anyhow}; #[cfg(feature = "fs")] use chrono::Utc; -use mithril_aggregator_discovery::{ - AggregatorDiscoverer, HttpConfigAggregatorDiscoverer, MithrilNetwork, -}; -use mithril_common::messages::AggregatorCapabilities; + use reqwest::Url; use serde::{Deserialize, Serialize}; use slog::{Logger, o}; use std::collections::HashMap; use std::sync::Arc; +use mithril_aggregator_discovery::{ + AggregatorDiscoverer, CapableAggregatorDiscoverer, HttpConfigAggregatorDiscoverer, + MithrilNetwork, +}; use mithril_common::api_version::APIVersionProvider; +use mithril_common::messages::AggregatorCapabilities; use mithril_common::{MITHRIL_CLIENT_TYPE_HEADER, MITHRIL_ORIGIN_TAG_HEADER}; use crate::MithrilResult; @@ -195,11 +197,9 @@ impl ClientBuilder { /// Constructs a new `ClientBuilder` that fetches data from the aggregator at the given /// endpoint and with the given genesis verification key. pub fn aggregator(endpoint: &str, genesis_verification_key: &str) -> ClientBuilder { - Self::new(AggregatorDiscoveryType::Url(endpoint.to_string())) - .with_genesis_verification_key(GenesisVerificationKey::JsonHex( - genesis_verification_key.to_string(), - )) - .with_aggregator_discoverer(Arc::new(HttpConfigAggregatorDiscoverer::default())) + Self::new(AggregatorDiscoveryType::Url(endpoint.to_string())).with_genesis_verification_key( + GenesisVerificationKey::JsonHex(genesis_verification_key.to_string()), + ) } /// Constructs a new `ClientBuilder` without any dependency set. @@ -253,6 +253,13 @@ impl ClientBuilder { self } + /// Sets the default aggregator discoverer to use to find the aggregator endpoint when in automatic discovery. + pub fn with_default_aggregator_discoverer(mut self) -> ClientBuilder { + self.aggregator_discoverer = Some(Arc::new(HttpConfigAggregatorDiscoverer::default())); + + self + } + /// Returns a `Client` that uses the dependencies provided to this `ClientBuilder`. /// /// The builder will try to create the missing dependencies using default implementations @@ -276,7 +283,7 @@ impl ClientBuilder { let feedback_sender = FeedbackSender::new(&self.feedback_receivers); let aggregator_client = match self.aggregator_client { - None => Arc::new(self.build_aggregator_client(logger.clone()).await?), + None => Arc::new(self.build_aggregator_client(logger.clone())?), Some(client) => client, }; @@ -379,24 +386,40 @@ impl ClientBuilder { }) } - async fn build_aggregator_client( + fn build_aggregator_client( &self, logger: Logger, ) -> Result { let aggregator_endpoint = match self.aggregator_discovery { AggregatorDiscoveryType::Url(ref url) => url.clone(), - AggregatorDiscoveryType::Automatic(ref network) => match &self.aggregator_discoverer { - Some(discoverer) => discoverer - .get_available_aggregators(network.to_owned()) - .await - .with_context(|| "Discovering aggregator endpoint failed")? - .next() - .unwrap() - .into(), - None => { - return Err(anyhow!("The aggregator discoverer must be provided to build the client with automatic discovery using the 'with_aggregator_discoverer' function").into()); + AggregatorDiscoveryType::Automatic(ref network) => { + match self.aggregator_discoverer.clone() { + Some(discoverer) => { + let discoverer = if let Some(capabilities) = &self.aggregator_capabilities { + Arc::new(CapableAggregatorDiscoverer::new( + capabilities.to_owned(), + discoverer.clone(), + )) as Arc + } else { + discoverer as Arc + }; + tokio::task::block_in_place(move || { + tokio::runtime::Handle::current().block_on(async move { + discoverer + .get_available_aggregators(network.to_owned()) + .await + .with_context(|| "Discovering aggregator endpoint failed")? + .next() + .ok_or(anyhow!("No aggregator was available through discovery")) + }) + })? + .into() + } + None => { + return Err(anyhow!("The aggregator discoverer must be provided to build the client with automatic discovery using the 'with_aggregator_discoverer' function").into()); + } } - }, + } }; let endpoint_url = Url::parse(&aggregator_endpoint).with_context(|| { format!("Invalid aggregator endpoint, it must be a correctly formed url: '{aggregator_endpoint}'") From 36b69b25be345c96390a3f447286b2e1107b96d3 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Tue, 18 Nov 2025 10:46:18 +0100 Subject: [PATCH 16/31] feat(aggregator-discovery): add HTTP timeouts --- .../src/http_config_discoverer.rs | 6 ++++-- internal/mithril-aggregator-discovery/src/model.rs | 8 +++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/mithril-aggregator-discovery/src/http_config_discoverer.rs b/internal/mithril-aggregator-discovery/src/http_config_discoverer.rs index d4d6ebb6c9f..34ba0622329 100644 --- a/internal/mithril-aggregator-discovery/src/http_config_discoverer.rs +++ b/internal/mithril-aggregator-discovery/src/http_config_discoverer.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, time::Duration}; use anyhow::Context; use reqwest::Client; @@ -45,6 +45,8 @@ pub struct HttpConfigAggregatorDiscoverer { } impl HttpConfigAggregatorDiscoverer { + const HTTP_TIMEOUT: Duration = Duration::from_secs(10); + /// Creates a new `HttpConfigAggregatorDiscoverer` instance with the provided results. pub fn new(configuration_file_url: &str) -> Self { Self { @@ -54,7 +56,7 @@ impl HttpConfigAggregatorDiscoverer { /// Builds a reqwest HTTP client. fn build_client(&self) -> StdResult { - let client_builder = Client::builder(); + let client_builder = Client::builder().timeout(Self::HTTP_TIMEOUT); let client = client_builder.build()?; Ok(client) diff --git a/internal/mithril-aggregator-discovery/src/model.rs b/internal/mithril-aggregator-discovery/src/model.rs index 4fa0805baf1..c394a2b1f39 100644 --- a/internal/mithril-aggregator-discovery/src/model.rs +++ b/internal/mithril-aggregator-discovery/src/model.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use mithril_aggregator_client::{AggregatorHttpClient, query::GetAggregatorFeaturesQuery}; use mithril_common::{StdResult, messages::AggregatorCapabilities}; @@ -30,6 +32,8 @@ pub struct AggregatorEndpoint { } impl AggregatorEndpoint { + const HTTP_TIMEOUT: Duration = Duration::from_secs(5); + /// Create a new AggregatorEndpoint instance pub fn new(url: String) -> Self { Self { url } @@ -37,7 +41,9 @@ impl AggregatorEndpoint { /// Retrieve the capabilities of the aggregator pub async fn retrieve_capabilities(&self) -> StdResult { - let aggregator_client = AggregatorHttpClient::builder(self.url.clone()).build()?; + let aggregator_client = AggregatorHttpClient::builder(self.url.clone()) + .with_timeout(Self::HTTP_TIMEOUT) + .build()?; Ok(aggregator_client .send(GetAggregatorFeaturesQuery::current()) From 7ab98ed27a6d3353900d365a4fee43834b2dd703 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Tue, 18 Nov 2025 17:25:54 +0100 Subject: [PATCH 17/31] refactor(aggregator-discoverer): introduce 'RequiredAggregatorCapabilities' type Which allows to combine signed entity types and aggregate signature type in a flexible way. --- .../src/capabilities_discoverer.rs | 618 ++++++++++-------- .../mithril-aggregator-discovery/src/lib.rs | 2 +- 2 files changed, 353 insertions(+), 267 deletions(-) diff --git a/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs b/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs index 538074c3124..13ec68dbf86 100644 --- a/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs +++ b/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs @@ -1,52 +1,85 @@ use std::sync::Arc; -use mithril_common::{StdResult, messages::AggregatorCapabilities}; +use mithril_common::{ + AggregateSignatureType, StdResult, entities::SignedEntityTypeDiscriminants, + messages::AggregatorCapabilities, +}; use crate::{AggregatorDiscoverer, AggregatorEndpoint, MithrilNetwork}; +/// Required capabilities for an aggregator. +#[derive(Clone)] +pub enum RequiredAggregatorCapabilities { + /// Signed entity type. + SignedEntityType(SignedEntityTypeDiscriminants), + /// Aggregate signature type. + AggregateSignatureType(AggregateSignatureType), + /// Logical OR of required capabilities. + Or(Vec), + /// Logical AND of required capabilities. + And(Vec), +} + +impl RequiredAggregatorCapabilities { + /// Check if the available capabilities match the required capabilities. + fn matches(&self, available: &AggregatorCapabilities) -> bool { + match self { + RequiredAggregatorCapabilities::SignedEntityType(required_signed_entity_type) => { + available + .signed_entity_types + .iter() + .any(|req| req == required_signed_entity_type) + } + RequiredAggregatorCapabilities::AggregateSignatureType( + required_aggregate_signature_types, + ) => *required_aggregate_signature_types == available.aggregate_signature_type, + RequiredAggregatorCapabilities::Or(requirements) => { + requirements.iter().any(|req| req.matches(available)) + } + RequiredAggregatorCapabilities::And(requirements) => { + requirements.iter().all(|req| req.matches(available)) + } + } + } +} + /// An aggregator discoverer for specific capabilities. pub struct CapableAggregatorDiscoverer { - capabilities: AggregatorCapabilities, + required_capabilities: RequiredAggregatorCapabilities, inner_discoverer: Arc, } impl CapableAggregatorDiscoverer { /// Creates a new `CapableAggregatorDiscoverer` instance with the provided capabilities. pub fn new( - capabilities: AggregatorCapabilities, + capabilities: RequiredAggregatorCapabilities, inner_discoverer: Arc, ) -> Self { Self { - capabilities, + required_capabilities: capabilities, inner_discoverer, } } +} - /// Check if the available capabilities match the required capabilities. - /// - /// Returns true if: - /// - The aggregate signature types are the same. - /// - All required signed entity types are included in the available signed entity types. - fn capabilities_match( - required: &AggregatorCapabilities, - available: &AggregatorCapabilities, - ) -> bool { - if available.aggregate_signature_type != required.aggregate_signature_type { - return false; - } - - let available_signed_entity_types = &available.signed_entity_types; - let required_signed_entity_types = &required.signed_entity_types; +#[async_trait::async_trait] +impl AggregatorDiscoverer for CapableAggregatorDiscoverer { + async fn get_available_aggregators( + &self, + network: MithrilNetwork, + ) -> StdResult>> { + let aggregator_endpoints = self.inner_discoverer.get_available_aggregators(network).await?; - required_signed_entity_types - .iter() - .all(|req| available_signed_entity_types.contains(req)) + Ok(Box::new(CapableAggregatorDiscovererIterator { + required_capabilities: self.required_capabilities.clone(), + inner_iterator: aggregator_endpoints, + })) } } /// An iterator over aggregator endpoints filtered by capabilities. struct CapableAggregatorDiscovererIterator { - capabilities: AggregatorCapabilities, + required_capabilities: RequiredAggregatorCapabilities, inner_iterator: Box>, } @@ -62,10 +95,7 @@ impl Iterator for CapableAggregatorDiscovererIterator { }) }); if let Ok(aggregator_capabilities) = aggregator_capabilities { - if CapableAggregatorDiscoverer::capabilities_match( - &self.capabilities, - &aggregator_capabilities, - ) { + if self.required_capabilities.matches(&aggregator_capabilities) { return Some(aggregator_endpoint); } } @@ -75,21 +105,6 @@ impl Iterator for CapableAggregatorDiscovererIterator { } } -#[async_trait::async_trait] -impl AggregatorDiscoverer for CapableAggregatorDiscoverer { - async fn get_available_aggregators( - &self, - network: MithrilNetwork, - ) -> StdResult>> { - let aggregator_endpoints = self.inner_discoverer.get_available_aggregators(network).await?; - - Ok(Box::new(CapableAggregatorDiscovererIterator { - capabilities: self.capabilities.clone(), - inner_iterator: aggregator_endpoints, - })) - } -} - #[cfg(test)] mod tests { use std::collections::BTreeSet; @@ -98,253 +113,324 @@ mod tests { use serde_json::json; use mithril_common::{ - AggregateSignatureType, + AggregateSignatureType::Concatenation, entities::SignedEntityTypeDiscriminants::{ CardanoDatabase, CardanoStakeDistribution, CardanoTransactions, + MithrilStakeDistribution, }, messages::AggregatorFeaturesMessage, }; use super::*; - fn create_aggregator_features_message( - capabilities: AggregatorCapabilities, - ) -> AggregatorFeaturesMessage { - AggregatorFeaturesMessage { - open_api_version: "1.0.0".to_string(), - documentation_url: "https://docs".to_string(), - capabilities, + mod required_capabilities { + use super::*; + + #[test] + fn required_capabilities_match_signed_entity_types_success() { + let required = + RequiredAggregatorCapabilities::SignedEntityType(CardanoStakeDistribution); + let available = AggregatorCapabilities { + aggregate_signature_type: Concatenation, + signed_entity_types: BTreeSet::from([ + CardanoTransactions, + CardanoStakeDistribution, + CardanoDatabase, + ]), + cardano_transactions_prover: None, + }; + + assert!(required.matches(&available)); } - } - #[test] - fn capabilities_match_success() { - let required = AggregatorCapabilities { - aggregate_signature_type: AggregateSignatureType::Concatenation, - signed_entity_types: BTreeSet::from([CardanoTransactions, CardanoStakeDistribution]), - cardano_transactions_prover: None, - }; - - let available = AggregatorCapabilities { - aggregate_signature_type: AggregateSignatureType::Concatenation, - signed_entity_types: BTreeSet::from([ - CardanoTransactions, - CardanoStakeDistribution, - CardanoDatabase, - ]), - cardano_transactions_prover: None, - }; - - assert!(CapableAggregatorDiscoverer::capabilities_match( - &required, &available - )); - } + #[test] + fn required_capabilities_match_signed_entity_types_failure() { + let required = + RequiredAggregatorCapabilities::SignedEntityType(MithrilStakeDistribution); + let available = AggregatorCapabilities { + aggregate_signature_type: Concatenation, + signed_entity_types: BTreeSet::from([ + CardanoTransactions, + CardanoStakeDistribution, + CardanoDatabase, + ]), + cardano_transactions_prover: None, + }; - #[test] - fn capabilities_match_failure() { - let required = AggregatorCapabilities { - aggregate_signature_type: AggregateSignatureType::Concatenation, - signed_entity_types: BTreeSet::from([CardanoTransactions, CardanoStakeDistribution]), - cardano_transactions_prover: None, - }; - - let available = AggregatorCapabilities { - aggregate_signature_type: AggregateSignatureType::Concatenation, - signed_entity_types: BTreeSet::from([CardanoTransactions]), - cardano_transactions_prover: None, - }; - - assert!(!CapableAggregatorDiscoverer::capabilities_match( - &required, &available - )); - } + assert!(!required.matches(&available)); + } - #[tokio::test(flavor = "multi_thread")] - async fn get_available_aggregators_success() { - let capabilities = AggregatorCapabilities { - aggregate_signature_type: AggregateSignatureType::Concatenation, - signed_entity_types: BTreeSet::from([CardanoStakeDistribution, CardanoTransactions]), - cardano_transactions_prover: None, - }; - let aggregator_server = MockServer::start(); - let aggregator_server_mock = aggregator_server.mock(|when, then| { - when.path("/"); - then.status(200) - .body(json!(create_aggregator_features_message(capabilities)).to_string()); - }); - let discoverer = CapableAggregatorDiscoverer::new( - AggregatorCapabilities { - aggregate_signature_type: AggregateSignatureType::Concatenation, + #[test] + fn required_capabilities_match_signed_aggregate_signature_type_success() { + let required = RequiredAggregatorCapabilities::AggregateSignatureType(Concatenation); + let available = AggregatorCapabilities { + aggregate_signature_type: Concatenation, + signed_entity_types: BTreeSet::from([ + CardanoTransactions, + CardanoStakeDistribution, + CardanoDatabase, + ]), + cardano_transactions_prover: None, + }; + + assert!(required.matches(&available)); + } + + #[test] + fn required_capabilities_match_or_success() { + let required = RequiredAggregatorCapabilities::Or(vec![ + RequiredAggregatorCapabilities::SignedEntityType(MithrilStakeDistribution), + RequiredAggregatorCapabilities::AggregateSignatureType(Concatenation), + ]); + let available = AggregatorCapabilities { + aggregate_signature_type: Concatenation, + signed_entity_types: BTreeSet::from([ + CardanoTransactions, + CardanoStakeDistribution, + CardanoDatabase, + ]), + cardano_transactions_prover: None, + }; + + assert!(required.matches(&available)); + } + + #[test] + fn required_capabilities_match_and_success() { + let required = RequiredAggregatorCapabilities::And(vec![ + RequiredAggregatorCapabilities::SignedEntityType(CardanoTransactions), + RequiredAggregatorCapabilities::SignedEntityType(CardanoStakeDistribution), + RequiredAggregatorCapabilities::AggregateSignatureType(Concatenation), + ]); + let available = AggregatorCapabilities { + aggregate_signature_type: Concatenation, + signed_entity_types: BTreeSet::from([ + CardanoTransactions, + CardanoStakeDistribution, + CardanoDatabase, + ]), + cardano_transactions_prover: None, + }; + + assert!(required.matches(&available)); + } + + #[test] + fn required_capabilities_match_and_failure() { + let required = RequiredAggregatorCapabilities::And(vec![ + RequiredAggregatorCapabilities::SignedEntityType(CardanoTransactions), + RequiredAggregatorCapabilities::SignedEntityType(CardanoStakeDistribution), + RequiredAggregatorCapabilities::AggregateSignatureType(Concatenation), + ]); + let available = AggregatorCapabilities { + aggregate_signature_type: Concatenation, signed_entity_types: BTreeSet::from([CardanoTransactions]), cardano_transactions_prover: None, - }, - Arc::new(crate::test::double::AggregatorDiscovererFake::new(vec![ - Ok(vec![AggregatorEndpoint::new(aggregator_server.url("/"))]), - ])), - ); - - let mut aggregators = discoverer - .get_available_aggregators(MithrilNetwork::new("release-devnet".into())) - .await - .unwrap(); - - let next_aggregator = aggregators.next(); - aggregator_server_mock.assert(); - assert_eq!( - Some(AggregatorEndpoint::new(aggregator_server.url("/"))), - next_aggregator - ); + }; + + assert!(!required.matches(&available)); + } } - #[tokio::test(flavor = "multi_thread")] - async fn get_available_aggregators_succeeds_when_aggregator_capabilities_do_not_match() { - let capabilities = AggregatorCapabilities { - aggregate_signature_type: AggregateSignatureType::Concatenation, - signed_entity_types: BTreeSet::from([CardanoTransactions]), - cardano_transactions_prover: None, - }; - let aggregator_server = MockServer::start(); - let aggregator_server_mock = aggregator_server.mock(|when, then| { - when.path("/"); - then.status(200) - .body(json!(create_aggregator_features_message(capabilities)).to_string()); - }); - let discoverer = CapableAggregatorDiscoverer::new( - AggregatorCapabilities { - aggregate_signature_type: AggregateSignatureType::Concatenation, - signed_entity_types: BTreeSet::from([CardanoDatabase]), + mod capable_discoverer { + use super::*; + + fn create_aggregator_features_message( + capabilities: AggregatorCapabilities, + ) -> AggregatorFeaturesMessage { + AggregatorFeaturesMessage { + open_api_version: "1.0.0".to_string(), + documentation_url: "https://docs".to_string(), + capabilities, + } + } + + #[tokio::test(flavor = "multi_thread")] + async fn get_available_aggregators_success() { + let capabilities = AggregatorCapabilities { + aggregate_signature_type: Concatenation, + signed_entity_types: BTreeSet::from([ + CardanoStakeDistribution, + CardanoTransactions, + ]), cardano_transactions_prover: None, - }, - Arc::new(crate::test::double::AggregatorDiscovererFake::new(vec![ - Ok(vec![AggregatorEndpoint::new(aggregator_server.url("/"))]), - ])), - ); - - let mut aggregators = discoverer - .get_available_aggregators(MithrilNetwork::new("release-devnet".into())) - .await - .unwrap(); - - let next_aggregator = aggregators.next(); - aggregator_server_mock.assert(); - assert!(next_aggregator.is_none()); - } + }; + let aggregator_server = MockServer::start(); + let aggregator_server_mock = aggregator_server.mock(|when, then| { + when.path("/"); + then.status(200) + .body(json!(create_aggregator_features_message(capabilities)).to_string()); + }); + let discoverer = CapableAggregatorDiscoverer::new( + RequiredAggregatorCapabilities::And(vec![ + RequiredAggregatorCapabilities::SignedEntityType(CardanoTransactions), + RequiredAggregatorCapabilities::AggregateSignatureType(Concatenation), + ]), + Arc::new(crate::test::double::AggregatorDiscovererFake::new(vec![ + Ok(vec![AggregatorEndpoint::new(aggregator_server.url("/"))]), + ])), + ); + + let mut aggregators = discoverer + .get_available_aggregators(MithrilNetwork::new("release-devnet".into())) + .await + .unwrap(); + + let next_aggregator = aggregators.next(); + aggregator_server_mock.assert(); + assert_eq!( + Some(AggregatorEndpoint::new(aggregator_server.url("/"))), + next_aggregator + ); + } - #[tokio::test(flavor = "multi_thread")] - async fn get_available_aggregators_succeeds_when_one_aggregator_returns_an_error() { - let aggregator_server_1 = MockServer::start(); - let aggregator_server_mock_1 = aggregator_server_1.mock(|when, then| { - when.path("/"); - then.status(500); - }); - let capabilities_2 = AggregatorCapabilities { - aggregate_signature_type: AggregateSignatureType::Concatenation, - signed_entity_types: BTreeSet::from([CardanoStakeDistribution, CardanoDatabase]), - cardano_transactions_prover: None, - }; - let aggregator_server_2 = MockServer::start(); - let aggregator_server_mock_2 = aggregator_server_2.mock(|when, then| { - when.path("/"); - then.status(200) - .body(json!(create_aggregator_features_message(capabilities_2)).to_string()); - }); - let discoverer = CapableAggregatorDiscoverer::new( - AggregatorCapabilities { - aggregate_signature_type: AggregateSignatureType::Concatenation, - signed_entity_types: BTreeSet::from([CardanoDatabase]), + #[tokio::test(flavor = "multi_thread")] + async fn get_available_aggregators_succeeds_when_aggregator_capabilities_do_not_match() { + let capabilities = AggregatorCapabilities { + aggregate_signature_type: Concatenation, + signed_entity_types: BTreeSet::from([CardanoTransactions]), cardano_transactions_prover: None, - }, - Arc::new(crate::test::double::AggregatorDiscovererFake::new(vec![ - Ok(vec![ - AggregatorEndpoint::new(aggregator_server_1.url("/")), - AggregatorEndpoint::new(aggregator_server_2.url("/")), + }; + let aggregator_server = MockServer::start(); + let aggregator_server_mock = aggregator_server.mock(|when, then| { + when.path("/"); + then.status(200) + .body(json!(create_aggregator_features_message(capabilities)).to_string()); + }); + let discoverer = CapableAggregatorDiscoverer::new( + RequiredAggregatorCapabilities::And(vec![ + RequiredAggregatorCapabilities::SignedEntityType(CardanoDatabase), + RequiredAggregatorCapabilities::AggregateSignatureType(Concatenation), ]), - ])), - ); - - let mut aggregators = discoverer - .get_available_aggregators(MithrilNetwork::new("release-devnet".into())) - .await - .unwrap(); - - let next_aggregator = aggregators.next(); - aggregator_server_mock_1.assert(); - aggregator_server_mock_2.assert(); - assert_eq!( - Some(AggregatorEndpoint::new(aggregator_server_2.url("/"))), - next_aggregator - ); - } + Arc::new(crate::test::double::AggregatorDiscovererFake::new(vec![ + Ok(vec![AggregatorEndpoint::new(aggregator_server.url("/"))]), + ])), + ); + + let mut aggregators = discoverer + .get_available_aggregators(MithrilNetwork::new("release-devnet".into())) + .await + .unwrap(); + + let next_aggregator = aggregators.next(); + aggregator_server_mock.assert(); + assert!(next_aggregator.is_none()); + } - #[tokio::test(flavor = "multi_thread")] - async fn get_available_aggregators_succeeds_and_makes_minimum_calls_to_aggregators() { - let aggregator_server_1 = MockServer::start(); - let aggregator_server_mock_1 = aggregator_server_1.mock(|when, then| { - when.path("/"); - then.status(500); - }); - let capabilities_2 = AggregatorCapabilities { - aggregate_signature_type: AggregateSignatureType::Concatenation, - signed_entity_types: BTreeSet::from([CardanoStakeDistribution]), - cardano_transactions_prover: None, - }; - let aggregator_server_2 = MockServer::start(); - let aggregator_server_mock_2 = aggregator_server_2.mock(|when, then| { - when.path("/"); - then.status(200) - .body(json!(create_aggregator_features_message(capabilities_2)).to_string()); - }); - let capabilities_3 = AggregatorCapabilities { - aggregate_signature_type: AggregateSignatureType::Concatenation, - signed_entity_types: BTreeSet::from([CardanoDatabase]), - cardano_transactions_prover: None, - }; - let aggregator_server_3 = MockServer::start(); - let aggregator_server_mock_3 = aggregator_server_3.mock(|when, then| { - when.path("/"); - then.status(200) - .body(json!(create_aggregator_features_message(capabilities_3)).to_string()); - }); - let capabilities_4 = AggregatorCapabilities { - aggregate_signature_type: AggregateSignatureType::Concatenation, - signed_entity_types: BTreeSet::from([CardanoDatabase]), - cardano_transactions_prover: None, - }; - let aggregator_server_4 = MockServer::start(); - let aggregator_server_mock_4 = aggregator_server_4.mock(|when, then| { - when.path("/"); - then.status(200) - .body(json!(create_aggregator_features_message(capabilities_4)).to_string()); - }); - let discoverer = CapableAggregatorDiscoverer::new( - AggregatorCapabilities { - aggregate_signature_type: AggregateSignatureType::Concatenation, + #[tokio::test(flavor = "multi_thread")] + async fn get_available_aggregators_succeeds_when_one_aggregator_returns_an_error() { + let aggregator_server_1 = MockServer::start(); + let aggregator_server_mock_1 = aggregator_server_1.mock(|when, then| { + when.path("/"); + then.status(500); + }); + let capabilities_2 = AggregatorCapabilities { + aggregate_signature_type: Concatenation, + signed_entity_types: BTreeSet::from([CardanoStakeDistribution, CardanoDatabase]), + cardano_transactions_prover: None, + }; + let aggregator_server_2 = MockServer::start(); + let aggregator_server_mock_2 = aggregator_server_2.mock(|when, then| { + when.path("/"); + then.status(200) + .body(json!(create_aggregator_features_message(capabilities_2)).to_string()); + }); + let discoverer = CapableAggregatorDiscoverer::new( + RequiredAggregatorCapabilities::And(vec![ + RequiredAggregatorCapabilities::SignedEntityType(CardanoDatabase), + RequiredAggregatorCapabilities::AggregateSignatureType(Concatenation), + ]), + Arc::new(crate::test::double::AggregatorDiscovererFake::new(vec![ + Ok(vec![ + AggregatorEndpoint::new(aggregator_server_1.url("/")), + AggregatorEndpoint::new(aggregator_server_2.url("/")), + ]), + ])), + ); + + let mut aggregators = discoverer + .get_available_aggregators(MithrilNetwork::new("release-devnet".into())) + .await + .unwrap(); + + let next_aggregator = aggregators.next(); + aggregator_server_mock_1.assert(); + aggregator_server_mock_2.assert(); + assert_eq!( + Some(AggregatorEndpoint::new(aggregator_server_2.url("/"))), + next_aggregator + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn get_available_aggregators_succeeds_and_makes_minimum_calls_to_aggregators() { + let aggregator_server_1 = MockServer::start(); + let aggregator_server_mock_1 = aggregator_server_1.mock(|when, then| { + when.path("/"); + then.status(500); + }); + let capabilities_2 = AggregatorCapabilities { + aggregate_signature_type: Concatenation, + signed_entity_types: BTreeSet::from([CardanoStakeDistribution]), + cardano_transactions_prover: None, + }; + let aggregator_server_2 = MockServer::start(); + let aggregator_server_mock_2 = aggregator_server_2.mock(|when, then| { + when.path("/"); + then.status(200) + .body(json!(create_aggregator_features_message(capabilities_2)).to_string()); + }); + let capabilities_3 = AggregatorCapabilities { + aggregate_signature_type: Concatenation, signed_entity_types: BTreeSet::from([CardanoDatabase]), cardano_transactions_prover: None, - }, - Arc::new(crate::test::double::AggregatorDiscovererFake::new(vec![ - Ok(vec![ - AggregatorEndpoint::new(aggregator_server_1.url("/")), - AggregatorEndpoint::new(aggregator_server_2.url("/")), - AggregatorEndpoint::new(aggregator_server_3.url("/")), - AggregatorEndpoint::new(aggregator_server_4.url("/")), + }; + let aggregator_server_3 = MockServer::start(); + let aggregator_server_mock_3 = aggregator_server_3.mock(|when, then| { + when.path("/"); + then.status(200) + .body(json!(create_aggregator_features_message(capabilities_3)).to_string()); + }); + let capabilities_4 = AggregatorCapabilities { + aggregate_signature_type: Concatenation, + signed_entity_types: BTreeSet::from([CardanoDatabase]), + cardano_transactions_prover: None, + }; + let aggregator_server_4 = MockServer::start(); + let aggregator_server_mock_4 = aggregator_server_4.mock(|when, then| { + when.path("/"); + then.status(200) + .body(json!(create_aggregator_features_message(capabilities_4)).to_string()); + }); + let discoverer = CapableAggregatorDiscoverer::new( + RequiredAggregatorCapabilities::And(vec![ + RequiredAggregatorCapabilities::SignedEntityType(CardanoDatabase), + RequiredAggregatorCapabilities::AggregateSignatureType(Concatenation), ]), - ])), - ); - - let mut aggregators = discoverer - .get_available_aggregators(MithrilNetwork::new("release-devnet".into())) - .await - .unwrap(); - - let next_aggregator = aggregators.next(); - aggregator_server_mock_1.assert(); - aggregator_server_mock_2.assert(); - aggregator_server_mock_3.assert(); - assert_eq!(0, aggregator_server_mock_4.calls()); - assert_eq!( - Some(AggregatorEndpoint::new(aggregator_server_3.url("/"))), - next_aggregator - ); + Arc::new(crate::test::double::AggregatorDiscovererFake::new(vec![ + Ok(vec![ + AggregatorEndpoint::new(aggregator_server_1.url("/")), + AggregatorEndpoint::new(aggregator_server_2.url("/")), + AggregatorEndpoint::new(aggregator_server_3.url("/")), + AggregatorEndpoint::new(aggregator_server_4.url("/")), + ]), + ])), + ); + + let mut aggregators = discoverer + .get_available_aggregators(MithrilNetwork::new("release-devnet".into())) + .await + .unwrap(); + + let next_aggregator = aggregators.next(); + aggregator_server_mock_1.assert(); + aggregator_server_mock_2.assert(); + aggregator_server_mock_3.assert(); + assert_eq!(0, aggregator_server_mock_4.calls()); + assert_eq!( + Some(AggregatorEndpoint::new(aggregator_server_3.url("/"))), + next_aggregator + ); + } } } diff --git a/internal/mithril-aggregator-discovery/src/lib.rs b/internal/mithril-aggregator-discovery/src/lib.rs index 2eeeab5edab..077c0fbf01a 100644 --- a/internal/mithril-aggregator-discovery/src/lib.rs +++ b/internal/mithril-aggregator-discovery/src/lib.rs @@ -8,7 +8,7 @@ mod model; mod rand_discoverer; pub mod test; -pub use capabilities_discoverer::CapableAggregatorDiscoverer; +pub use capabilities_discoverer::{CapableAggregatorDiscoverer, RequiredAggregatorCapabilities}; pub use http_config_discoverer::HttpConfigAggregatorDiscoverer; pub use interface::AggregatorDiscoverer; pub use model::{AggregatorEndpoint, MithrilNetwork}; From 28e04d5c42ef416b53ff192da47baa4ad39d2c2a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Tue, 18 Nov 2025 17:27:13 +0100 Subject: [PATCH 18/31] chore(client): re-export missing types from common --- mithril-client/src/type_alias.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mithril-client/src/type_alias.rs b/mithril-client/src/type_alias.rs index 08f58ff75c0..a59082c1ece 100644 --- a/mithril-client/src/type_alias.rs +++ b/mithril-client/src/type_alias.rs @@ -66,13 +66,14 @@ pub use mithril_common::messages::CardanoStakeDistributionListItemMessage as Car /// `mithril-common` re-exports pub mod common { + pub use mithril_common::AggregateSignatureType; pub use mithril_common::crypto_helper::MKProof; pub use mithril_common::entities::{ AncillaryLocation, BlockHash, BlockNumber, CardanoDbBeacon, CardanoNetwork, ChainPoint, CompressionAlgorithm, DigestLocation, Epoch, EpochSpecifier, ImmutableFileNumber, ImmutablesLocation, MagicId, MultiFilesUri, ProtocolMessage, ProtocolMessagePartKey, - ProtocolParameters, SignedEntityType, SlotNumber, StakeDistribution, SupportedEra, - TemplateUri, TransactionHash, + ProtocolParameters, SignedEntityType, SignedEntityTypeDiscriminants, SlotNumber, + StakeDistribution, SupportedEra, TemplateUri, TransactionHash, }; pub use mithril_common::messages::{ AncillaryMessagePart, DigestsMessagePart, ImmutablesMessagePart, @@ -85,3 +86,6 @@ pub mod common { pub use mithril_common::test::double::Dummy; } } + +/// Required capabilities for an aggregator. +pub use mithril_aggregator_discovery::RequiredAggregatorCapabilities; From fd5c73c1214ae904ca24ee25ccf8679de4c35eaf Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Tue, 18 Nov 2025 17:43:18 +0100 Subject: [PATCH 19/31] chore(client): update capabilities to required capabilities --- .../src/capabilities_discoverer.rs | 10 ++++----- mithril-client/src/client.rs | 22 +++++++++++++++---- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs b/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs index 13ec68dbf86..e0cc8712da2 100644 --- a/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs +++ b/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs @@ -87,17 +87,17 @@ impl Iterator for CapableAggregatorDiscovererIterator { type Item = AggregatorEndpoint; fn next(&mut self) -> Option { - while let Some(aggregator_endpoint) = self.inner_iterator.next() { + for aggregator_endpoint in self.inner_iterator.by_ref() { let aggregator_endpoint_clone = aggregator_endpoint.clone(); let aggregator_capabilities = tokio::task::block_in_place(move || { tokio::runtime::Handle::current().block_on(async move { aggregator_endpoint_clone.retrieve_capabilities().await }) }); - if let Ok(aggregator_capabilities) = aggregator_capabilities { - if self.required_capabilities.matches(&aggregator_capabilities) { - return Some(aggregator_endpoint); - } + if let Ok(aggregator_capabilities) = aggregator_capabilities + && self.required_capabilities.matches(&aggregator_capabilities) + { + return Some(aggregator_endpoint); } } diff --git a/mithril-client/src/client.rs b/mithril-client/src/client.rs index f06850c2f15..7e683cfbf9e 100644 --- a/mithril-client/src/client.rs +++ b/mithril-client/src/client.rs @@ -10,10 +10,9 @@ use std::sync::Arc; use mithril_aggregator_discovery::{ AggregatorDiscoverer, CapableAggregatorDiscoverer, HttpConfigAggregatorDiscoverer, - MithrilNetwork, + MithrilNetwork, RequiredAggregatorCapabilities, }; use mithril_common::api_version::APIVersionProvider; -use mithril_common::messages::AggregatorCapabilities; use mithril_common::{MITHRIL_CLIENT_TYPE_HEADER, MITHRIL_ORIGIN_TAG_HEADER}; use crate::MithrilResult; @@ -174,7 +173,7 @@ impl Client { /// Builder than can be used to create a [Client] easily or with custom dependencies. pub struct ClientBuilder { aggregator_discovery: AggregatorDiscoveryType, - aggregator_capabilities: Option, + aggregator_capabilities: Option, aggregator_discoverer: Option>, genesis_verification_key: Option, origin_tag: Option, @@ -202,6 +201,18 @@ impl ClientBuilder { ) } + /// Constructs a new `ClientBuilder` that automatically discovers the aggregator for the given + /// Mithril network and with the given genesis verification key. + pub fn automatic(network: &str, genesis_verification_key: &str) -> ClientBuilder { + Self::new(AggregatorDiscoveryType::Automatic(MithrilNetwork::new( + network.to_string(), + ))) + .with_genesis_verification_key(GenesisVerificationKey::JsonHex( + genesis_verification_key.to_string(), + )) + .with_default_aggregator_discoverer() + } + /// Constructs a new `ClientBuilder` without any dependency set. pub fn new(aggregator_discovery: AggregatorDiscoveryType) -> ClientBuilder { Self { @@ -237,7 +248,10 @@ impl ClientBuilder { } /// Sets the aggregator capabilities expected to be matched by the aggregator with which the client will interact. - pub fn with_capabilities(mut self, capabilities: AggregatorCapabilities) -> ClientBuilder { + pub fn with_capabilities( + mut self, + capabilities: RequiredAggregatorCapabilities, + ) -> ClientBuilder { self.aggregator_capabilities = Some(capabilities); self From 4b92b45b11a6a7ebb9d3c6be722a570058ad00d2 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Mon, 24 Nov 2025 19:10:46 +0100 Subject: [PATCH 20/31] fix(aggregator-client): features route of the aggregator was not reached --- .../src/query/get/get_aggregator_features.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/mithril-aggregator-client/src/query/get/get_aggregator_features.rs b/internal/mithril-aggregator-client/src/query/get/get_aggregator_features.rs index 50ae26fa504..2158727bccc 100644 --- a/internal/mithril-aggregator-client/src/query/get/get_aggregator_features.rs +++ b/internal/mithril-aggregator-client/src/query/get/get_aggregator_features.rs @@ -29,7 +29,7 @@ impl AggregatorQuery for GetAggregatorFeaturesQuery { } fn route(&self) -> String { - "/".to_string() + "".to_string() } async fn handle_response( From 678222223564e9d319fa3b51939bc6498d5b971d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Mon, 24 Nov 2025 19:11:36 +0100 Subject: [PATCH 21/31] fix(aggregator-discovery): added missing 'All' variant for required capabilities --- .../src/capabilities_discoverer.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs b/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs index e0cc8712da2..dad9d609b6e 100644 --- a/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs +++ b/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs @@ -8,12 +8,15 @@ use mithril_common::{ use crate::{AggregatorDiscoverer, AggregatorEndpoint, MithrilNetwork}; /// Required capabilities for an aggregator. -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq, Debug)] pub enum RequiredAggregatorCapabilities { + /// All + All, /// Signed entity type. SignedEntityType(SignedEntityTypeDiscriminants), /// Aggregate signature type. AggregateSignatureType(AggregateSignatureType), + /// /// Logical OR of required capabilities. Or(Vec), /// Logical AND of required capabilities. @@ -24,6 +27,7 @@ impl RequiredAggregatorCapabilities { /// Check if the available capabilities match the required capabilities. fn matches(&self, available: &AggregatorCapabilities) -> bool { match self { + RequiredAggregatorCapabilities::All => true, RequiredAggregatorCapabilities::SignedEntityType(required_signed_entity_type) => { available .signed_entity_types @@ -126,6 +130,18 @@ mod tests { mod required_capabilities { use super::*; + #[test] + fn required_capabilities_match_all_success() { + let required = RequiredAggregatorCapabilities::All; + let available = AggregatorCapabilities { + aggregate_signature_type: Concatenation, + signed_entity_types: BTreeSet::from([]), + cardano_transactions_prover: None, + }; + + assert!(required.matches(&available)); + } + #[test] fn required_capabilities_match_signed_entity_types_success() { let required = From 956e5a7b8d29d08c84b8d344685191e7fb9e28bb Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Mon, 24 Nov 2025 19:12:51 +0100 Subject: [PATCH 22/31] feat(client-cli): add a 'aggregator-discovery' command --- .../mithril-aggregator-discovery/src/model.rs | 12 ++ .../commands/tools/aggregator_discovery.rs | 197 ++++++++++++++++++ mithril-client-cli/src/commands/tools/mod.rs | 23 ++ mithril-client/src/client.rs | 70 ++++--- mithril-client/src/type_alias.rs | 3 + .../src/aggregate_signature/signature.rs | 16 +- 6 files changed, 288 insertions(+), 33 deletions(-) create mode 100644 mithril-client-cli/src/commands/tools/aggregator_discovery.rs diff --git a/internal/mithril-aggregator-discovery/src/model.rs b/internal/mithril-aggregator-discovery/src/model.rs index c394a2b1f39..6f6f27666d4 100644 --- a/internal/mithril-aggregator-discovery/src/model.rs +++ b/internal/mithril-aggregator-discovery/src/model.rs @@ -25,6 +25,12 @@ impl MithrilNetwork { } } +impl From for MithrilNetwork { + fn from(name: String) -> Self { + MithrilNetwork::new(name) + } +} + /// Representation of an aggregator endpoint #[derive(Debug, Clone, PartialEq, Eq)] pub struct AggregatorEndpoint { @@ -57,3 +63,9 @@ impl From for String { endpoint.url } } + +impl std::fmt::Display for AggregatorEndpoint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.url) + } +} diff --git a/mithril-client-cli/src/commands/tools/aggregator_discovery.rs b/mithril-client-cli/src/commands/tools/aggregator_discovery.rs new file mode 100644 index 00000000000..c771f57dca3 --- /dev/null +++ b/mithril-client-cli/src/commands/tools/aggregator_discovery.rs @@ -0,0 +1,197 @@ +use clap::Parser; + +use mithril_client::{ + AggregatorDiscoveryType, ClientBuilder, MithrilNetwork, MithrilResult, + RequiredAggregatorCapabilities, + common::{AggregateSignatureType, SignedEntityTypeDiscriminants}, +}; + +/// Clap command to select an aggregator from the available ones with automatic discovery. +#[derive(Parser, Debug, Clone)] +pub struct AggregatorSelectCommand { + /// Path to the Cardano node database directory. + #[clap(long)] + network: MithrilNetwork, + + /// Maximum number of entries to retrieve + #[clap(long, default_value_t = 1)] + max_entries: usize, + + /// Signed entity types to consider for the discovery + /// + /// If not provided, all signed entity types are considered. + #[clap(long, value_parser, num_args = 0.., value_delimiter = ',')] + signed_entity_types: Vec, + + /// Aggregate signature types to consider for the discovery + /// + /// If not provided, all aggregate signature types are considered. + #[clap(long, value_parser, num_args = 0.., value_delimiter = ',')] + aggregate_signature_types: Vec, +} + +impl AggregatorSelectCommand { + /// Main command execution + pub async fn execute(&self) -> MithrilResult<()> { + let required_capabilities = self.build_required_capabilities(); + let client_builder = + ClientBuilder::new(AggregatorDiscoveryType::Automatic(self.network.clone())) + .with_capabilities(required_capabilities) + .with_default_aggregator_discoverer(); + let aggregator_endpoints = client_builder + .discover_aggregator(&self.network)? + .take(self.max_entries); + + println!( + "Discovering at most {} aggregator endpoints:", + self.max_entries, + ); + let mut found_aggregators = 0; + for endpoint in aggregator_endpoints { + println!("- Found: {endpoint}"); + found_aggregators += 1; + } + if found_aggregators == 0 { + println!("- No aggregator endpoint found matching the requirements."); + } + + Ok(()) + } + + fn build_required_capabilities(&self) -> RequiredAggregatorCapabilities { + if self.signed_entity_types.is_empty() && self.aggregate_signature_types.is_empty() { + return RequiredAggregatorCapabilities::All; + } + + let mut required_capabilities = vec![]; + if !self.signed_entity_types.is_empty() { + let mut required_capabilities_signed_entity_types = vec![]; + for signed_entity_type in &self.signed_entity_types { + required_capabilities_signed_entity_types.push( + RequiredAggregatorCapabilities::SignedEntityType(*signed_entity_type), + ); + } + required_capabilities.push(RequiredAggregatorCapabilities::Or( + required_capabilities_signed_entity_types, + )); + } + + if !self.aggregate_signature_types.is_empty() { + let mut required_capabilities_aggregate_signature_types = vec![]; + for aggregate_signature_type in &self.aggregate_signature_types { + required_capabilities_aggregate_signature_types.push( + RequiredAggregatorCapabilities::AggregateSignatureType( + *aggregate_signature_type, + ), + ); + } + required_capabilities.push(RequiredAggregatorCapabilities::Or( + required_capabilities_aggregate_signature_types, + )); + } + if required_capabilities.len() == 1 { + return required_capabilities.into_iter().next().unwrap(); + } else { + return RequiredAggregatorCapabilities::And(required_capabilities); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use mithril_client::common::SignedEntityTypeDiscriminants; + + #[test] + fn test_build_required_capabilities_all() { + let command = AggregatorSelectCommand { + network: MithrilNetwork::dummy(), + max_entries: 1, + signed_entity_types: vec![], + aggregate_signature_types: vec![], + }; + + let required_capabilities = command.build_required_capabilities(); + assert_eq!(required_capabilities, RequiredAggregatorCapabilities::All); + } + + #[test] + fn test_build_required_capabilities_signed_entity_types() { + let command = AggregatorSelectCommand { + network: MithrilNetwork::dummy(), + max_entries: 1, + signed_entity_types: vec![ + SignedEntityTypeDiscriminants::CardanoTransactions, + SignedEntityTypeDiscriminants::CardanoStakeDistribution, + ], + aggregate_signature_types: vec![], + }; + + let required_capabilities = command.build_required_capabilities(); + + assert_eq!( + required_capabilities, + RequiredAggregatorCapabilities::Or(vec![ + RequiredAggregatorCapabilities::SignedEntityType( + SignedEntityTypeDiscriminants::CardanoTransactions + ), + RequiredAggregatorCapabilities::SignedEntityType( + SignedEntityTypeDiscriminants::CardanoStakeDistribution + ), + ]) + ); + } + + #[test] + fn test_build_required_capabilities_aggregate_signature_types() { + let command = AggregatorSelectCommand { + network: MithrilNetwork::dummy(), + max_entries: 1, + signed_entity_types: vec![], + aggregate_signature_types: vec![AggregateSignatureType::Concatenation], + }; + let required_capabilities = command.build_required_capabilities(); + + assert_eq!( + required_capabilities, + RequiredAggregatorCapabilities::Or(vec![ + RequiredAggregatorCapabilities::AggregateSignatureType( + AggregateSignatureType::Concatenation + ), + ]) + ); + } + + #[test] + fn test_build_required_capabilities_both() { + let command = AggregatorSelectCommand { + network: MithrilNetwork::dummy(), + max_entries: 1, + signed_entity_types: vec![ + SignedEntityTypeDiscriminants::CardanoTransactions, + SignedEntityTypeDiscriminants::CardanoStakeDistribution, + ], + aggregate_signature_types: vec![AggregateSignatureType::Concatenation], + }; + let required_capabilities = command.build_required_capabilities(); + + assert_eq!( + required_capabilities, + RequiredAggregatorCapabilities::And(vec![ + RequiredAggregatorCapabilities::Or(vec![ + RequiredAggregatorCapabilities::SignedEntityType( + SignedEntityTypeDiscriminants::CardanoTransactions + ), + RequiredAggregatorCapabilities::SignedEntityType( + SignedEntityTypeDiscriminants::CardanoStakeDistribution + ), + ]), + RequiredAggregatorCapabilities::Or(vec![ + RequiredAggregatorCapabilities::AggregateSignatureType( + AggregateSignatureType::Concatenation + ), + ]), + ]) + ); + } +} diff --git a/mithril-client-cli/src/commands/tools/mod.rs b/mithril-client-cli/src/commands/tools/mod.rs index 65fefa1d54e..181b362d81b 100644 --- a/mithril-client-cli/src/commands/tools/mod.rs +++ b/mithril-client-cli/src/commands/tools/mod.rs @@ -3,8 +3,10 @@ //! Provides utility subcommands such as converting restored InMemory UTxO-HD ledger snapshot //! to different flavors (Legacy, LMDB). +mod aggregator_discovery; mod snapshot_converter; +pub use aggregator_discovery::*; pub use snapshot_converter::*; use anyhow::anyhow; @@ -18,6 +20,9 @@ pub enum ToolsCommands { /// UTxO-HD related commands #[clap(subcommand, name = "utxo-hd")] UTxOHD(UTxOHDCommands), + /// Aggregator discovery related commands + #[clap(subcommand, name = "aggregator-discovery")] + AggregatorDiscovery(AggregatorDiscoveryCommands), } impl ToolsCommands { @@ -25,6 +30,7 @@ impl ToolsCommands { pub async fn execute(&self) -> MithrilResult<()> { match self { Self::UTxOHD(cmd) => cmd.execute().await, + Self::AggregatorDiscovery(cmd) => cmd.execute().await, } } } @@ -52,3 +58,20 @@ impl UTxOHDCommands { } } } + +/// Aggregator discovery related commands +#[derive(Subcommand, Debug, Clone)] +pub enum AggregatorDiscoveryCommands { + /// Select an aggregator from the available ones with automatic discovery + #[clap(arg_required_else_help = false)] + Select(AggregatorSelectCommand), +} + +impl AggregatorDiscoveryCommands { + /// Execute Aggregator discovery command + pub async fn execute(&self) -> MithrilResult<()> { + match self { + Self::Select(cmd) => cmd.execute().await, + } + } +} diff --git a/mithril-client/src/client.rs b/mithril-client/src/client.rs index 7e683cfbf9e..9650e54e39e 100644 --- a/mithril-client/src/client.rs +++ b/mithril-client/src/client.rs @@ -9,8 +9,8 @@ use std::collections::HashMap; use std::sync::Arc; use mithril_aggregator_discovery::{ - AggregatorDiscoverer, CapableAggregatorDiscoverer, HttpConfigAggregatorDiscoverer, - MithrilNetwork, RequiredAggregatorCapabilities, + AggregatorDiscoverer, AggregatorEndpoint, CapableAggregatorDiscoverer, + HttpConfigAggregatorDiscoverer, MithrilNetwork, RequiredAggregatorCapabilities, }; use mithril_common::api_version::APIVersionProvider; use mithril_common::{MITHRIL_CLIENT_TYPE_HEADER, MITHRIL_ORIGIN_TAG_HEADER}; @@ -289,8 +289,7 @@ impl ClientBuilder { None => { return Err(anyhow!( "The genesis verification key must be provided to build the client with the 'with_genesis_verification_key' function" - ) - .into()); + )); } }; @@ -400,40 +399,47 @@ impl ClientBuilder { }) } + /// Discover available aggregator endpoints for the given Mithril network and required capabilities. + pub fn discover_aggregator( + &self, + network: &MithrilNetwork, + ) -> MithrilResult> { + match self.aggregator_discoverer.clone() { + Some(discoverer) => { + let discoverer = if let Some(capabilities) = &self.aggregator_capabilities { + Arc::new(CapableAggregatorDiscoverer::new( + capabilities.to_owned(), + discoverer.clone(), + )) as Arc + } else { + discoverer as Arc + }; + tokio::task::block_in_place(move || { + tokio::runtime::Handle::current().block_on(async move { + discoverer + .get_available_aggregators(network.to_owned()) + .await + .with_context(|| "Discovering aggregator endpoint failed") + }) + }) + } + None => Err(anyhow!( + "The aggregator discoverer must be provided to build the client with automatic discovery using the 'with_aggregator_discoverer' function" + )), + } + } + fn build_aggregator_client( &self, logger: Logger, ) -> Result { let aggregator_endpoint = match self.aggregator_discovery { AggregatorDiscoveryType::Url(ref url) => url.clone(), - AggregatorDiscoveryType::Automatic(ref network) => { - match self.aggregator_discoverer.clone() { - Some(discoverer) => { - let discoverer = if let Some(capabilities) = &self.aggregator_capabilities { - Arc::new(CapableAggregatorDiscoverer::new( - capabilities.to_owned(), - discoverer.clone(), - )) as Arc - } else { - discoverer as Arc - }; - tokio::task::block_in_place(move || { - tokio::runtime::Handle::current().block_on(async move { - discoverer - .get_available_aggregators(network.to_owned()) - .await - .with_context(|| "Discovering aggregator endpoint failed")? - .next() - .ok_or(anyhow!("No aggregator was available through discovery")) - }) - })? - .into() - } - None => { - return Err(anyhow!("The aggregator discoverer must be provided to build the client with automatic discovery using the 'with_aggregator_discoverer' function").into()); - } - } - } + AggregatorDiscoveryType::Automatic(ref network) => self + .discover_aggregator(network)? + .next() + .ok_or_else(|| anyhow!("No aggregator was available through discovery"))? + .into(), }; let endpoint_url = Url::parse(&aggregator_endpoint).with_context(|| { format!("Invalid aggregator endpoint, it must be a correctly formed url: '{aggregator_endpoint}'") diff --git a/mithril-client/src/type_alias.rs b/mithril-client/src/type_alias.rs index a59082c1ece..6b0200c1b6f 100644 --- a/mithril-client/src/type_alias.rs +++ b/mithril-client/src/type_alias.rs @@ -89,3 +89,6 @@ pub mod common { /// Required capabilities for an aggregator. pub use mithril_aggregator_discovery::RequiredAggregatorCapabilities; + +/// Mithril network +pub use mithril_aggregator_discovery::MithrilNetwork; diff --git a/mithril-stm/src/aggregate_signature/signature.rs b/mithril-stm/src/aggregate_signature/signature.rs index dc8c6fb3a77..00f3e1b7e4e 100644 --- a/mithril-stm/src/aggregate_signature/signature.rs +++ b/mithril-stm/src/aggregate_signature/signature.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::fmt::Display; use std::hash::Hash; +use std::str::FromStr; use anyhow::anyhow; use blake2::digest::{Digest, FixedOutput}; @@ -8,7 +9,7 @@ use serde::{Deserialize, Serialize}; use crate::error::AggregateSignatureError; use crate::merkle_tree::MerkleBatchPath; -use crate::{AggregateVerificationKey, Parameters, StmResult}; +use crate::{AggregateVerificationKey, Parameters, StmError, StmResult}; use super::ConcatenationProof; @@ -60,6 +61,19 @@ impl From<&AggregateSignature> } } +impl FromStr for AggregateSignatureType { + type Err = StmError; + + fn from_str(s: &str) -> Result { + match s { + "Concatenation" => Ok(AggregateSignatureType::Concatenation), + #[cfg(feature = "future_proof_system")] + "Future" => Ok(AggregateSignatureType::Future), + _ => Err(anyhow!("Unknown aggregate signature type: {}", s)), + } + } +} + impl Display for AggregateSignatureType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { From 6a338b7622d1e78187d6217f05eb2ccbf2ff5080 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Tue, 25 Nov 2025 16:57:40 +0100 Subject: [PATCH 23/31] chore: apply review comments --- README.md | 5 +---- internal/mithril-aggregator-discovery/Cargo.toml | 2 +- .../src/capabilities_discoverer.rs | 1 - internal/mithril-aggregator-discovery/src/interface.rs | 2 +- .../src/commands/tools/aggregator_discovery.rs | 6 +++--- 5 files changed, 6 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5d0e9b5f950..d967f8f32a1 100644 --- a/README.md +++ b/README.md @@ -80,10 +80,9 @@ This repository consists of the following parts: - [**Mithril signer**](./mithril-signer): the node of the **Mithril network** responsible for producing individual signatures that are collected and aggregated by the **Mithril aggregator**. - [**Internal**](./internal): the shared tools and API used by **Mithril** crates. - - [**Mithril aggregator client**](./internal/mithril-aggregator-client): a client to request data from a Mithril aggregator, used by **Mithril network** nodes and client library. - - [**Mithril aggregator discovery**](./internal/mithril-aggregator-discovery): mechanisms to discover available Mithril aggregator, used by **Mithril network** nodes and client library. + - [**Mithril aggregator discovery**](./internal/mithril-aggregator-discovery): mechanisms to discover available Mithril aggregators, used by **Mithril network** nodes and client library. - [**Mithril build script**](./internal/mithril-build-script): a toolbox for Mithril crates that uses a build script phase. @@ -116,13 +115,11 @@ This repository consists of the following parts: - [**Mithril signed entity prealoader**](./internal/signed-entity/mithril-signed-entity-preloader): a **preload** mechanism for the Cardano transaction signed entity, used by **Mithril network** nodes. - [**tests**](./internal/tests): shared testing tools used by **Mithril** crates. - - [**Mithril api spec**](./internal/tests/mithril-api-spec): toolset to verify conformity of http routes against an Open Api specification, used by **Mithril network** nodes. - [**Mithril test http server**](internal/tests/mithril-test-http-server): provides a test http server, used by **Mithril network** nodes. - [**Mithril test lab**](./mithril-test-lab): the suite of tools that allow us to test and stress the **Mithril** protocol implementations. - - [**Mithril devnet**](./mithril-test-lab/mithril-devnet): the private **Mithril/Cardano network** used to scaffold a **Mithril network** on top of a **Cardano network**. - [**Mithril end to end**](./mithril-test-lab/mithril-end-to-end): the tool used to run test scenarios against a **Mithril devnet**. diff --git a/internal/mithril-aggregator-discovery/Cargo.toml b/internal/mithril-aggregator-discovery/Cargo.toml index f6106fc4f47..cdc8ccf1315 100644 --- a/internal/mithril-aggregator-discovery/Cargo.toml +++ b/internal/mithril-aggregator-discovery/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-aggregator-discovery" -description = "Mechanisms to discover aggregator available in a Mithril network." +description = "Mechanisms to discover aggregators available in a Mithril network." version = "0.1.0" authors.workspace = true documentation.workspace = true diff --git a/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs b/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs index dad9d609b6e..0f7b6c91619 100644 --- a/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs +++ b/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs @@ -16,7 +16,6 @@ pub enum RequiredAggregatorCapabilities { SignedEntityType(SignedEntityTypeDiscriminants), /// Aggregate signature type. AggregateSignatureType(AggregateSignatureType), - /// /// Logical OR of required capabilities. Or(Vec), /// Logical AND of required capabilities. diff --git a/internal/mithril-aggregator-discovery/src/interface.rs b/internal/mithril-aggregator-discovery/src/interface.rs index 61b725a12f1..df6f9e99535 100644 --- a/internal/mithril-aggregator-discovery/src/interface.rs +++ b/internal/mithril-aggregator-discovery/src/interface.rs @@ -10,7 +10,7 @@ use crate::model::{AggregatorEndpoint, MithrilNetwork}; pub trait AggregatorDiscoverer: Sync + Send { /// Get an iterator over a list of available aggregators in a Mithril network. /// - /// Note: there is no guarantee that the returned aggregators is sorted, complete or up-to-date. + /// Note: there is no guarantee that the returned aggregators are sorted, complete or up-to-date. async fn get_available_aggregators( &self, network: MithrilNetwork, diff --git a/mithril-client-cli/src/commands/tools/aggregator_discovery.rs b/mithril-client-cli/src/commands/tools/aggregator_discovery.rs index c771f57dca3..62f26b240ba 100644 --- a/mithril-client-cli/src/commands/tools/aggregator_discovery.rs +++ b/mithril-client-cli/src/commands/tools/aggregator_discovery.rs @@ -9,7 +9,7 @@ use mithril_client::{ /// Clap command to select an aggregator from the available ones with automatic discovery. #[derive(Parser, Debug, Clone)] pub struct AggregatorSelectCommand { - /// Path to the Cardano node database directory. + /// Mithril network name #[clap(long)] network: MithrilNetwork, @@ -90,9 +90,9 @@ impl AggregatorSelectCommand { )); } if required_capabilities.len() == 1 { - return required_capabilities.into_iter().next().unwrap(); + required_capabilities.into_iter().next().unwrap() } else { - return RequiredAggregatorCapabilities::And(required_capabilities); + RequiredAggregatorCapabilities::And(required_capabilities) } } } From 98646a4329b4e1aee00a2080e3e1a1d5f180deff Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Tue, 25 Nov 2025 17:14:39 +0100 Subject: [PATCH 24/31] refactor: move 'MithrilNetwork' to common --- Cargo.lock | 1 + .../src/capabilities_discoverer.rs | 5 +-- .../src/http_config_discoverer.rs | 6 ++-- .../src/interface.rs | 4 +-- .../mithril-aggregator-discovery/src/lib.rs | 2 +- .../mithril-aggregator-discovery/src/model.rs | 28 ---------------- .../src/rand_discoverer.rs | 8 ++--- .../src/test/double/discoverer.rs | 4 +-- mithril-client/Cargo.toml | 1 + mithril-client/src/client.rs | 11 +++++-- mithril-client/src/type_alias.rs | 2 +- .../src/entities/mithril_network.rs | 32 +++++++++++++++++++ mithril-common/src/entities/mod.rs | 2 ++ 13 files changed, 60 insertions(+), 46 deletions(-) create mode 100644 mithril-common/src/entities/mithril_network.rs diff --git a/Cargo.lock b/Cargo.lock index 0683c520c04..cd2dc043b08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3948,6 +3948,7 @@ dependencies = [ "mithril-cardano-node-internal-database", "mithril-common", "mockall", + "rand 0.9.2", "reqwest", "semver", "serde", diff --git a/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs b/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs index 0f7b6c91619..cb76b253e36 100644 --- a/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs +++ b/internal/mithril-aggregator-discovery/src/capabilities_discoverer.rs @@ -1,11 +1,12 @@ use std::sync::Arc; use mithril_common::{ - AggregateSignatureType, StdResult, entities::SignedEntityTypeDiscriminants, + AggregateSignatureType, StdResult, + entities::{MithrilNetwork, SignedEntityTypeDiscriminants}, messages::AggregatorCapabilities, }; -use crate::{AggregatorDiscoverer, AggregatorEndpoint, MithrilNetwork}; +use crate::{AggregatorDiscoverer, AggregatorEndpoint}; /// Required capabilities for an aggregator. #[derive(Clone, PartialEq, Eq, Debug)] diff --git a/internal/mithril-aggregator-discovery/src/http_config_discoverer.rs b/internal/mithril-aggregator-discovery/src/http_config_discoverer.rs index 34ba0622329..80faaafa9b6 100644 --- a/internal/mithril-aggregator-discovery/src/http_config_discoverer.rs +++ b/internal/mithril-aggregator-discovery/src/http_config_discoverer.rs @@ -4,9 +4,9 @@ use anyhow::Context; use reqwest::Client; use serde::{Deserialize, Serialize}; -use mithril_common::StdResult; +use mithril_common::{StdResult, entities::MithrilNetwork}; -use crate::{AggregatorDiscoverer, AggregatorEndpoint, MithrilNetwork}; +use crate::{AggregatorDiscoverer, AggregatorEndpoint}; const DEFAULT_REMOTE_NETWORKS_CONFIG_URL: &str = "https://raw.githubusercontent.com/input-output-hk/mithril/main/networks.json"; @@ -47,7 +47,7 @@ pub struct HttpConfigAggregatorDiscoverer { impl HttpConfigAggregatorDiscoverer { const HTTP_TIMEOUT: Duration = Duration::from_secs(10); - /// Creates a new `HttpConfigAggregatorDiscoverer` instance with the provided results. + /// Creates a new `HttpConfigAggregatorDiscoverer` instance with the configuration file URL. pub fn new(configuration_file_url: &str) -> Self { Self { configuration_file_url: configuration_file_url.to_string(), diff --git a/internal/mithril-aggregator-discovery/src/interface.rs b/internal/mithril-aggregator-discovery/src/interface.rs index df6f9e99535..83980888844 100644 --- a/internal/mithril-aggregator-discovery/src/interface.rs +++ b/internal/mithril-aggregator-discovery/src/interface.rs @@ -1,8 +1,8 @@ //! Interface definition for Mithril Protocol Configuration provider. -use mithril_common::StdResult; +use mithril_common::{StdResult, entities::MithrilNetwork}; -use crate::model::{AggregatorEndpoint, MithrilNetwork}; +use crate::model::AggregatorEndpoint; /// An aggregator discoverer. #[cfg_attr(test, mockall::automock)] diff --git a/internal/mithril-aggregator-discovery/src/lib.rs b/internal/mithril-aggregator-discovery/src/lib.rs index 077c0fbf01a..19657ff9b5e 100644 --- a/internal/mithril-aggregator-discovery/src/lib.rs +++ b/internal/mithril-aggregator-discovery/src/lib.rs @@ -11,5 +11,5 @@ pub mod test; pub use capabilities_discoverer::{CapableAggregatorDiscoverer, RequiredAggregatorCapabilities}; pub use http_config_discoverer::HttpConfigAggregatorDiscoverer; pub use interface::AggregatorDiscoverer; -pub use model::{AggregatorEndpoint, MithrilNetwork}; +pub use model::AggregatorEndpoint; pub use rand_discoverer::ShuffleAggregatorDiscoverer; diff --git a/internal/mithril-aggregator-discovery/src/model.rs b/internal/mithril-aggregator-discovery/src/model.rs index 6f6f27666d4..7896933f6d4 100644 --- a/internal/mithril-aggregator-discovery/src/model.rs +++ b/internal/mithril-aggregator-discovery/src/model.rs @@ -3,34 +3,6 @@ use std::time::Duration; use mithril_aggregator_client::{AggregatorHttpClient, query::GetAggregatorFeaturesQuery}; use mithril_common::{StdResult, messages::AggregatorCapabilities}; -/// Representation of a Mithril network -// TODO: to move to mithril common -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MithrilNetwork(String); - -impl MithrilNetwork { - /// Create a new MithrilNetwork instance - pub fn new(name: String) -> Self { - Self(name) - } - - /// Create a dummy MithrilNetwork instance for testing purposes - pub fn dummy() -> Self { - Self("dummy".to_string()) - } - - /// Retrieve the name of the Mithril network - pub fn name(&self) -> &str { - &self.0 - } -} - -impl From for MithrilNetwork { - fn from(name: String) -> Self { - MithrilNetwork::new(name) - } -} - /// Representation of an aggregator endpoint #[derive(Debug, Clone, PartialEq, Eq)] pub struct AggregatorEndpoint { diff --git a/internal/mithril-aggregator-discovery/src/rand_discoverer.rs b/internal/mithril-aggregator-discovery/src/rand_discoverer.rs index a967ae8b127..a616d15dcff 100644 --- a/internal/mithril-aggregator-discovery/src/rand_discoverer.rs +++ b/internal/mithril-aggregator-discovery/src/rand_discoverer.rs @@ -3,13 +3,13 @@ use std::sync::Arc; use rand::{Rng, seq::SliceRandom}; use tokio::sync::Mutex; -use mithril_common::StdResult; +use mithril_common::{StdResult, entities::MithrilNetwork}; -use crate::{AggregatorDiscoverer, AggregatorEndpoint, MithrilNetwork}; +use crate::{AggregatorDiscoverer, AggregatorEndpoint}; /// A discoverer that returns a random set of aggregators pub struct ShuffleAggregatorDiscoverer { - random_generator: Arc>, + random_generator: Arc>>, inner_discoverer: Arc, } @@ -18,7 +18,7 @@ impl ShuffleAggregatorDiscoverer { pub fn new(inner_discoverer: Arc, random_generator: R) -> Self { Self { inner_discoverer, - random_generator: Arc::new(Mutex::new(random_generator)), + random_generator: Arc::new(Mutex::new(Box::new(random_generator))), } } } diff --git a/internal/mithril-aggregator-discovery/src/test/double/discoverer.rs b/internal/mithril-aggregator-discovery/src/test/double/discoverer.rs index 808fb441174..e4807d5d999 100644 --- a/internal/mithril-aggregator-discovery/src/test/double/discoverer.rs +++ b/internal/mithril-aggregator-discovery/src/test/double/discoverer.rs @@ -2,9 +2,9 @@ use std::collections::VecDeque; use tokio::sync::Mutex; -use mithril_common::StdResult; +use mithril_common::{StdResult, entities::MithrilNetwork}; -use crate::{AggregatorDiscoverer, AggregatorEndpoint, MithrilNetwork}; +use crate::{AggregatorDiscoverer, AggregatorEndpoint}; type AggregatorListReturn = StdResult>; diff --git a/mithril-client/Cargo.toml b/mithril-client/Cargo.toml index 379ef92a99f..d3409bbf3a6 100644 --- a/mithril-client/Cargo.toml +++ b/mithril-client/Cargo.toml @@ -60,6 +60,7 @@ flume = { version = "0.11.1", optional = true } futures = "0.3.31" mithril-common = { path = "../mithril-common", version = ">=0.6", default-features = false } mithril-aggregator-discovery = { path = "../internal/mithril-aggregator-discovery" } +rand = { version = "0.9.2"} reqwest = { workspace = true, default-features = false, features = [ "charset", "http2", diff --git a/mithril-client/src/client.rs b/mithril-client/src/client.rs index 9650e54e39e..a1c784412f8 100644 --- a/mithril-client/src/client.rs +++ b/mithril-client/src/client.rs @@ -2,6 +2,7 @@ use anyhow::{Context, anyhow}; #[cfg(feature = "fs")] use chrono::Utc; +use mithril_common::entities::MithrilNetwork; use reqwest::Url; use serde::{Deserialize, Serialize}; use slog::{Logger, o}; @@ -10,7 +11,7 @@ use std::sync::Arc; use mithril_aggregator_discovery::{ AggregatorDiscoverer, AggregatorEndpoint, CapableAggregatorDiscoverer, - HttpConfigAggregatorDiscoverer, MithrilNetwork, RequiredAggregatorCapabilities, + HttpConfigAggregatorDiscoverer, RequiredAggregatorCapabilities, ShuffleAggregatorDiscoverer, }; use mithril_common::api_version::APIVersionProvider; use mithril_common::{MITHRIL_CLIENT_TYPE_HEADER, MITHRIL_ORIGIN_TAG_HEADER}; @@ -269,7 +270,11 @@ impl ClientBuilder { /// Sets the default aggregator discoverer to use to find the aggregator endpoint when in automatic discovery. pub fn with_default_aggregator_discoverer(mut self) -> ClientBuilder { - self.aggregator_discoverer = Some(Arc::new(HttpConfigAggregatorDiscoverer::default())); + /* self.aggregator_discoverer = Some(Arc::new(HttpConfigAggregatorDiscoverer::default())); */ + self.aggregator_discoverer = Some(Arc::new(ShuffleAggregatorDiscoverer::new( + Arc::new(HttpConfigAggregatorDiscoverer::default()), + Arc::new(rand::rand_core::OsRng) as Arc, // Explicitly cast to trait object + )?)); self } @@ -408,7 +413,7 @@ impl ClientBuilder { Some(discoverer) => { let discoverer = if let Some(capabilities) = &self.aggregator_capabilities { Arc::new(CapableAggregatorDiscoverer::new( - capabilities.to_owned(), + capabilities to_owned(), discoverer.clone(), )) as Arc } else { diff --git a/mithril-client/src/type_alias.rs b/mithril-client/src/type_alias.rs index 6b0200c1b6f..a01f71b8f97 100644 --- a/mithril-client/src/type_alias.rs +++ b/mithril-client/src/type_alias.rs @@ -91,4 +91,4 @@ pub mod common { pub use mithril_aggregator_discovery::RequiredAggregatorCapabilities; /// Mithril network -pub use mithril_aggregator_discovery::MithrilNetwork; +pub use mithril_common::entities::MithrilNetwork; diff --git a/mithril-common/src/entities/mithril_network.rs b/mithril-common/src/entities/mithril_network.rs new file mode 100644 index 00000000000..1e40335054d --- /dev/null +++ b/mithril-common/src/entities/mithril_network.rs @@ -0,0 +1,32 @@ +/// Representation of a Mithril network +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MithrilNetwork(String); + +impl MithrilNetwork { + /// Create a new MithrilNetwork instance + pub fn new(name: String) -> Self { + Self(name) + } + + /// Create a dummy MithrilNetwork instance for testing purposes + pub fn dummy() -> Self { + Self("dummy".to_string()) + } + + /// Retrieve the name of the Mithril network + pub fn name(&self) -> &str { + &self.0 + } +} + +impl From for MithrilNetwork { + fn from(name: String) -> Self { + MithrilNetwork::new(name) + } +} + +impl From<&str> for MithrilNetwork { + fn from(name: &str) -> Self { + MithrilNetwork::new(name.to_string()) + } +} diff --git a/mithril-common/src/entities/mod.rs b/mithril-common/src/entities/mod.rs index 0356c3ed83a..07f46586449 100644 --- a/mithril-common/src/entities/mod.rs +++ b/mithril-common/src/entities/mod.rs @@ -17,6 +17,7 @@ mod compression_algorithm; mod epoch; mod file_uri; mod http_server_error; +mod mithril_network; mod mithril_stake_distribution; mod protocol_message; mod protocol_parameters; @@ -51,6 +52,7 @@ pub use compression_algorithm::*; pub use epoch::{Epoch, EpochError, EpochSpecifier}; pub use file_uri::{FileUri, MultiFilesUri, TemplateUri}; pub use http_server_error::{ClientError, ServerError}; +pub use mithril_network::MithrilNetwork; pub use mithril_stake_distribution::MithrilStakeDistribution; pub use protocol_message::{ProtocolMessage, ProtocolMessagePartKey, ProtocolMessagePartValue}; pub use protocol_parameters::ProtocolParameters; From 8022c746fc4a7aa8a7f0451ee038d3f30585ebef Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Tue, 25 Nov 2025 18:41:24 +0100 Subject: [PATCH 25/31] refactor(client): support for default aggregator discoverer with shuffling --- .../commands/tools/aggregator_discovery.rs | 3 +- mithril-client/src/client.rs | 72 ++++++++++--------- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/mithril-client-cli/src/commands/tools/aggregator_discovery.rs b/mithril-client-cli/src/commands/tools/aggregator_discovery.rs index 62f26b240ba..dc08a57b494 100644 --- a/mithril-client-cli/src/commands/tools/aggregator_discovery.rs +++ b/mithril-client-cli/src/commands/tools/aggregator_discovery.rs @@ -36,8 +36,7 @@ impl AggregatorSelectCommand { let required_capabilities = self.build_required_capabilities(); let client_builder = ClientBuilder::new(AggregatorDiscoveryType::Automatic(self.network.clone())) - .with_capabilities(required_capabilities) - .with_default_aggregator_discoverer(); + .with_capabilities(required_capabilities); let aggregator_endpoints = client_builder .discover_aggregator(&self.network)? .take(self.max_entries); diff --git a/mithril-client/src/client.rs b/mithril-client/src/client.rs index a1c784412f8..c04ff647b3e 100644 --- a/mithril-client/src/client.rs +++ b/mithril-client/src/client.rs @@ -3,6 +3,8 @@ use anyhow::{Context, anyhow}; use chrono::Utc; use mithril_common::entities::MithrilNetwork; +use rand::SeedableRng; +use rand::rngs::StdRng; use reqwest::Url; use serde::{Deserialize, Serialize}; use slog::{Logger, o}; @@ -211,7 +213,20 @@ impl ClientBuilder { .with_genesis_verification_key(GenesisVerificationKey::JsonHex( genesis_verification_key.to_string(), )) - .with_default_aggregator_discoverer() + } + + /// Default aggregator discoverer to use to find the aggregator endpoint when in automatic discovery. + fn default_aggregator_discoverer() -> Arc { + Arc::new(ShuffleAggregatorDiscoverer::new( + Arc::new(HttpConfigAggregatorDiscoverer::default()), + { + let mut seed = [0u8; 32]; + let timestamp = Utc::now().timestamp_nanos_opt().unwrap_or(0); + seed[..8].copy_from_slice(×tamp.to_le_bytes()); + + StdRng::from_seed(seed) + }, + )) } /// Constructs a new `ClientBuilder` without any dependency set. @@ -268,17 +283,6 @@ impl ClientBuilder { self } - /// Sets the default aggregator discoverer to use to find the aggregator endpoint when in automatic discovery. - pub fn with_default_aggregator_discoverer(mut self) -> ClientBuilder { - /* self.aggregator_discoverer = Some(Arc::new(HttpConfigAggregatorDiscoverer::default())); */ - self.aggregator_discoverer = Some(Arc::new(ShuffleAggregatorDiscoverer::new( - Arc::new(HttpConfigAggregatorDiscoverer::default()), - Arc::new(rand::rand_core::OsRng) as Arc, // Explicitly cast to trait object - )?)); - - self - } - /// Returns a `Client` that uses the dependencies provided to this `ClientBuilder`. /// /// The builder will try to create the missing dependencies using default implementations @@ -409,29 +413,27 @@ impl ClientBuilder { &self, network: &MithrilNetwork, ) -> MithrilResult> { - match self.aggregator_discoverer.clone() { - Some(discoverer) => { - let discoverer = if let Some(capabilities) = &self.aggregator_capabilities { - Arc::new(CapableAggregatorDiscoverer::new( - capabilities to_owned(), - discoverer.clone(), - )) as Arc - } else { - discoverer as Arc - }; - tokio::task::block_in_place(move || { - tokio::runtime::Handle::current().block_on(async move { - discoverer - .get_available_aggregators(network.to_owned()) - .await - .with_context(|| "Discovering aggregator endpoint failed") - }) - }) - } - None => Err(anyhow!( - "The aggregator discoverer must be provided to build the client with automatic discovery using the 'with_aggregator_discoverer' function" - )), - } + let discoverer = self + .aggregator_discoverer + .clone() + .unwrap_or_else(|| Self::default_aggregator_discoverer()); + let discoverer = if let Some(capabilities) = &self.aggregator_capabilities { + Arc::new(CapableAggregatorDiscoverer::new( + capabilities.to_owned(), + discoverer.clone(), + )) as Arc + } else { + discoverer as Arc + }; + + tokio::task::block_in_place(move || { + tokio::runtime::Handle::current().block_on(async move { + discoverer + .get_available_aggregators(network.to_owned()) + .await + .with_context(|| "Discovering aggregator endpoint failed") + }) + }) } fn build_aggregator_client( From 083660f50b88e2e23383b9b2a4265525ace734d7 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Tue, 25 Nov 2025 18:58:51 +0100 Subject: [PATCH 26/31] chore(aggregator-discovery): fix Cargo.toml file --- Cargo.lock | 3 --- internal/mithril-aggregator-discovery/Cargo.toml | 9 +++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cd2dc043b08..1a794b89465 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3815,11 +3815,8 @@ dependencies = [ "reqwest", "serde", "serde_json", - "slog", "slog-async", - "slog-scope", "slog-term", - "thiserror 2.0.17", "tokio", ] diff --git a/internal/mithril-aggregator-discovery/Cargo.toml b/internal/mithril-aggregator-discovery/Cargo.toml index cdc8ccf1315..489c17b1fc4 100644 --- a/internal/mithril-aggregator-discovery/Cargo.toml +++ b/internal/mithril-aggregator-discovery/Cargo.toml @@ -13,9 +13,9 @@ include = ["**/*.rs", "Cargo.toml", "README.md", ".gitignore"] [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } -mithril-common = { path = "../../mithril-common" } mithril-aggregator-client = { path = "../mithril-aggregator-client" } -rand = { version = "0.9.2"} +mithril-common = { path = "../../mithril-common" } +rand = { version = "0.9.2" } reqwest = { workspace = true, features = [ "default", "gzip", @@ -25,14 +25,11 @@ reqwest = { workspace = true, features = [ ] } serde = { workspace = true } serde_json = { workspace = true } -slog = { workspace = true } -slog-scope = "4.4.0" -thiserror = { workspace = true } tokio = { workspace = true, features = ["sync", "rt-multi-thread"] } [dev-dependencies] -mockall = { workspace = true } httpmock = "0.8.1" +mockall = { workspace = true } slog-async = { workspace = true } slog-term = { workspace = true } tokio = { workspace = true, features = ["macros"] } From 7747626636a66d182c8b39640eec322251c88d2c Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Fri, 28 Nov 2025 16:05:40 +0100 Subject: [PATCH 27/31] fix(client): aggregator discovery not available in wasm --- mithril-client/Cargo.toml | 4 ++-- mithril-client/src/client.rs | 19 +++++++++++++++++-- mithril-client/src/type_alias.rs | 1 + 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/mithril-client/Cargo.toml b/mithril-client/Cargo.toml index d3409bbf3a6..f49b527f2c1 100644 --- a/mithril-client/Cargo.toml +++ b/mithril-client/Cargo.toml @@ -59,8 +59,6 @@ flate2 = { version = "1.1.4", optional = true } flume = { version = "0.11.1", optional = true } futures = "0.3.31" mithril-common = { path = "../mithril-common", version = ">=0.6", default-features = false } -mithril-aggregator-discovery = { path = "../internal/mithril-aggregator-discovery" } -rand = { version = "0.9.2"} reqwest = { workspace = true, default-features = false, features = [ "charset", "http2", @@ -79,7 +77,9 @@ uuid = { version = "1.18.1", features = ["v4"] } zstd = { version = "0.13.3", optional = true } [target.'cfg(not(target_family = "wasm"))'.dependencies] +mithril-aggregator-discovery = { path = "../internal/mithril-aggregator-discovery" } mithril-cardano-node-internal-database = { path = "../internal/cardano-node/mithril-cardano-node-internal-database", version = "=0.1" } +rand = { version = "0.9.2" } [target.'cfg(target_family = "wasm")'.dependencies] getrandom = { version = "0.2.16", features = ["js"] } diff --git a/mithril-client/src/client.rs b/mithril-client/src/client.rs index c04ff647b3e..6d89cf2cb07 100644 --- a/mithril-client/src/client.rs +++ b/mithril-client/src/client.rs @@ -2,8 +2,11 @@ use anyhow::{Context, anyhow}; #[cfg(feature = "fs")] use chrono::Utc; +#[cfg(not(target_family = "wasm"))] use mithril_common::entities::MithrilNetwork; +#[cfg(not(target_family = "wasm"))] use rand::SeedableRng; +#[cfg(not(target_family = "wasm"))] use rand::rngs::StdRng; use reqwest::Url; use serde::{Deserialize, Serialize}; @@ -11,6 +14,7 @@ use slog::{Logger, o}; use std::collections::HashMap; use std::sync::Arc; +#[cfg(not(target_family = "wasm"))] use mithril_aggregator_discovery::{ AggregatorDiscoverer, AggregatorEndpoint, CapableAggregatorDiscoverer, HttpConfigAggregatorDiscoverer, RequiredAggregatorCapabilities, ShuffleAggregatorDiscoverer, @@ -50,10 +54,11 @@ const fn one_week_in_seconds() -> u32 { /// The type of discovery to use to find the aggregator to connect to. pub enum AggregatorDiscoveryType { - /// Automatically discover the aggregator. - Automatic(MithrilNetwork), /// Use a specific URL to connect to the aggregator. Url(String), + /// Automatically discover the aggregator. + #[cfg(not(target_family = "wasm"))] + Automatic(MithrilNetwork), } /// The genesis verification key. @@ -176,7 +181,9 @@ impl Client { /// Builder than can be used to create a [Client] easily or with custom dependencies. pub struct ClientBuilder { aggregator_discovery: AggregatorDiscoveryType, + #[cfg(not(target_family = "wasm"))] aggregator_capabilities: Option, + #[cfg(not(target_family = "wasm"))] aggregator_discoverer: Option>, genesis_verification_key: Option, origin_tag: Option, @@ -206,6 +213,7 @@ impl ClientBuilder { /// Constructs a new `ClientBuilder` that automatically discovers the aggregator for the given /// Mithril network and with the given genesis verification key. + #[cfg(not(target_family = "wasm"))] pub fn automatic(network: &str, genesis_verification_key: &str) -> ClientBuilder { Self::new(AggregatorDiscoveryType::Automatic(MithrilNetwork::new( network.to_string(), @@ -216,6 +224,7 @@ impl ClientBuilder { } /// Default aggregator discoverer to use to find the aggregator endpoint when in automatic discovery. + #[cfg(not(target_family = "wasm"))] fn default_aggregator_discoverer() -> Arc { Arc::new(ShuffleAggregatorDiscoverer::new( Arc::new(HttpConfigAggregatorDiscoverer::default()), @@ -233,7 +242,9 @@ impl ClientBuilder { pub fn new(aggregator_discovery: AggregatorDiscoveryType) -> ClientBuilder { Self { aggregator_discovery, + #[cfg(not(target_family = "wasm"))] aggregator_capabilities: None, + #[cfg(not(target_family = "wasm"))] aggregator_discoverer: None, genesis_verification_key: None, origin_tag: None, @@ -264,6 +275,7 @@ impl ClientBuilder { } /// Sets the aggregator capabilities expected to be matched by the aggregator with which the client will interact. + #[cfg(not(target_family = "wasm"))] pub fn with_capabilities( mut self, capabilities: RequiredAggregatorCapabilities, @@ -274,6 +286,7 @@ impl ClientBuilder { } /// Sets the aggregator discoverer to use to find the aggregator endpoint when in automatic discovery. + #[cfg(not(target_family = "wasm"))] pub fn with_aggregator_discoverer( mut self, discoverer: Arc, @@ -409,6 +422,7 @@ impl ClientBuilder { } /// Discover available aggregator endpoints for the given Mithril network and required capabilities. + #[cfg(not(target_family = "wasm"))] pub fn discover_aggregator( &self, network: &MithrilNetwork, @@ -442,6 +456,7 @@ impl ClientBuilder { ) -> Result { let aggregator_endpoint = match self.aggregator_discovery { AggregatorDiscoveryType::Url(ref url) => url.clone(), + #[cfg(not(target_family = "wasm"))] AggregatorDiscoveryType::Automatic(ref network) => self .discover_aggregator(network)? .next() diff --git a/mithril-client/src/type_alias.rs b/mithril-client/src/type_alias.rs index a01f71b8f97..b0883418c66 100644 --- a/mithril-client/src/type_alias.rs +++ b/mithril-client/src/type_alias.rs @@ -88,6 +88,7 @@ pub mod common { } /// Required capabilities for an aggregator. +#[cfg(not(target_family = "wasm"))] pub use mithril_aggregator_discovery::RequiredAggregatorCapabilities; /// Mithril network From e65c312035eabea679e14d34c735560cb386da88 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Fri, 28 Nov 2025 16:24:22 +0100 Subject: [PATCH 28/31] feat(client-cli): add json logs support --- .../commands/tools/aggregator_discovery.rs | 32 ++++++++++++++----- mithril-client-cli/src/commands/tools/mod.rs | 14 ++++---- mithril-client-cli/src/main.rs | 2 +- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/mithril-client-cli/src/commands/tools/aggregator_discovery.rs b/mithril-client-cli/src/commands/tools/aggregator_discovery.rs index dc08a57b494..ecff2b157d0 100644 --- a/mithril-client-cli/src/commands/tools/aggregator_discovery.rs +++ b/mithril-client-cli/src/commands/tools/aggregator_discovery.rs @@ -6,6 +6,11 @@ use mithril_client::{ common::{AggregateSignatureType, SignedEntityTypeDiscriminants}, }; +use crate::{ + CommandContext, + utils::{ProgressOutputType, ProgressPrinter}, +}; + /// Clap command to select an aggregator from the available ones with automatic discovery. #[derive(Parser, Debug, Clone)] pub struct AggregatorSelectCommand { @@ -32,7 +37,13 @@ pub struct AggregatorSelectCommand { impl AggregatorSelectCommand { /// Main command execution - pub async fn execute(&self) -> MithrilResult<()> { + pub async fn execute(&self, context: &CommandContext) -> MithrilResult<()> { + let progress_output_type = if context.is_json_output_enabled() { + ProgressOutputType::JsonReporter + } else { + ProgressOutputType::Tty + }; + let progress_printer = ProgressPrinter::new(progress_output_type, 1); let required_capabilities = self.build_required_capabilities(); let client_builder = ClientBuilder::new(AggregatorDiscoveryType::Automatic(self.network.clone())) @@ -40,18 +51,23 @@ impl AggregatorSelectCommand { let aggregator_endpoints = client_builder .discover_aggregator(&self.network)? .take(self.max_entries); - - println!( - "Discovering at most {} aggregator endpoints:", - self.max_entries, - ); + progress_printer.report_step( + 1, + &format!( + "Discovering at most {} aggregator endpoints:", + self.max_entries + ), + )?; let mut found_aggregators = 0; for endpoint in aggregator_endpoints { - println!("- Found: {endpoint}"); + progress_printer.report_step(1, &format!("Found: {endpoint}"))?; found_aggregators += 1; } if found_aggregators == 0 { - println!("- No aggregator endpoint found matching the requirements."); + progress_printer.report_step( + 1, + "- No aggregator endpoint found matching the requirements.", + )?; } Ok(()) diff --git a/mithril-client-cli/src/commands/tools/mod.rs b/mithril-client-cli/src/commands/tools/mod.rs index 181b362d81b..8564b7a7261 100644 --- a/mithril-client-cli/src/commands/tools/mod.rs +++ b/mithril-client-cli/src/commands/tools/mod.rs @@ -13,6 +13,8 @@ use anyhow::anyhow; use clap::Subcommand; use mithril_client::MithrilResult; +use crate::CommandContext; + /// Tools commands #[derive(Subcommand, Debug, Clone)] #[command(about = "Tools commands")] @@ -27,10 +29,10 @@ pub enum ToolsCommands { impl ToolsCommands { /// Execute Tools command - pub async fn execute(&self) -> MithrilResult<()> { + pub async fn execute(&self, context: CommandContext) -> MithrilResult<()> { match self { - Self::UTxOHD(cmd) => cmd.execute().await, - Self::AggregatorDiscovery(cmd) => cmd.execute().await, + Self::UTxOHD(cmd) => cmd.execute(context).await, + Self::AggregatorDiscovery(cmd) => cmd.execute(context).await, } } } @@ -45,7 +47,7 @@ pub enum UTxOHDCommands { impl UTxOHDCommands { /// Execute UTxO-HD command - pub async fn execute(&self) -> MithrilResult<()> { + pub async fn execute(&self, _context: CommandContext) -> MithrilResult<()> { match self { Self::SnapshotConverter(cmd) => { if cfg!(target_os = "linux") && cfg!(target_arch = "aarch64") { @@ -69,9 +71,9 @@ pub enum AggregatorDiscoveryCommands { impl AggregatorDiscoveryCommands { /// Execute Aggregator discovery command - pub async fn execute(&self) -> MithrilResult<()> { + pub async fn execute(&self, context: CommandContext) -> MithrilResult<()> { match self { - Self::Select(cmd) => cmd.execute().await, + Self::Select(cmd) => cmd.execute(&context).await, } } } diff --git a/mithril-client-cli/src/main.rs b/mithril-client-cli/src/main.rs index 3d201acfc97..6d77c98a582 100644 --- a/mithril-client-cli/src/main.rs +++ b/mithril-client-cli/src/main.rs @@ -250,7 +250,7 @@ impl ArtifactCommands { Self::GenerateDoc(cmd) => { cmd.execute(&mut Args::command()).map_err(|message| anyhow!(message)) } - Self::Tools(cmd) => cmd.execute().await, + Self::Tools(cmd) => cmd.execute(context).await, } } } From d547c0f3710675fa964c12b39e69652f48a0e68c Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Fri, 28 Nov 2025 16:38:36 +0100 Subject: [PATCH 29/31] feat(client-cli): make 'tools aggregator-discovery' command unstable --- mithril-client-cli/src/commands/tools/mod.rs | 21 ++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/mithril-client-cli/src/commands/tools/mod.rs b/mithril-client-cli/src/commands/tools/mod.rs index 8564b7a7261..e20d8bf63a1 100644 --- a/mithril-client-cli/src/commands/tools/mod.rs +++ b/mithril-client-cli/src/commands/tools/mod.rs @@ -22,7 +22,7 @@ pub enum ToolsCommands { /// UTxO-HD related commands #[clap(subcommand, name = "utxo-hd")] UTxOHD(UTxOHDCommands), - /// Aggregator discovery related commands + /// Aggregator discovery related commands (unstable) #[clap(subcommand, name = "aggregator-discovery")] AggregatorDiscovery(AggregatorDiscoveryCommands), } @@ -32,9 +32,26 @@ impl ToolsCommands { pub async fn execute(&self, context: CommandContext) -> MithrilResult<()> { match self { Self::UTxOHD(cmd) => cmd.execute(context).await, - Self::AggregatorDiscovery(cmd) => cmd.execute(context).await, + Self::AggregatorDiscovery(cmd) => { + if !context.is_unstable_enabled() { + Err(anyhow!(Self::unstable_flag_missing_message( + "aggregator discovery", + "tools" + ))) + } else { + cmd.execute(context).await + } + } } } + + fn unstable_flag_missing_message(sub_command: &str, command: &str) -> String { + format!( + "The \"{}\" subcommand is only accepted using the --unstable flag.\n\n\ + e.g.: \"mithril-client --unstable {} {}\"", + sub_command, command, sub_command + ) + } } /// UTxO-HD related commands From dafb26938ae9082ead0df01d51255980e5ed5468 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Fri, 28 Nov 2025 16:50:17 +0100 Subject: [PATCH 30/31] feat(client): deprecate 'aggregator' constructor --- .../client-cardano-database-v2/src/main.rs | 21 +++++++++----- .../src/main.rs | 18 ++++++++---- .../client-cardano-transaction/src/main.rs | 19 +++++++++---- .../src/main.rs | 18 ++++++++---- mithril-client-cli/src/commands/mod.rs | 28 +++++++++++-------- mithril-client/src/client.rs | 14 ++++++---- 6 files changed, 76 insertions(+), 42 deletions(-) diff --git a/examples/client-cardano-database-v2/src/main.rs b/examples/client-cardano-database-v2/src/main.rs index b7b43c49a1f..0aa1ea5f083 100644 --- a/examples/client-cardano-database-v2/src/main.rs +++ b/examples/client-cardano-database-v2/src/main.rs @@ -17,7 +17,10 @@ use std::time::Duration; use tokio::sync::RwLock; use mithril_client::feedback::{FeedbackReceiver, MithrilEvent, MithrilEventCardanoDatabase}; -use mithril_client::{ClientBuilder, MessageBuilder, MithrilError, MithrilResult}; +use mithril_client::{ + AggregatorDiscoveryType, ClientBuilder, GenesisVerificationKey, MessageBuilder, MithrilError, + MithrilResult, +}; #[derive(Parser, Debug)] #[command(version)] @@ -52,12 +55,16 @@ async fn main() -> MithrilResult<()> { let args = Args::parse(); let work_dir = get_temp_dir()?; let progress_bar = indicatif::MultiProgress::new(); - let client = - ClientBuilder::aggregator(&args.aggregator_endpoint, &args.genesis_verification_key) - .set_ancillary_verification_key(args.ancillary_verification_key) - .with_origin_tag(Some("EXAMPLE".to_string())) - .add_feedback_receiver(Arc::new(IndicatifFeedbackReceiver::new(&progress_bar))) - .build()?; + let client = ClientBuilder::new(AggregatorDiscoveryType::Url( + args.aggregator_endpoint.clone(), + )) + .set_genesis_verification_key(GenesisVerificationKey::JsonHex( + args.genesis_verification_key.clone(), + )) + .set_ancillary_verification_key(args.ancillary_verification_key) + .with_origin_tag(Some("EXAMPLE".to_string())) + .add_feedback_receiver(Arc::new(IndicatifFeedbackReceiver::new(&progress_bar))) + .build()?; let cardano_database_snapshots = client.cardano_database_v2().list().await?; diff --git a/examples/client-cardano-stake-distribution/src/main.rs b/examples/client-cardano-stake-distribution/src/main.rs index f1d4812001e..73f67d48193 100644 --- a/examples/client-cardano-stake-distribution/src/main.rs +++ b/examples/client-cardano-stake-distribution/src/main.rs @@ -7,7 +7,9 @@ use clap::Parser; use slog::info; use std::sync::Arc; -use mithril_client::{ClientBuilder, MessageBuilder, MithrilResult}; +use mithril_client::{ + AggregatorDiscoveryType, ClientBuilder, GenesisVerificationKey, MessageBuilder, MithrilResult, +}; #[derive(Parser, Debug)] #[command(version)] @@ -33,11 +35,15 @@ pub struct Args { async fn main() -> MithrilResult<()> { let args = Args::parse(); let logger = build_logger(); - let client = - ClientBuilder::aggregator(&args.aggregator_endpoint, &args.genesis_verification_key) - .with_origin_tag(Some("EXAMPLE".to_string())) - .with_logger(logger.clone()) - .build()?; + let client = ClientBuilder::new(AggregatorDiscoveryType::Url( + args.aggregator_endpoint.clone(), + )) + .set_genesis_verification_key(GenesisVerificationKey::JsonHex( + args.genesis_verification_key.clone(), + )) + .with_origin_tag(Some("EXAMPLE".to_string())) + .with_logger(logger.clone()) + .build()?; let cardano_stake_distributions = client.cardano_stake_distribution().list().await?; info!( diff --git a/examples/client-cardano-transaction/src/main.rs b/examples/client-cardano-transaction/src/main.rs index 32af5a03b5d..d05249becca 100644 --- a/examples/client-cardano-transaction/src/main.rs +++ b/examples/client-cardano-transaction/src/main.rs @@ -8,7 +8,10 @@ use slog::info; use std::sync::Arc; use mithril_client::common::TransactionHash; -use mithril_client::{ClientBuilder, MessageBuilder, MithrilResult, VerifiedCardanoTransactions}; +use mithril_client::{ + AggregatorDiscoveryType, ClientBuilder, GenesisVerificationKey, MessageBuilder, MithrilResult, + VerifiedCardanoTransactions, +}; #[derive(Parser, Debug)] #[command(version)] @@ -43,11 +46,15 @@ async fn main() -> MithrilResult<()> { .map(|s| s.as_str()) .collect::>(); let logger = build_logger(); - let client = - ClientBuilder::aggregator(&args.aggregator_endpoint, &args.genesis_verification_key) - .with_origin_tag(Some("EXAMPLE".to_string())) - .with_logger(logger.clone()) - .build()?; + let client = ClientBuilder::new(AggregatorDiscoveryType::Url( + args.aggregator_endpoint.clone(), + )) + .set_genesis_verification_key(GenesisVerificationKey::JsonHex( + args.genesis_verification_key.clone(), + )) + .with_origin_tag(Some("EXAMPLE".to_string())) + .with_logger(logger.clone()) + .build()?; info!(logger, "Fetching a proof for the given transactions...",); let cardano_transaction_proof = client diff --git a/examples/client-mithril-stake-distribution/src/main.rs b/examples/client-mithril-stake-distribution/src/main.rs index 71730a953a7..5923e3b26e9 100644 --- a/examples/client-mithril-stake-distribution/src/main.rs +++ b/examples/client-mithril-stake-distribution/src/main.rs @@ -7,7 +7,9 @@ use clap::Parser; use slog::info; use std::sync::Arc; -use mithril_client::{ClientBuilder, MessageBuilder, MithrilResult}; +use mithril_client::{ + AggregatorDiscoveryType, ClientBuilder, GenesisVerificationKey, MessageBuilder, MithrilResult, +}; #[derive(Parser, Debug)] #[command(version)] @@ -33,11 +35,15 @@ pub struct Args { async fn main() -> MithrilResult<()> { let args = Args::parse(); let logger = build_logger(); - let client = - ClientBuilder::aggregator(&args.aggregator_endpoint, &args.genesis_verification_key) - .with_origin_tag(Some("EXAMPLE".to_string())) - .with_logger(logger.clone()) - .build()?; + let client = ClientBuilder::new(AggregatorDiscoveryType::Url( + args.aggregator_endpoint.clone(), + )) + .set_genesis_verification_key(GenesisVerificationKey::JsonHex( + args.genesis_verification_key.clone(), + )) + .with_origin_tag(Some("EXAMPLE".to_string())) + .with_logger(logger.clone()) + .build()?; let mithril_stake_distributions = client.mithril_stake_distribution().list().await?; info!( diff --git a/mithril-client-cli/src/commands/mod.rs b/mithril-client-cli/src/commands/mod.rs index 69f2ff66853..22f6017121c 100644 --- a/mithril-client-cli/src/commands/mod.rs +++ b/mithril-client-cli/src/commands/mod.rs @@ -14,17 +14,21 @@ pub use deprecation::{DeprecatedCommand, Deprecation}; use std::sync::Arc; -use mithril_client::{ClientBuilder, MithrilResult}; +use mithril_client::{ + AggregatorDiscoveryType, ClientBuilder, GenesisVerificationKey, MithrilResult, +}; use crate::{configuration::ConfigParameters, utils::ForcedEraFetcher}; const CLIENT_TYPE_CLI: &str = "CLI"; pub(crate) fn client_builder(params: &ConfigParameters) -> MithrilResult { - let builder = ClientBuilder::aggregator( - ¶ms.require("aggregator_endpoint")?, - ¶ms.require("genesis_verification_key")?, - ); + let builder = ClientBuilder::new(AggregatorDiscoveryType::Url( + params.require("aggregator_endpoint")?, + )) + .set_genesis_verification_key(GenesisVerificationKey::JsonHex( + params.require("genesis_verification_key")?, + )); Ok(finalize_builder_config(builder, params)) } @@ -38,13 +42,13 @@ pub(crate) fn client_builder_with_fallback_genesis_key( 382c32322c35392c3230362c3130352c3233312c3135302c3231352c33302c37382c3231322c37362c31362c323\ 5322c3138302c37322c3133342c3133372c3234372c3136312c36385d"; - let builder = ClientBuilder::aggregator( - ¶ms.require("aggregator_endpoint")?, - ¶ms.get_or( - "genesis_verification_key", - fallback_genesis_verification_key, - ), - ); + let builder = ClientBuilder::new(AggregatorDiscoveryType::Url( + params.require("aggregator_endpoint")?, + )) + .set_genesis_verification_key(GenesisVerificationKey::JsonHex(params.get_or( + "genesis_verification_key", + fallback_genesis_verification_key, + ))); Ok(finalize_builder_config(builder, params)) } diff --git a/mithril-client/src/client.rs b/mithril-client/src/client.rs index 6d89cf2cb07..5dbc6471db7 100644 --- a/mithril-client/src/client.rs +++ b/mithril-client/src/client.rs @@ -178,7 +178,7 @@ impl Client { } } -/// Builder than can be used to create a [Client] easily or with custom dependencies. +/// Builder that can be used to create a [Client] easily or with custom dependencies. pub struct ClientBuilder { aggregator_discovery: AggregatorDiscoveryType, #[cfg(not(target_family = "wasm"))] @@ -205,8 +205,12 @@ pub struct ClientBuilder { impl ClientBuilder { /// Constructs a new `ClientBuilder` that fetches data from the aggregator at the given /// endpoint and with the given genesis verification key. + #[deprecated( + since = "0.12.36", + note = "Use `new` function instead and set the genesis verification key with `set_genesis_verification_key`" + )] pub fn aggregator(endpoint: &str, genesis_verification_key: &str) -> ClientBuilder { - Self::new(AggregatorDiscoveryType::Url(endpoint.to_string())).with_genesis_verification_key( + Self::new(AggregatorDiscoveryType::Url(endpoint.to_string())).set_genesis_verification_key( GenesisVerificationKey::JsonHex(genesis_verification_key.to_string()), ) } @@ -218,7 +222,7 @@ impl ClientBuilder { Self::new(AggregatorDiscoveryType::Automatic(MithrilNetwork::new( network.to_string(), ))) - .with_genesis_verification_key(GenesisVerificationKey::JsonHex( + .set_genesis_verification_key(GenesisVerificationKey::JsonHex( genesis_verification_key.to_string(), )) } @@ -265,7 +269,7 @@ impl ClientBuilder { } /// Sets the genesis verification key to use when verifying certificates. - pub fn with_genesis_verification_key( + pub fn set_genesis_verification_key( mut self, genesis_verification_key: GenesisVerificationKey, ) -> ClientBuilder { @@ -310,7 +314,7 @@ impl ClientBuilder { Some(GenesisVerificationKey::JsonHex(ref key)) => key, None => { return Err(anyhow!( - "The genesis verification key must be provided to build the client with the 'with_genesis_verification_key' function" + "The genesis verification key must be provided to build the client with the 'set_genesis_verification_key' function" )); } }; From 9a0ab5a3e79c371c0b157032ca5765c45ab649b8 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Fri, 28 Nov 2025 18:11:26 +0100 Subject: [PATCH 31/31] chore: apply review comments --- .../src/http_config_discoverer.rs | 5 +---- mithril-client/src/client.rs | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/internal/mithril-aggregator-discovery/src/http_config_discoverer.rs b/internal/mithril-aggregator-discovery/src/http_config_discoverer.rs index 80faaafa9b6..1ea33221dd3 100644 --- a/internal/mithril-aggregator-discovery/src/http_config_discoverer.rs +++ b/internal/mithril-aggregator-discovery/src/http_config_discoverer.rs @@ -56,10 +56,7 @@ impl HttpConfigAggregatorDiscoverer { /// Builds a reqwest HTTP client. fn build_client(&self) -> StdResult { - let client_builder = Client::builder().timeout(Self::HTTP_TIMEOUT); - let client = client_builder.build()?; - - Ok(client) + Ok(Client::builder().timeout(Self::HTTP_TIMEOUT).build()?) } } diff --git a/mithril-client/src/client.rs b/mithril-client/src/client.rs index 5dbc6471db7..abe741a9991 100644 --- a/mithril-client/src/client.rs +++ b/mithril-client/src/client.rs @@ -1,5 +1,5 @@ use anyhow::{Context, anyhow}; -#[cfg(feature = "fs")] +#[cfg(any(feature = "fs", not(target_family = "wasm")))] use chrono::Utc; #[cfg(not(target_family = "wasm"))]