1use anyhow::{anyhow, Context};
2use config::{ConfigError, Map, Source, Value, ValueKind};
3use serde::{Deserialize, Serialize};
4use std::collections::{BTreeSet, HashMap};
5use std::path::PathBuf;
6use std::str::FromStr;
7
8use mithril_cli_helper::{register_config_value, serde_deserialization};
9use mithril_common::chain_observer::ChainObserverType;
10use mithril_common::crypto_helper::{ManifestSigner, ProtocolGenesisSigner};
11use mithril_common::entities::{
12 BlockNumber, CardanoTransactionsSigningConfig, CompressionAlgorithm,
13 HexEncodedGenesisVerificationKey, HexEncodedKey, ProtocolParameters, SignedEntityConfig,
14 SignedEntityTypeDiscriminants,
15};
16use mithril_common::era::adapters::EraReaderAdapterType;
17use mithril_common::{CardanoNetwork, StdResult};
18use mithril_doc::{Documenter, DocumenterDefault, StructDoc};
19
20use crate::entities::AggregatorEpochSettings;
21use crate::http_server::SERVER_BASE_PATH;
22use crate::tools::url_sanitizer::SanitizedUrlWithTrailingSlash;
23
24#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
26pub enum ExecutionEnvironment {
27 Test,
29
30 Production,
33}
34
35impl FromStr for ExecutionEnvironment {
36 type Err = ConfigError;
37
38 fn from_str(s: &str) -> Result<Self, Self::Err> {
39 match s {
40 "production" => Ok(Self::Production),
41 "test" => Ok(Self::Test),
42 _ => Err(ConfigError::Message(format!(
43 "Unknown execution environment {s}"
44 ))),
45 }
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, Documenter)]
51pub struct Configuration {
52 pub environment: ExecutionEnvironment,
54
55 #[example = "`cardano-cli`"]
57 pub cardano_cli_path: PathBuf,
58
59 #[example = "`/tmp/cardano.sock`"]
62 pub cardano_node_socket_path: PathBuf,
63
64 pub cardano_node_version: String,
70
71 #[example = "`1097911063` or `42`"]
75 pub network_magic: Option<u64>,
76
77 #[example = "`testnet` or `mainnet` or `devnet`"]
79 pub network: String,
80
81 pub chain_observer_type: ChainObserverType,
83
84 #[example = "`{ k: 5, m: 100, phi_f: 0.65 }`"]
86 pub protocol_parameters: ProtocolParameters,
87
88 #[example = "`gcp` or `local`"]
90 pub snapshot_uploader_type: SnapshotUploaderType,
91
92 pub snapshot_bucket_name: Option<String>,
94
95 pub snapshot_use_cdn_domain: bool,
97
98 pub server_ip: String,
100
101 pub server_port: u16,
103
104 pub public_server_url: Option<String>,
106
107 #[example = "`60000`"]
109 pub run_interval: u64,
110
111 pub db_directory: PathBuf,
113
114 pub snapshot_directory: PathBuf,
116
117 #[example = "`./mithril-aggregator/stores`"]
119 pub data_stores_directory: PathBuf,
120
121 pub genesis_verification_key: HexEncodedGenesisVerificationKey,
123
124 pub reset_digests_cache: bool,
126
127 pub disable_digests_cache: bool,
129
130 pub store_retention_limit: Option<usize>,
135
136 pub era_reader_adapter_type: EraReaderAdapterType,
138
139 pub era_reader_adapter_params: Option<String>,
141
142 #[example = "`{ \"type\": \"secret-key\", \"secret_key\": \"136372c3138312c3138382c3130352c3233312c3135\" }`"]
146 #[serde(deserialize_with = "serde_deserialization::string_or_struct")]
147 pub ancillary_files_signer_config: AncillaryFilesSignerConfig,
148
149 #[example = "`MithrilStakeDistribution,CardanoImmutableFilesFull,CardanoStakeDistribution`"]
155 pub signed_entity_types: Option<String>,
156
157 #[example = "`gzip` or `zstandard`"]
159 pub snapshot_compression_algorithm: CompressionAlgorithm,
160
161 #[example = "`{ level: 9, number_of_workers: 4 }`"]
164 pub zstandard_parameters: Option<ZstandardCompressionParameters>,
165
166 pub cexplorer_pools_url: Option<String>,
168
169 pub signer_importer_run_interval: u64,
171
172 pub allow_unparsable_block: bool,
176
177 pub cardano_transactions_prover_cache_pool_size: usize,
179
180 pub cardano_transactions_database_connection_pool_size: usize,
182
183 #[example = "`{ security_parameter: 3000, step: 120 }`"]
185 pub cardano_transactions_signing_config: CardanoTransactionsSigningConfig,
186
187 pub cardano_transactions_prover_max_hashes_allowed_by_request: usize,
189
190 pub cardano_transactions_block_streamer_max_roll_forwards_per_poll: usize,
192
193 pub enable_metrics_server: bool,
195
196 pub metrics_server_ip: String,
198
199 pub metrics_server_port: u16,
201
202 pub persist_usage_report_interval_in_seconds: u64,
204
205 pub leader_aggregator_endpoint: Option<String>,
211}
212
213#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
215#[serde(rename_all = "lowercase")]
216pub enum SnapshotUploaderType {
217 Gcp,
219 Local,
221}
222
223#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
225pub struct ZstandardCompressionParameters {
226 pub level: i32,
228
229 pub number_of_workers: u32,
231}
232
233impl Default for ZstandardCompressionParameters {
234 fn default() -> Self {
235 Self {
236 level: 9,
237 number_of_workers: 4,
238 }
239 }
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
246#[serde(rename_all = "kebab-case", tag = "type")]
247pub enum AncillaryFilesSignerConfig {
248 SecretKey {
250 secret_key: HexEncodedKey,
252 },
253}
254
255impl FromStr for AncillaryFilesSignerConfig {
256 type Err = serde_json::Error;
257
258 fn from_str(s: &str) -> Result<Self, Self::Err> {
259 serde_json::from_str(s)
260 }
261}
262
263impl Configuration {
264 pub fn new_sample(tmp_path: PathBuf) -> Self {
266 let genesis_verification_key = ProtocolGenesisSigner::create_deterministic_signer()
267 .create_verifier()
268 .to_verification_key();
269 let ancillary_files_signer_secret_key =
270 ManifestSigner::create_deterministic_signer().secret_key();
271
272 Self {
273 environment: ExecutionEnvironment::Test,
274 cardano_cli_path: PathBuf::new(),
275 cardano_node_socket_path: PathBuf::new(),
276 cardano_node_version: "0.0.1".to_string(),
277 network_magic: Some(42),
278 network: "devnet".to_string(),
279 chain_observer_type: ChainObserverType::Fake,
280 protocol_parameters: ProtocolParameters {
281 k: 5,
282 m: 100,
283 phi_f: 0.95,
284 },
285 snapshot_uploader_type: SnapshotUploaderType::Local,
286 snapshot_bucket_name: None,
287 snapshot_use_cdn_domain: false,
288 server_ip: "0.0.0.0".to_string(),
289 server_port: 8000,
290 public_server_url: None,
291 run_interval: 5000,
292 db_directory: PathBuf::new(),
293 snapshot_directory: tmp_path,
300 data_stores_directory: PathBuf::from(":memory:"),
301 genesis_verification_key: genesis_verification_key.to_json_hex().unwrap(),
302 reset_digests_cache: false,
303 disable_digests_cache: false,
304 store_retention_limit: None,
305 era_reader_adapter_type: EraReaderAdapterType::Bootstrap,
306 era_reader_adapter_params: None,
307 ancillary_files_signer_config: AncillaryFilesSignerConfig::SecretKey {
308 secret_key: ancillary_files_signer_secret_key.to_json_hex().unwrap(),
309 },
310 signed_entity_types: None,
311 snapshot_compression_algorithm: CompressionAlgorithm::Zstandard,
312 zstandard_parameters: Some(ZstandardCompressionParameters::default()),
313 cexplorer_pools_url: None,
314 signer_importer_run_interval: 1,
315 allow_unparsable_block: false,
316 cardano_transactions_prover_cache_pool_size: 3,
317 cardano_transactions_database_connection_pool_size: 5,
318 cardano_transactions_signing_config: CardanoTransactionsSigningConfig {
319 security_parameter: BlockNumber(120),
320 step: BlockNumber(15),
321 },
322 cardano_transactions_prover_max_hashes_allowed_by_request: 100,
323 cardano_transactions_block_streamer_max_roll_forwards_per_poll: 1000,
324 enable_metrics_server: true,
325 metrics_server_ip: "0.0.0.0".to_string(),
326 metrics_server_port: 9090,
327 persist_usage_report_interval_in_seconds: 10,
328 leader_aggregator_endpoint: None,
329 }
330 }
331
332 pub fn get_local_server_url(&self) -> StdResult<SanitizedUrlWithTrailingSlash> {
334 SanitizedUrlWithTrailingSlash::parse(&format!(
335 "http://{}:{}/{SERVER_BASE_PATH}/",
336 self.server_ip, self.server_port
337 ))
338 }
339
340 pub fn get_server_url(&self) -> StdResult<SanitizedUrlWithTrailingSlash> {
344 match &self.public_server_url {
345 Some(url) => SanitizedUrlWithTrailingSlash::parse(url),
346 None => self.get_local_server_url(),
347 }
348 }
349
350 pub fn get_network(&self) -> StdResult<CardanoNetwork> {
352 CardanoNetwork::from_code(self.network.clone(), self.network_magic)
353 .map_err(|e| anyhow!(ConfigError::Message(e.to_string())))
354 }
355
356 pub fn get_sqlite_dir(&self) -> PathBuf {
358 let store_dir = &self.data_stores_directory;
359
360 if !store_dir.exists() {
361 std::fs::create_dir_all(store_dir).unwrap();
362 }
363
364 self.data_stores_directory.clone()
365 }
366
367 pub fn get_snapshot_dir(&self) -> StdResult<PathBuf> {
369 if !&self.snapshot_directory.exists() {
370 std::fs::create_dir_all(&self.snapshot_directory)?;
371 }
372
373 Ok(self.snapshot_directory.clone())
374 }
375
376 pub fn safe_epoch_retention_limit(&self) -> Option<u64> {
382 self.store_retention_limit
383 .map(|limit| if limit > 3 { limit as u64 } else { 3 })
384 }
385
386 pub fn compute_allowed_signed_entity_types_discriminants(
388 &self,
389 ) -> StdResult<BTreeSet<SignedEntityTypeDiscriminants>> {
390 let allowed_discriminants = self
391 .signed_entity_types
392 .as_ref()
393 .map(SignedEntityTypeDiscriminants::parse_list)
394 .transpose()
395 .with_context(|| "Invalid 'signed_entity_types' configuration")?
396 .unwrap_or_default();
397 let allowed_discriminants =
398 SignedEntityConfig::append_allowed_signed_entity_types_discriminants(
399 allowed_discriminants,
400 );
401
402 Ok(allowed_discriminants)
403 }
404
405 pub fn allow_http_serve_directory(&self) -> bool {
408 match self.snapshot_uploader_type {
409 SnapshotUploaderType::Local => true,
410 SnapshotUploaderType::Gcp => false,
411 }
412 }
413
414 pub fn get_epoch_settings_configuration(&mut self) -> AggregatorEpochSettings {
416 AggregatorEpochSettings {
417 protocol_parameters: self.protocol_parameters.clone(),
418 cardano_transactions_signing_config: self.cardano_transactions_signing_config.clone(),
419 }
420 }
421
422 pub fn is_follower_aggregator(&self) -> bool {
424 self.leader_aggregator_endpoint.is_some()
425 }
426}
427
428#[derive(Debug, Clone, DocumenterDefault)]
430pub struct DefaultConfiguration {
431 pub environment: ExecutionEnvironment,
433
434 pub server_ip: String,
436
437 pub server_port: String,
439
440 pub db_directory: String,
442
443 pub snapshot_directory: String,
445
446 #[example = "`gcp` or `local`"]
448 pub snapshot_store_type: String,
449
450 pub snapshot_uploader_type: String,
452
453 pub era_reader_adapter_type: String,
455
456 pub chain_observer_type: String,
458
459 pub reset_digests_cache: String,
461
462 pub disable_digests_cache: String,
464
465 pub snapshot_compression_algorithm: String,
467
468 pub snapshot_use_cdn_domain: String,
470
471 pub signer_importer_run_interval: u64,
473
474 pub allow_unparsable_block: String,
478
479 pub cardano_transactions_prover_cache_pool_size: u32,
481
482 pub cardano_transactions_database_connection_pool_size: u32,
484
485 pub cardano_transactions_signing_config: CardanoTransactionsSigningConfig,
487
488 pub cardano_transactions_prover_max_hashes_allowed_by_request: u32,
490
491 pub cardano_transactions_block_streamer_max_roll_forwards_per_poll: u32,
493
494 pub enable_metrics_server: String,
496
497 pub metrics_server_ip: String,
499
500 pub metrics_server_port: u16,
502
503 pub persist_usage_report_interval_in_seconds: u64,
505}
506
507impl Default for DefaultConfiguration {
508 fn default() -> Self {
509 Self {
510 environment: ExecutionEnvironment::Production,
511 server_ip: "0.0.0.0".to_string(),
512 server_port: "8080".to_string(),
513 db_directory: "/db".to_string(),
514 snapshot_directory: ".".to_string(),
515 snapshot_store_type: "local".to_string(),
516 snapshot_uploader_type: "gcp".to_string(),
517 era_reader_adapter_type: "bootstrap".to_string(),
518 chain_observer_type: "pallas".to_string(),
519 reset_digests_cache: "false".to_string(),
520 disable_digests_cache: "false".to_string(),
521 snapshot_compression_algorithm: "zstandard".to_string(),
522 snapshot_use_cdn_domain: "false".to_string(),
523 signer_importer_run_interval: 720,
524 allow_unparsable_block: "false".to_string(),
525 cardano_transactions_prover_cache_pool_size: 10,
526 cardano_transactions_database_connection_pool_size: 10,
527 cardano_transactions_signing_config: CardanoTransactionsSigningConfig {
528 security_parameter: BlockNumber(3000),
529 step: BlockNumber(120),
530 },
531 cardano_transactions_prover_max_hashes_allowed_by_request: 100,
532 cardano_transactions_block_streamer_max_roll_forwards_per_poll: 10000,
533 enable_metrics_server: "false".to_string(),
534 metrics_server_ip: "0.0.0.0".to_string(),
535 metrics_server_port: 9090,
536 persist_usage_report_interval_in_seconds: 10,
537 }
538 }
539}
540
541impl DefaultConfiguration {
542 fn namespace() -> String {
543 "default configuration".to_string()
544 }
545}
546
547impl From<ExecutionEnvironment> for ValueKind {
548 fn from(value: ExecutionEnvironment) -> Self {
549 match value {
550 ExecutionEnvironment::Production => ValueKind::String("Production".to_string()),
551 ExecutionEnvironment::Test => ValueKind::String("Test".to_string()),
552 }
553 }
554}
555
556impl Source for DefaultConfiguration {
557 fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
558 Box::new(self.clone())
559 }
560
561 fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
562 let mut result = Map::new();
563
564 let namespace = DefaultConfiguration::namespace();
565
566 let myself = self.clone();
567 register_config_value!(result, &namespace, myself.environment);
568 register_config_value!(result, &namespace, myself.server_ip);
569 register_config_value!(result, &namespace, myself.server_port);
570 register_config_value!(result, &namespace, myself.db_directory);
571 register_config_value!(result, &namespace, myself.snapshot_directory);
572 register_config_value!(result, &namespace, myself.snapshot_store_type);
573 register_config_value!(result, &namespace, myself.snapshot_uploader_type);
574 register_config_value!(result, &namespace, myself.era_reader_adapter_type);
575 register_config_value!(result, &namespace, myself.reset_digests_cache);
576 register_config_value!(result, &namespace, myself.disable_digests_cache);
577 register_config_value!(result, &namespace, myself.snapshot_compression_algorithm);
578 register_config_value!(result, &namespace, myself.snapshot_use_cdn_domain);
579 register_config_value!(result, &namespace, myself.signer_importer_run_interval);
580 register_config_value!(result, &namespace, myself.allow_unparsable_block);
581 register_config_value!(
582 result,
583 &namespace,
584 myself.cardano_transactions_prover_cache_pool_size
585 );
586 register_config_value!(
587 result,
588 &namespace,
589 myself.cardano_transactions_database_connection_pool_size
590 );
591 register_config_value!(
592 result,
593 &namespace,
594 myself.cardano_transactions_prover_max_hashes_allowed_by_request
595 );
596 register_config_value!(
597 result,
598 &namespace,
599 myself.cardano_transactions_block_streamer_max_roll_forwards_per_poll
600 );
601 register_config_value!(result, &namespace, myself.enable_metrics_server);
602 register_config_value!(result, &namespace, myself.metrics_server_ip);
603 register_config_value!(result, &namespace, myself.metrics_server_port);
604 register_config_value!(
605 result,
606 &namespace,
607 myself.persist_usage_report_interval_in_seconds
608 );
609 register_config_value!(
610 result,
611 &namespace,
612 myself.cardano_transactions_signing_config,
613 |v: CardanoTransactionsSigningConfig| HashMap::from([
614 (
615 "security_parameter".to_string(),
616 ValueKind::from(*v.security_parameter,),
617 ),
618 ("step".to_string(), ValueKind::from(*v.step),)
619 ])
620 );
621 Ok(result)
622 }
623}
624
625#[cfg(test)]
626mod test {
627 use mithril_common::temp_dir;
628
629 use super::*;
630
631 #[test]
632 fn safe_epoch_retention_limit_wont_change_a_value_higher_than_three() {
633 for limit in 4..=10u64 {
634 let configuration = Configuration {
635 store_retention_limit: Some(limit as usize),
636 ..Configuration::new_sample(temp_dir!())
637 };
638 assert_eq!(configuration.safe_epoch_retention_limit(), Some(limit));
639 }
640 }
641
642 #[test]
643 fn safe_epoch_retention_limit_wont_change_a_none_value() {
644 let configuration = Configuration {
645 store_retention_limit: None,
646 ..Configuration::new_sample(temp_dir!())
647 };
648 assert_eq!(configuration.safe_epoch_retention_limit(), None);
649 }
650
651 #[test]
652 fn safe_epoch_retention_limit_wont_yield_a_value_lower_than_three() {
653 for limit in 0..=3 {
654 let configuration = Configuration {
655 store_retention_limit: Some(limit),
656 ..Configuration::new_sample(temp_dir!())
657 };
658 assert_eq!(configuration.safe_epoch_retention_limit(), Some(3));
659 }
660 }
661
662 #[test]
663 fn can_build_config_with_ctx_signing_config_from_default_configuration() {
664 #[derive(Debug, Deserialize)]
665 struct TargetConfig {
666 cardano_transactions_signing_config: CardanoTransactionsSigningConfig,
667 }
668
669 let config_builder = config::Config::builder().add_source(DefaultConfiguration::default());
670 let target: TargetConfig = config_builder.build().unwrap().try_deserialize().unwrap();
671
672 assert_eq!(
673 target.cardano_transactions_signing_config,
674 DefaultConfiguration::default().cardano_transactions_signing_config
675 );
676 }
677
678 #[test]
679 fn compute_allowed_signed_entity_types_discriminants_append_default_discriminants() {
680 let config = Configuration {
681 signed_entity_types: None,
682 ..Configuration::new_sample(temp_dir!())
683 };
684
685 assert_eq!(
686 config
687 .compute_allowed_signed_entity_types_discriminants()
688 .unwrap(),
689 BTreeSet::from(SignedEntityConfig::DEFAULT_ALLOWED_DISCRIMINANTS)
690 );
691 }
692
693 #[test]
694 fn allow_http_serve_directory() {
695 let config = Configuration {
696 snapshot_uploader_type: SnapshotUploaderType::Local,
697 ..Configuration::new_sample(temp_dir!())
698 };
699
700 assert!(config.allow_http_serve_directory());
701
702 let config = Configuration {
703 snapshot_uploader_type: SnapshotUploaderType::Gcp,
704 ..Configuration::new_sample(temp_dir!())
705 };
706
707 assert!(!config.allow_http_serve_directory());
708 }
709
710 #[test]
711 fn get_server_url_return_local_url_with_server_base_path_if_public_url_is_not_set() {
712 let config = Configuration {
713 server_ip: "1.2.3.4".to_string(),
714 server_port: 5678,
715 public_server_url: None,
716 ..Configuration::new_sample(temp_dir!())
717 };
718
719 assert_eq!(
720 config.get_server_url().unwrap().as_str(),
721 &format!("http://1.2.3.4:5678/{SERVER_BASE_PATH}/")
722 );
723 }
724
725 #[test]
726 fn get_server_url_return_sanitized_public_url_if_it_is_set() {
727 let config = Configuration {
728 server_ip: "1.2.3.4".to_string(),
729 server_port: 5678,
730 public_server_url: Some("https://example.com".to_string()),
731 ..Configuration::new_sample(temp_dir!())
732 };
733
734 assert_eq!(
735 config.get_server_url().unwrap().as_str(),
736 "https://example.com/"
737 );
738 }
739
740 #[test]
741 fn joining_to_local_server_url_keep_base_path() {
742 let config = Configuration {
743 server_ip: "1.2.3.4".to_string(),
744 server_port: 6789,
745 public_server_url: None,
746 ..Configuration::new_sample(temp_dir!())
747 };
748
749 let joined_url = config
750 .get_local_server_url()
751 .unwrap()
752 .join("some/path")
753 .unwrap();
754 assert!(
755 joined_url.as_str().contains(SERVER_BASE_PATH),
756 "Joined URL `{joined_url}`, does not contain base path `{SERVER_BASE_PATH}`"
757 );
758 }
759
760 #[test]
761 fn joining_to_public_server_url_without_trailing_slash() {
762 let subpath_without_trailing_slash = "subpath_without_trailing_slash";
763 let config = Configuration {
764 public_server_url: Some(format!(
765 "https://example.com/{subpath_without_trailing_slash}"
766 )),
767 ..Configuration::new_sample(temp_dir!())
768 };
769
770 let joined_url = config.get_server_url().unwrap().join("some/path").unwrap();
771 assert!(
772 joined_url.as_str().contains(subpath_without_trailing_slash),
773 "Joined URL `{joined_url}`, does not contain subpath `{subpath_without_trailing_slash}`"
774 );
775 }
776
777 #[test]
778 fn is_follower_aggregator_returns_true_when_in_follower_mode() {
779 let config = Configuration {
780 leader_aggregator_endpoint: Some("some_endpoint".to_string()),
781 ..Configuration::new_sample(temp_dir!())
782 };
783
784 assert!(config.is_follower_aggregator());
785 }
786
787 #[test]
788 fn is_follower_aggregator_returns_false_when_in_leader_mode() {
789 let config = Configuration {
790 leader_aggregator_endpoint: None,
791 ..Configuration::new_sample(temp_dir!())
792 };
793
794 assert!(!config.is_follower_aggregator());
795 }
796
797 #[test]
798 fn serialized_ancillary_files_signer_config_use_snake_case_for_keys_and_kebab_case_for_type_value(
799 ) {
800 let serialized_json = r#"{
801 "type": "secret-key",
802 "secret_key": "whatever"
803 }"#;
804
805 let deserialized: AncillaryFilesSignerConfig =
806 serde_json::from_str(serialized_json).unwrap();
807 assert_eq!(
808 deserialized,
809 AncillaryFilesSignerConfig::SecretKey {
810 secret_key: "whatever".to_string()
811 }
812 );
813 }
814}