use anyhow::{anyhow, Context};
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, HashMap};
use std::path::PathBuf;
use std::str::FromStr;
use mithril_common::entities::{
BlockNumber, CardanoTransactionsSigningConfig, CompressionAlgorithm,
HexEncodedGenesisVerificationKey, ProtocolParameters, SignedEntityConfig,
SignedEntityTypeDiscriminants,
};
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,
pub cardano_transactions_prover_cache_pool_size: usize,
pub cardano_transactions_database_connection_pool_size: usize,
#[example = "`{ security_parameter: 3000, step: 120 }`"]
pub cardano_transactions_signing_config: CardanoTransactionsSigningConfig,
pub cardano_transactions_prover_max_hashes_allowed_by_request: usize,
pub cardano_transactions_block_streamer_max_roll_forwards_per_poll: usize,
pub enable_metrics_server: bool,
pub metrics_server_ip: String,
pub metrics_server_port: u16,
pub persist_usage_report_interval_in_seconds: u64,
}
#[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,
cardano_transactions_prover_cache_pool_size: 3,
cardano_transactions_database_connection_pool_size: 5,
cardano_transactions_signing_config: CardanoTransactionsSigningConfig {
security_parameter: BlockNumber(120),
step: BlockNumber(15),
},
cardano_transactions_prover_max_hashes_allowed_by_request: 100,
cardano_transactions_block_streamer_max_roll_forwards_per_poll: 1000,
enable_metrics_server: true,
metrics_server_ip: "0.0.0.0".to_string(),
metrics_server_port: 9090,
persist_usage_report_interval_in_seconds: 10,
}
}
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 compute_allowed_signed_entity_types_discriminants(
&self,
) -> StdResult<BTreeSet<SignedEntityTypeDiscriminants>> {
let allowed_discriminants = self
.signed_entity_types
.as_ref()
.map(SignedEntityTypeDiscriminants::parse_list)
.transpose()
.with_context(|| "Invalid 'signed_entity_types' configuration")?
.unwrap_or_default();
let allowed_discriminants =
SignedEntityConfig::append_allowed_signed_entity_types_discriminants(
allowed_discriminants,
);
Ok(allowed_discriminants)
}
}
#[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,
pub cardano_transactions_prover_cache_pool_size: u32,
pub cardano_transactions_database_connection_pool_size: u32,
pub cardano_transactions_signing_config: CardanoTransactionsSigningConfig,
pub cardano_transactions_prover_max_hashes_allowed_by_request: u32,
pub cardano_transactions_block_streamer_max_roll_forwards_per_poll: u32,
pub enable_metrics_server: String,
pub metrics_server_ip: String,
pub metrics_server_port: u16,
pub persist_usage_report_interval_in_seconds: u64,
}
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(),
cardano_transactions_prover_cache_pool_size: 10,
cardano_transactions_database_connection_pool_size: 10,
cardano_transactions_signing_config: CardanoTransactionsSigningConfig {
security_parameter: BlockNumber(3000),
step: BlockNumber(120),
},
cardano_transactions_prover_max_hashes_allowed_by_request: 100,
cardano_transactions_block_streamer_max_roll_forwards_per_poll: 10000,
enable_metrics_server: "false".to_string(),
metrics_server_ip: "0.0.0.0".to_string(),
metrics_server_port: 9090,
persist_usage_report_interval_in_seconds: 10,
}
}
}
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> {
macro_rules! insert_default_configuration {
( $map:ident, $config:ident.$parameter:ident ) => {{
$map.insert(
stringify!($parameter).to_string(),
into_value($config.$parameter),
);
}};
}
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();
insert_default_configuration!(result, myself.environment);
insert_default_configuration!(result, myself.server_ip);
insert_default_configuration!(result, myself.server_port);
insert_default_configuration!(result, myself.db_directory);
insert_default_configuration!(result, myself.snapshot_directory);
insert_default_configuration!(result, myself.snapshot_store_type);
insert_default_configuration!(result, myself.snapshot_uploader_type);
insert_default_configuration!(result, myself.era_reader_adapter_type);
insert_default_configuration!(result, myself.reset_digests_cache);
insert_default_configuration!(result, myself.disable_digests_cache);
insert_default_configuration!(result, myself.snapshot_compression_algorithm);
insert_default_configuration!(result, myself.snapshot_use_cdn_domain);
insert_default_configuration!(result, myself.signer_importer_run_interval);
insert_default_configuration!(result, myself.allow_unparsable_block);
insert_default_configuration!(result, myself.cardano_transactions_prover_cache_pool_size);
insert_default_configuration!(
result,
myself.cardano_transactions_database_connection_pool_size
);
insert_default_configuration!(
result,
myself.cardano_transactions_prover_max_hashes_allowed_by_request
);
insert_default_configuration!(
result,
myself.cardano_transactions_block_streamer_max_roll_forwards_per_poll
);
insert_default_configuration!(result, myself.enable_metrics_server);
insert_default_configuration!(result, myself.metrics_server_ip);
insert_default_configuration!(result, myself.metrics_server_port);
insert_default_configuration!(result, myself.persist_usage_report_interval_in_seconds);
result.insert(
"cardano_transactions_signing_config".to_string(),
into_value(HashMap::from([
(
"security_parameter".to_string(),
ValueKind::from(
*myself
.cardano_transactions_signing_config
.security_parameter,
),
),
(
"step".to_string(),
ValueKind::from(*myself.cardano_transactions_signing_config.step),
),
])),
);
Ok(result)
}
}
#[cfg(test)]
mod test {
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 can_build_config_with_ctx_signing_config_from_default_configuration() {
#[derive(Debug, Deserialize)]
struct TargetConfig {
cardano_transactions_signing_config: CardanoTransactionsSigningConfig,
}
let config_builder = config::Config::builder().add_source(DefaultConfiguration::default());
let target: TargetConfig = config_builder.build().unwrap().try_deserialize().unwrap();
assert_eq!(
target.cardano_transactions_signing_config,
DefaultConfiguration::default().cardano_transactions_signing_config
);
}
#[test]
fn compute_allowed_signed_entity_types_discriminants_append_default_discriminants() {
let config = Configuration {
signed_entity_types: None,
..Configuration::new_sample()
};
assert_eq!(
config
.compute_allowed_signed_entity_types_discriminants()
.unwrap(),
BTreeSet::from(SignedEntityConfig::DEFAULT_ALLOWED_DISCRIMINANTS)
);
}
}