use std::collections::HashMap;
#[cfg(not(target_family = "wasm"))]
use std::str::FromStr;
use std::sync::Arc;
use anyhow::{Context, anyhow};
#[cfg(any(feature = "fs", not(target_family = "wasm")))]
use chrono::Utc;
#[cfg(not(target_family = "wasm"))]
use rand::SeedableRng;
#[cfg(not(target_family = "wasm"))]
use rand::rngs::StdRng;
use serde::{Deserialize, Serialize};
use slog::{Logger, o};
use mithril_aggregator_client::AggregatorHttpClient;
#[cfg(not(target_family = "wasm"))]
use mithril_aggregator_discovery::{
AggregatorDiscoverer, AggregatorEndpoint, AggregatorEndpointWithCapabilities,
CapableAggregatorDiscoverer, HttpConfigAggregatorDiscoverer, RequiredAggregatorCapabilities,
ShuffleAggregatorDiscoverer,
};
use mithril_common::{MITHRIL_CLIENT_TYPE_HEADER, MITHRIL_ORIGIN_TAG_HEADER};
use crate::MithrilResult;
#[cfg(feature = "unstable")]
use crate::cardano_block_client::CardanoBlockClient;
use crate::cardano_database_client::CardanoDatabaseClient;
use crate::cardano_stake_distribution_client::CardanoStakeDistributionClient;
use crate::cardano_transaction_client::CardanoTransactionClient;
#[cfg(feature = "unstable")]
use crate::cardano_transaction_v2_client::CardanoTransactionV2Client;
#[cfg(feature = "unstable")]
use crate::certificate_client::CertificateVerifierCache;
use crate::certificate_client::{
CertificateClient, CertificateVerifier, MithrilCertificateVerifier,
};
#[cfg(not(target_family = "wasm"))]
use crate::common::MithrilNetwork;
use crate::era::{EraFetcher, MithrilEraClient};
use crate::feedback::{FeedbackReceiver, FeedbackSender};
#[cfg(feature = "fs")]
use crate::file_downloader::{
FileDownloadRetryPolicy, FileDownloader, HttpFileDownloader, RetryDownloader,
};
use crate::mithril_stake_distribution_client::MithrilStakeDistributionClient;
#[cfg(feature = "fs")]
use crate::utils::AncillaryVerifier;
#[cfg(feature = "fs")]
use crate::utils::TimestampTempDirectoryProvider;
const DEFAULT_CLIENT_TYPE: &str = "LIBRARY";
#[cfg(target_family = "wasm")]
const fn one_week_in_seconds() -> u32 {
604800
}
pub enum AggregatorDiscoveryType {
Url(String),
#[cfg(not(target_family = "wasm"))]
Automatic(MithrilNetwork),
}
#[cfg(not(target_family = "wasm"))]
impl FromStr for AggregatorDiscoveryType {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(network) = s.strip_prefix("auto:") {
Ok(AggregatorDiscoveryType::Automatic(MithrilNetwork::new(
network.to_string(),
)))
} else {
Ok(AggregatorDiscoveryType::Url(s.to_string()))
}
}
}
pub enum GenesisVerificationKey {
JsonHex(String),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClientOptions {
pub http_headers: Option<HashMap<String, String>>,
#[cfg(target_family = "wasm")]
#[cfg_attr(target_family = "wasm", serde(default))]
pub origin_tag: Option<String>,
#[cfg(target_family = "wasm")]
#[cfg_attr(target_family = "wasm", serde(default))]
pub unstable: bool,
#[cfg(target_family = "wasm")]
#[cfg_attr(target_family = "wasm", serde(default))]
pub enable_certificate_chain_verification_cache: bool,
#[cfg(target_family = "wasm")]
#[cfg_attr(target_family = "wasm", serde(default = "one_week_in_seconds"))]
pub certificate_chain_verification_cache_duration_in_seconds: u32,
}
impl ClientOptions {
pub fn new(http_headers: Option<HashMap<String, String>>) -> Self {
Self {
http_headers,
#[cfg(target_family = "wasm")]
origin_tag: None,
#[cfg(target_family = "wasm")]
unstable: false,
#[cfg(target_family = "wasm")]
enable_certificate_chain_verification_cache: false,
#[cfg(target_family = "wasm")]
certificate_chain_verification_cache_duration_in_seconds: one_week_in_seconds(),
}
}
#[cfg(target_family = "wasm")]
pub fn with_unstable_features(self, unstable: bool) -> Self {
Self { unstable, ..self }
}
}
#[derive(Clone)]
pub struct Client {
certificate_client: Arc<CertificateClient>,
mithril_stake_distribution_client: Arc<MithrilStakeDistributionClient>,
cardano_database_client: Arc<CardanoDatabaseClient>,
cardano_transaction_client: Arc<CardanoTransactionClient>,
#[cfg(feature = "unstable")]
cardano_transaction_v2_client: Arc<CardanoTransactionV2Client>,
#[cfg(feature = "unstable")]
cardano_block_client: Arc<CardanoBlockClient>,
cardano_stake_distribution_client: Arc<CardanoStakeDistributionClient>,
mithril_era_client: Arc<MithrilEraClient>,
}
impl Client {
pub fn certificate(&self) -> Arc<CertificateClient> {
self.certificate_client.clone()
}
pub fn mithril_stake_distribution(&self) -> Arc<MithrilStakeDistributionClient> {
self.mithril_stake_distribution_client.clone()
}
pub fn cardano_database_v2(&self) -> Arc<CardanoDatabaseClient> {
self.cardano_database_client.clone()
}
pub fn cardano_transaction(&self) -> Arc<CardanoTransactionClient> {
self.cardano_transaction_client.clone()
}
cfg_unstable! {
pub fn cardano_transaction_v2(&self) -> Arc<CardanoTransactionV2Client> {
self.cardano_transaction_v2_client.clone()
}
pub fn cardano_block(&self) -> Arc<CardanoBlockClient> {
self.cardano_block_client.clone()
}
}
pub fn cardano_stake_distribution(&self) -> Arc<CardanoStakeDistributionClient> {
self.cardano_stake_distribution_client.clone()
}
pub fn mithril_era_client(&self) -> Arc<MithrilEraClient> {
self.mithril_era_client.clone()
}
}
pub struct ClientBuilder {
aggregator_discovery: AggregatorDiscoveryType,
#[cfg(not(target_family = "wasm"))]
aggregator_capabilities: Option<RequiredAggregatorCapabilities>,
#[cfg(not(target_family = "wasm"))]
aggregator_discoverer: Option<Arc<dyn AggregatorDiscoverer<AggregatorEndpoint>>>,
genesis_verification_key: Option<GenesisVerificationKey>,
origin_tag: Option<String>,
client_type: Option<String>,
#[cfg(feature = "fs")]
ancillary_verification_key: Option<String>,
certificate_verifier: Option<Arc<dyn CertificateVerifier>>,
#[cfg(feature = "fs")]
http_file_downloader: Option<Arc<dyn FileDownloader>>,
#[cfg(feature = "unstable")]
certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
era_fetcher: Option<Arc<dyn EraFetcher>>,
logger: Option<Logger>,
feedback_receivers: Vec<Arc<dyn FeedbackReceiver>>,
options: ClientOptions,
}
impl ClientBuilder {
#[deprecated(
since = "0.12.36",
note = "Use `new` method 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())).set_genesis_verification_key(
GenesisVerificationKey::JsonHex(genesis_verification_key.to_string()),
)
}
#[cfg(not(target_family = "wasm"))]
pub fn automatic(network: &str, genesis_verification_key: &str) -> ClientBuilder {
Self::new(AggregatorDiscoveryType::Automatic(MithrilNetwork::new(
network.to_string(),
)))
.set_genesis_verification_key(GenesisVerificationKey::JsonHex(
genesis_verification_key.to_string(),
))
}
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,
client_type: None,
#[cfg(feature = "fs")]
ancillary_verification_key: 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(),
}
}
pub fn set_genesis_verification_key(
mut self,
genesis_verification_key: GenesisVerificationKey,
) -> ClientBuilder {
self.genesis_verification_key = Some(genesis_verification_key);
self
}
#[cfg(not(target_family = "wasm"))]
pub fn with_capabilities(
mut self,
capabilities: RequiredAggregatorCapabilities,
) -> ClientBuilder {
self.aggregator_capabilities = Some(capabilities);
self
}
#[cfg(not(target_family = "wasm"))]
pub fn with_aggregator_discoverer(
mut self,
discoverer: Arc<dyn AggregatorDiscoverer<AggregatorEndpoint>>,
) -> ClientBuilder {
self.aggregator_discoverer = Some(discoverer);
self
}
pub fn build(self) -> MithrilResult<Client> {
let logger = self
.logger
.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 'set_genesis_verification_key' function"
));
}
};
let feedback_sender = FeedbackSender::new(&self.feedback_receivers);
let aggregator_client = Arc::new(self.build_aggregator_client(logger.clone())?);
let mithril_era_client = match self.era_fetcher {
None => Arc::new(MithrilEraClient::new(aggregator_client.clone())),
Some(era_fetcher) => Arc::new(MithrilEraClient::new(era_fetcher)),
};
let certificate_verifier = match self.certificate_verifier {
None => Arc::new(
MithrilCertificateVerifier::new(
aggregator_client.clone(),
genesis_verification_key,
feedback_sender.clone(),
#[cfg(feature = "unstable")]
self.certificate_verifier_cache,
logger.clone(),
)
.with_context(|| "Building certificate verifier failed")?,
),
Some(verifier) => verifier,
};
let certificate_client = Arc::new(CertificateClient::new(
aggregator_client.clone(),
certificate_verifier,
logger.clone(),
));
let mithril_stake_distribution_client = Arc::new(MithrilStakeDistributionClient::new(
aggregator_client.clone(),
));
#[cfg(feature = "fs")]
let http_file_downloader = match self.http_file_downloader {
None => Arc::new(RetryDownloader::new(
Arc::new(
HttpFileDownloader::new(feedback_sender.clone(), logger.clone())
.with_context(|| "Building http file downloader failed")?,
),
FileDownloadRetryPolicy::default(),
)),
Some(http_file_downloader) => http_file_downloader,
};
#[cfg(feature = "fs")]
let ancillary_verifier = match self.ancillary_verification_key {
None => None,
Some(verification_key) => Some(Arc::new(AncillaryVerifier::new(
verification_key
.try_into()
.with_context(|| "Building ancillary verifier failed")?,
))),
};
let cardano_database_client = Arc::new(CardanoDatabaseClient::new(
aggregator_client.clone(),
#[cfg(feature = "fs")]
http_file_downloader,
#[cfg(feature = "fs")]
ancillary_verifier,
#[cfg(feature = "fs")]
feedback_sender,
#[cfg(feature = "fs")]
Arc::new(TimestampTempDirectoryProvider::new(&format!(
"{}",
Utc::now().timestamp_micros()
))),
#[cfg(feature = "fs")]
logger,
));
let cardano_transaction_client =
Arc::new(CardanoTransactionClient::new(aggregator_client.clone()));
#[cfg(feature = "unstable")]
let cardano_transaction_v2_client =
Arc::new(CardanoTransactionV2Client::new(aggregator_client.clone()));
#[cfg(feature = "unstable")]
let cardano_block_client = Arc::new(CardanoBlockClient::new(aggregator_client.clone()));
let cardano_stake_distribution_client =
Arc::new(CardanoStakeDistributionClient::new(aggregator_client));
Ok(Client {
certificate_client,
mithril_stake_distribution_client,
cardano_database_client,
cardano_transaction_client,
#[cfg(feature = "unstable")]
cardano_transaction_v2_client,
#[cfg(feature = "unstable")]
cardano_block_client,
cardano_stake_distribution_client,
mithril_era_client,
})
}
#[cfg(not(target_family = "wasm"))]
pub fn discover_aggregator(
&self,
network: &MithrilNetwork,
) -> MithrilResult<impl Iterator<Item = AggregatorEndpointWithCapabilities>> {
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<dyn AggregatorDiscoverer<AggregatorEndpointWithCapabilities>>
} else {
Arc::new(CapableAggregatorDiscoverer::new(
RequiredAggregatorCapabilities::All,
discoverer.clone(),
)) as Arc<dyn AggregatorDiscoverer<AggregatorEndpointWithCapabilities>>
};
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")
})
})
}
#[cfg(not(target_family = "wasm"))]
fn default_aggregator_discoverer() -> Arc<dyn AggregatorDiscoverer<AggregatorEndpoint>> {
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)
},
))
}
fn build_aggregator_client(&self, logger: Logger) -> MithrilResult<AggregatorHttpClient> {
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()
.with_context(|| "No aggregator was available through discovery")?
.into(),
};
let headers = self.compute_http_headers();
AggregatorHttpClient::builder(aggregator_endpoint)
.with_logger(logger)
.with_headers(headers)
.build()
}
fn compute_http_headers(&self) -> HashMap<String, String> {
let mut headers = self.options.http_headers.clone().unwrap_or_default();
if let Some(origin_tag) = self.origin_tag.clone() {
headers.insert(MITHRIL_ORIGIN_TAG_HEADER.to_string(), origin_tag);
}
if let Some(client_type) = self.client_type.clone() {
headers.insert(MITHRIL_CLIENT_TYPE_HEADER.to_string(), client_type);
} else if !headers.contains_key(MITHRIL_CLIENT_TYPE_HEADER) {
headers.insert(
MITHRIL_CLIENT_TYPE_HEADER.to_string(),
DEFAULT_CLIENT_TYPE.to_string(),
);
}
headers
}
pub fn with_era_fetcher(mut self, era_fetcher: Arc<dyn EraFetcher>) -> ClientBuilder {
self.era_fetcher = Some(era_fetcher);
self
}
pub fn with_certificate_verifier(
mut self,
certificate_verifier: Arc<dyn CertificateVerifier>,
) -> ClientBuilder {
self.certificate_verifier = Some(certificate_verifier);
self
}
cfg_unstable! {
pub fn with_certificate_verifier_cache(
mut self,
certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
) -> ClientBuilder {
self.certificate_verifier_cache = certificate_verifier_cache;
self
}
}
cfg_fs! {
pub fn with_http_file_downloader(
mut self,
http_file_downloader: Arc<dyn FileDownloader>,
) -> ClientBuilder {
self.http_file_downloader = Some(http_file_downloader);
self
}
pub fn set_ancillary_verification_key<T: Into<Option<String>>>(
mut self,
ancillary_verification_key: T,
) -> ClientBuilder {
self.ancillary_verification_key = ancillary_verification_key.into();
self
}
}
pub fn with_logger(mut self, logger: Logger) -> Self {
self.logger = Some(logger);
self
}
pub fn with_origin_tag(mut self, origin_tag: Option<String>) -> Self {
self.origin_tag = origin_tag;
self
}
pub fn with_client_type(mut self, client_type: Option<String>) -> Self {
self.client_type = client_type;
self
}
pub fn with_options(mut self, options: ClientOptions) -> Self {
self.options = options;
self
}
pub fn add_feedback_receiver(mut self, receiver: Arc<dyn FeedbackReceiver>) -> Self {
self.feedback_receivers.push(receiver);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_headers() -> HashMap<String, String> {
HashMap::from([(
MITHRIL_CLIENT_TYPE_HEADER.to_string(),
DEFAULT_CLIENT_TYPE.to_string(),
)])
}
#[tokio::test]
async fn compute_http_headers_returns_options_http_headers() {
let http_headers = default_headers();
let client_builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
.with_options(ClientOptions {
http_headers: Some(http_headers.clone()),
});
let computed_headers = client_builder.compute_http_headers();
assert_eq!(computed_headers, http_headers);
}
#[tokio::test]
async fn compute_http_headers_with_origin_tag_returns_options_http_headers_with_origin_tag() {
let http_headers = default_headers();
let client_builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
.with_options(ClientOptions {
http_headers: Some(http_headers.clone()),
})
.with_origin_tag(Some("CLIENT_TAG".to_string()));
let mut expected_headers = http_headers.clone();
expected_headers.insert(
MITHRIL_ORIGIN_TAG_HEADER.to_string(),
"CLIENT_TAG".to_string(),
);
let computed_headers = client_builder.compute_http_headers();
assert_eq!(computed_headers, expected_headers);
}
#[tokio::test]
async fn test_with_origin_tag_not_overwrite_other_client_options_attributes() {
let builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
.with_options(ClientOptions { http_headers: None })
.with_origin_tag(Some("TEST".to_string()));
assert_eq!(None, builder.options.http_headers);
assert_eq!(Some("TEST".to_string()), builder.origin_tag);
let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
let builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
.with_options(ClientOptions {
http_headers: Some(http_headers.clone()),
})
.with_origin_tag(Some("TEST".to_string()));
assert_eq!(Some(http_headers), builder.options.http_headers);
assert_eq!(Some("TEST".to_string()), builder.origin_tag);
}
#[tokio::test]
async fn test_with_origin_tag_can_be_unset() {
let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
let client_options = ClientOptions {
http_headers: Some(http_headers.clone()),
};
let builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
.with_options(client_options)
.with_origin_tag(None);
assert_eq!(Some(http_headers), builder.options.http_headers);
assert_eq!(None, builder.origin_tag);
}
#[tokio::test]
async fn compute_http_headers_with_client_type_returns_options_http_headers_with_client_type() {
let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
let client_builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
.with_options(ClientOptions {
http_headers: Some(http_headers.clone()),
})
.with_client_type(Some("CLIENT_TYPE".to_string()));
let computed_headers = client_builder.compute_http_headers();
assert_eq!(
computed_headers,
HashMap::from([
("Key".to_string(), "Value".to_string()),
(
MITHRIL_CLIENT_TYPE_HEADER.to_string(),
"CLIENT_TYPE".to_string()
)
])
);
}
#[tokio::test]
async fn compute_http_headers_with_options_containing_client_type_returns_client_type() {
let http_headers = HashMap::from([(
MITHRIL_CLIENT_TYPE_HEADER.to_string(),
"client type from options".to_string(),
)]);
let client_builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
.with_options(ClientOptions {
http_headers: Some(http_headers.clone()),
});
let computed_headers = client_builder.compute_http_headers();
assert_eq!(computed_headers, http_headers);
}
#[tokio::test]
async fn test_with_client_type_not_overwrite_other_client_options_attributes() {
let builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
.with_options(ClientOptions { http_headers: None })
.with_client_type(Some("TEST".to_string()));
assert_eq!(None, builder.options.http_headers);
assert_eq!(Some("TEST".to_string()), builder.client_type);
let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
let builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
.with_options(ClientOptions {
http_headers: Some(http_headers.clone()),
})
.with_client_type(Some("TEST".to_string()));
assert_eq!(Some(http_headers), builder.options.http_headers);
assert_eq!(Some("TEST".to_string()), builder.client_type);
}
#[tokio::test]
async fn test_given_a_none_client_type_compute_http_headers_will_set_client_type_to_default_value()
{
let builder_without_client_type =
ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()));
let computed_headers = builder_without_client_type.compute_http_headers();
assert_eq!(
computed_headers,
HashMap::from([(
MITHRIL_CLIENT_TYPE_HEADER.to_string(),
DEFAULT_CLIENT_TYPE.to_string()
)])
);
let builder_with_none_client_type =
ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string())).with_client_type(None);
let computed_headers = builder_with_none_client_type.compute_http_headers();
assert_eq!(
computed_headers,
HashMap::from([(
MITHRIL_CLIENT_TYPE_HEADER.to_string(),
DEFAULT_CLIENT_TYPE.to_string()
)])
);
}
#[tokio::test]
async fn test_compute_http_headers_will_compute_client_type_header_from_struct_attribute_over_options()
{
let http_headers = HashMap::from([(
MITHRIL_CLIENT_TYPE_HEADER.to_string(),
"client type from options".to_string(),
)]);
let client_builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
.with_options(ClientOptions {
http_headers: Some(http_headers.clone()),
})
.with_client_type(Some("client type".to_string()));
let computed_headers = client_builder.compute_http_headers();
assert_eq!(
computed_headers,
HashMap::from([(
MITHRIL_CLIENT_TYPE_HEADER.to_string(),
"client type".to_string()
)])
);
}
}