use anyhow::anyhow;
use config::{ConfigError, Map, Source, Value, ValueKind};
use mithril_common::chain_observer::ChainObserverType;
use mithril_common::crypto_helper::ProtocolGenesisSigner;
use mithril_common::era::adapters::EraReaderAdapterType;
use mithril_doc::{Documenter, DocumenterDefault, StructDoc};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use std::path::PathBuf;
use std::str::FromStr;
use mithril_common::entities::{
CompressionAlgorithm, HexEncodedGenesisVerificationKey, ProtocolParameters, SignedEntityType,
SignedEntityTypeDiscriminants, TimePoint,
};
use mithril_common::{CardanoNetwork, StdResult};
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum ExecutionEnvironment {
Test,
Production,
}
impl FromStr for ExecutionEnvironment {
type Err = ConfigError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"production" => Ok(Self::Production),
"test" => Ok(Self::Test),
_ => Err(ConfigError::Message(format!(
"Unknown execution environment {s}"
))),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Documenter)]
pub struct Configuration {
pub environment: ExecutionEnvironment,
#[example = "`cardano-cli`"]
pub cardano_cli_path: PathBuf,
#[example = "`/tmp/cardano.sock`"]
pub cardano_node_socket_path: PathBuf,
pub cardano_node_version: String,
#[example = "`1097911063` or `42`"]
pub network_magic: Option<u64>,
#[example = "`testnet` or `mainnet` or `devnet`"]
pub network: String,
pub chain_observer_type: ChainObserverType,
#[example = "`{ k: 5, m: 100, phi_f: 0.65 }`"]
pub protocol_parameters: ProtocolParameters,
#[example = "`gcp` or `local`"]
pub snapshot_uploader_type: SnapshotUploaderType,
pub snapshot_bucket_name: Option<String>,
pub snapshot_use_cdn_domain: bool,
pub server_ip: String,
pub server_port: u16,
#[example = "`60000`"]
pub run_interval: u64,
pub db_directory: PathBuf,
pub snapshot_directory: PathBuf,
#[example = "`./mithril-aggregator/stores`"]
pub data_stores_directory: PathBuf,
pub genesis_verification_key: HexEncodedGenesisVerificationKey,
pub reset_digests_cache: bool,
pub disable_digests_cache: bool,
pub store_retention_limit: Option<usize>,
pub era_reader_adapter_type: EraReaderAdapterType,
pub era_reader_adapter_params: Option<String>,
#[example = "`MithrilStakeDistribution,CardanoImmutableFilesFull,CardanoStakeDistribution`"]
pub signed_entity_types: Option<String>,
#[example = "`gzip` or `zstandard`"]
pub snapshot_compression_algorithm: CompressionAlgorithm,
#[example = "`{ level: 9, number_of_workers: 4 }`"]
pub zstandard_parameters: Option<ZstandardCompressionParameters>,
pub cexplorer_pools_url: Option<String>,
pub signer_importer_run_interval: u64,
pub allow_unparsable_block: bool,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SnapshotUploaderType {
Gcp,
Local,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct ZstandardCompressionParameters {
pub level: i32,
pub number_of_workers: u32,
}
impl Default for ZstandardCompressionParameters {
fn default() -> Self {
Self {
level: 9,
number_of_workers: 4,
}
}
}
impl Configuration {
pub fn new_sample() -> Self {
let genesis_verification_key = ProtocolGenesisSigner::create_deterministic_genesis_signer()
.create_genesis_verifier()
.to_verification_key();
Self {
environment: ExecutionEnvironment::Test,
cardano_cli_path: PathBuf::new(),
cardano_node_socket_path: PathBuf::new(),
cardano_node_version: "0.0.1".to_string(),
network_magic: Some(42),
network: "devnet".to_string(),
chain_observer_type: ChainObserverType::Fake,
protocol_parameters: ProtocolParameters {
k: 5,
m: 100,
phi_f: 0.95,
},
snapshot_uploader_type: SnapshotUploaderType::Local,
snapshot_bucket_name: None,
snapshot_use_cdn_domain: false,
server_ip: "0.0.0.0".to_string(),
server_port: 8000,
run_interval: 5000,
db_directory: PathBuf::new(),
snapshot_directory: PathBuf::new(),
data_stores_directory: PathBuf::from(":memory:"),
genesis_verification_key: genesis_verification_key.to_json_hex().unwrap(),
reset_digests_cache: false,
disable_digests_cache: false,
store_retention_limit: None,
era_reader_adapter_type: EraReaderAdapterType::Bootstrap,
era_reader_adapter_params: None,
signed_entity_types: None,
snapshot_compression_algorithm: CompressionAlgorithm::Zstandard,
zstandard_parameters: Some(ZstandardCompressionParameters::default()),
cexplorer_pools_url: None,
signer_importer_run_interval: 1,
allow_unparsable_block: false,
}
}
pub fn get_server_url(&self) -> String {
format!("http://{}:{}/", self.server_ip, self.server_port)
}
pub fn get_network(&self) -> StdResult<CardanoNetwork> {
CardanoNetwork::from_code(self.network.clone(), self.network_magic)
.map_err(|e| anyhow!(ConfigError::Message(e.to_string())))
}
pub fn get_sqlite_dir(&self) -> PathBuf {
let store_dir = &self.data_stores_directory;
if !store_dir.exists() {
std::fs::create_dir_all(store_dir).unwrap();
}
self.data_stores_directory.clone()
}
pub fn safe_epoch_retention_limit(&self) -> Option<u64> {
self.store_retention_limit
.map(|limit| if limit > 3 { limit as u64 } else { 3 })
}
pub fn list_allowed_signed_entity_types_discriminants(
&self,
) -> StdResult<BTreeSet<SignedEntityTypeDiscriminants>> {
let default_discriminants = BTreeSet::from([
SignedEntityTypeDiscriminants::MithrilStakeDistribution,
SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
]);
let mut all_discriminants = default_discriminants;
let discriminant_names = self.signed_entity_types.clone().unwrap_or_default();
for discriminant in discriminant_names
.split(',')
.filter_map(|name| SignedEntityTypeDiscriminants::from_str(name.trim()).ok())
{
all_discriminants.insert(discriminant);
}
Ok(all_discriminants)
}
pub fn list_allowed_signed_entity_types(
&self,
time_point: &TimePoint,
) -> StdResult<Vec<SignedEntityType>> {
let allowed_discriminants = self.list_allowed_signed_entity_types_discriminants()?;
let signed_entity_types = allowed_discriminants
.into_iter()
.map(|discriminant| {
SignedEntityType::from_time_point(&discriminant, &self.network, time_point)
})
.collect();
Ok(signed_entity_types)
}
}
#[derive(Debug, Clone, DocumenterDefault)]
pub struct DefaultConfiguration {
pub environment: ExecutionEnvironment,
pub server_ip: String,
pub server_port: String,
pub db_directory: String,
pub snapshot_directory: String,
#[example = "`gcp` or `local`"]
pub snapshot_store_type: String,
pub snapshot_uploader_type: String,
pub era_reader_adapter_type: String,
pub chain_observer_type: String,
pub reset_digests_cache: String,
pub disable_digests_cache: String,
pub snapshot_compression_algorithm: String,
pub snapshot_use_cdn_domain: String,
pub signer_importer_run_interval: u64,
pub allow_unparsable_block: String,
}
impl Default for DefaultConfiguration {
fn default() -> Self {
Self {
environment: ExecutionEnvironment::Production,
server_ip: "0.0.0.0".to_string(),
server_port: "8080".to_string(),
db_directory: "/db".to_string(),
snapshot_directory: ".".to_string(),
snapshot_store_type: "local".to_string(),
snapshot_uploader_type: "gcp".to_string(),
era_reader_adapter_type: "bootstrap".to_string(),
chain_observer_type: "pallas".to_string(),
reset_digests_cache: "false".to_string(),
disable_digests_cache: "false".to_string(),
snapshot_compression_algorithm: "zstandard".to_string(),
snapshot_use_cdn_domain: "false".to_string(),
signer_importer_run_interval: 720,
allow_unparsable_block: "false".to_string(),
}
}
}
impl DefaultConfiguration {
fn namespace() -> String {
"default configuration".to_string()
}
}
impl From<ExecutionEnvironment> for ValueKind {
fn from(value: ExecutionEnvironment) -> Self {
match value {
ExecutionEnvironment::Production => ValueKind::String("Production".to_string()),
ExecutionEnvironment::Test => ValueKind::String("Test".to_string()),
}
}
}
impl Source for DefaultConfiguration {
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
Box::new(self.clone())
}
fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
fn into_value<V: Into<ValueKind>>(value: V) -> Value {
Value::new(Some(&DefaultConfiguration::namespace()), value.into())
}
let mut result = Map::new();
let myself = self.clone();
result.insert("environment".to_string(), into_value(myself.environment));
result.insert("server_ip".to_string(), into_value(myself.server_ip));
result.insert("server_port".to_string(), into_value(myself.server_port));
result.insert("db_directory".to_string(), into_value(myself.db_directory));
result.insert(
"snapshot_directory".to_string(),
into_value(myself.snapshot_directory),
);
result.insert(
"snapshot_store_type".to_string(),
into_value(myself.snapshot_store_type),
);
result.insert(
"snapshot_uploader_type".to_string(),
into_value(myself.snapshot_uploader_type),
);
result.insert(
"era_reader_adapter_type".to_string(),
into_value(myself.era_reader_adapter_type),
);
result.insert(
"reset_digests_cache".to_string(),
into_value(myself.reset_digests_cache),
);
result.insert(
"disable_digests_cache".to_string(),
into_value(myself.disable_digests_cache),
);
result.insert(
"snapshot_compression_algorithm".to_string(),
into_value(myself.snapshot_compression_algorithm),
);
result.insert(
"snapshot_use_cdn_domain".to_string(),
into_value(myself.snapshot_use_cdn_domain),
);
result.insert(
"signer_importer_run_interval".to_string(),
into_value(myself.signer_importer_run_interval),
);
result.insert(
"allow_unparsable_block".to_string(),
into_value(myself.allow_unparsable_block),
);
Ok(result)
}
}
#[cfg(test)]
mod test {
use mithril_common::test_utils::fake_data;
use super::*;
#[test]
fn safe_epoch_retention_limit_wont_change_a_value_higher_than_three() {
for limit in 4..=10u64 {
let configuration = Configuration {
store_retention_limit: Some(limit as usize),
..Configuration::new_sample()
};
assert_eq!(configuration.safe_epoch_retention_limit(), Some(limit));
}
}
#[test]
fn safe_epoch_retention_limit_wont_change_a_none_value() {
let configuration = Configuration {
store_retention_limit: None,
..Configuration::new_sample()
};
assert_eq!(configuration.safe_epoch_retention_limit(), None);
}
#[test]
fn safe_epoch_retention_limit_wont_yield_a_value_lower_than_three() {
for limit in 0..=3 {
let configuration = Configuration {
store_retention_limit: Some(limit),
..Configuration::new_sample()
};
assert_eq!(configuration.safe_epoch_retention_limit(), Some(3));
}
}
#[test]
fn test_list_allowed_signed_entity_types_discriminant_without_specific_configuration() {
let config = Configuration {
signed_entity_types: None,
..Configuration::new_sample()
};
let discriminants = config
.list_allowed_signed_entity_types_discriminants()
.unwrap();
assert_eq!(
BTreeSet::from([
SignedEntityTypeDiscriminants::MithrilStakeDistribution,
SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
]),
discriminants
);
}
#[test]
fn test_list_allowed_signed_entity_types_discriminant_should_not_return_unknown_signed_entity_types_in_configuration(
) {
let config = Configuration {
signed_entity_types: Some("Unknown".to_string()),
..Configuration::new_sample()
};
let discriminants = config
.list_allowed_signed_entity_types_discriminants()
.unwrap();
assert_eq!(
BTreeSet::from([
SignedEntityTypeDiscriminants::MithrilStakeDistribution,
SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
]),
discriminants
);
}
#[test]
fn test_list_allowed_signed_entity_types_discriminant_should_not_duplicate_a_signed_entity_discriminant_type_already_in_default_ones(
) {
let config = Configuration {
signed_entity_types: Some(
"CardanoImmutableFilesFull, MithrilStakeDistribution, CardanoImmutableFilesFull"
.to_string(),
),
..Configuration::new_sample()
};
let discriminants = config
.list_allowed_signed_entity_types_discriminants()
.unwrap();
assert_eq!(
BTreeSet::from([
SignedEntityTypeDiscriminants::MithrilStakeDistribution,
SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
]),
discriminants
);
}
#[test]
fn test_list_allowed_signed_entity_types_discriminants_should_add_signed_entity_types_in_configuration_at_the_end(
) {
let config = Configuration {
signed_entity_types: Some("CardanoStakeDistribution, CardanoTransactions".to_string()),
..Configuration::new_sample()
};
let discriminants = config
.list_allowed_signed_entity_types_discriminants()
.unwrap();
assert_eq!(
BTreeSet::from([
SignedEntityTypeDiscriminants::MithrilStakeDistribution,
SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
SignedEntityTypeDiscriminants::CardanoStakeDistribution,
SignedEntityTypeDiscriminants::CardanoTransactions,
]),
discriminants
);
}
#[test]
fn test_list_allowed_signed_entity_types_discriminants_with_multiple_identical_signed_entity_types_in_configuration_should_not_be_added_several_times(
) {
let config = Configuration {
signed_entity_types: Some(
"CardanoStakeDistribution, CardanoStakeDistribution, CardanoStakeDistribution"
.to_string(),
),
..Configuration::new_sample()
};
let discriminants = config
.list_allowed_signed_entity_types_discriminants()
.unwrap();
assert_eq!(
BTreeSet::from([
SignedEntityTypeDiscriminants::MithrilStakeDistribution,
SignedEntityTypeDiscriminants::CardanoStakeDistribution,
SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
]),
discriminants
);
}
#[test]
fn test_list_allowed_signed_entity_types_with_specific_configuration() {
let beacon = fake_data::beacon();
let time_point = TimePoint::new(*beacon.epoch, beacon.immutable_file_number);
let config = Configuration {
network: beacon.network.clone(),
signed_entity_types: Some("CardanoStakeDistribution, CardanoTransactions".to_string()),
..Configuration::new_sample()
};
let signed_entity_types = config
.list_allowed_signed_entity_types(&time_point)
.unwrap();
assert_eq!(
vec![
SignedEntityType::MithrilStakeDistribution(beacon.epoch),
SignedEntityType::CardanoStakeDistribution(beacon.epoch),
SignedEntityType::CardanoImmutableFilesFull(beacon.clone()),
SignedEntityType::CardanoTransactions(beacon.clone()),
],
signed_entity_types
);
}
}