mithril_aggregator/
configuration.rs

1use anyhow::Context;
2use config::{ConfigError, Map, Source, Value, ValueKind};
3use serde::{Deserialize, Serialize};
4use std::collections::{BTreeSet, HashMap, HashSet};
5use std::path::PathBuf;
6use std::str::FromStr;
7
8use mithril_cardano_node_chain::chain_observer::ChainObserverType;
9use mithril_cli_helper::{register_config_value, serde_deserialization};
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::{CardanoNetwork, StdResult};
17use mithril_doc::{Documenter, DocumenterDefault, StructDoc};
18use mithril_era::adapters::EraReaderAdapterType;
19
20use crate::entities::AggregatorEpochSettings;
21use crate::http_server::SERVER_BASE_PATH;
22use crate::services::ancillary_signer::GcpCryptoKeyVersionResourceName;
23use crate::tools::DEFAULT_GCP_CREDENTIALS_JSON_ENV_VAR;
24use crate::tools::url_sanitizer::SanitizedUrlWithTrailingSlash;
25
26/// Different kinds of execution environments
27#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
28pub enum ExecutionEnvironment {
29    /// Test environment, maximum logging, memory stores etc.
30    Test,
31
32    /// Production environment, minimum logging, maximum performances,
33    /// persistent stores etc.
34    Production,
35}
36
37impl FromStr for ExecutionEnvironment {
38    type Err = ConfigError;
39
40    fn from_str(s: &str) -> Result<Self, Self::Err> {
41        match s {
42            "production" => Ok(Self::Production),
43            "test" => Ok(Self::Test),
44            _ => Err(ConfigError::Message(format!(
45                "Unknown execution environment {s}"
46            ))),
47        }
48    }
49}
50
51/// This trait defines the configuration interface for the aggregator.
52///
53/// By default, each function panics if not overridden, forcing concrete configuration
54/// implementations to explicitly provide the necessary values.
55pub trait ConfigurationSource {
56    /// What kind of runtime environment the configuration is meant to.
57    fn environment(&self) -> ExecutionEnvironment;
58
59    /// Cardano CLI tool path
60    fn cardano_cli_path(&self) -> PathBuf {
61        panic!("cardano_cli_path is not implemented.");
62    }
63
64    /// Path of the socket opened by the Cardano node
65    fn cardano_node_socket_path(&self) -> PathBuf {
66        panic!("cardano_node_socket_path is not implemented.");
67    }
68
69    /// Path of the socket opened by the DMQ node
70    fn dmq_node_socket_path(&self) -> Option<PathBuf> {
71        panic!("dmq_node_socket_path is not implemented.");
72    }
73
74    /// Cardano node version.
75    ///
76    /// **NOTE**: This cannot be verified for now (see [this
77    /// issue](https://github.com/input-output-hk/cardano-cli/issues/224)). This
78    /// is why it has to be manually given to the Aggregator
79    fn cardano_node_version(&self) -> String {
80        panic!("cardano_node_version is not implemented.");
81    }
82
83    /// Cardano Network Magic number
84    ///
85    /// useful for TestNet & DevNet
86    fn network_magic(&self) -> Option<u64> {
87        panic!("network_magic is not implemented.");
88    }
89
90    /// Cardano network
91    fn network(&self) -> String {
92        panic!("network is not implemented.");
93    }
94
95    /// Cardano chain observer type
96    fn chain_observer_type(&self) -> ChainObserverType {
97        panic!("chain_observer_type is not implemented.");
98    }
99
100    /// Protocol parameters
101    fn protocol_parameters(&self) -> ProtocolParameters {
102        panic!("protocol_parameters is not implemented.");
103    }
104
105    /// Type of snapshot uploader to use
106    fn snapshot_uploader_type(&self) -> SnapshotUploaderType {
107        panic!("snapshot_uploader_type is not implemented.");
108    }
109
110    /// Bucket name where the snapshots are stored if snapshot_uploader_type is Gcp
111    fn snapshot_bucket_name(&self) -> Option<String> {
112        panic!("snapshot_bucket_name is not implemented.");
113    }
114
115    /// Use CDN domain to construct snapshot urls if snapshot_uploader_type is Gcp
116    fn snapshot_use_cdn_domain(&self) -> bool {
117        panic!("snapshot_use_cdn_domain is not implemented.");
118    }
119
120    /// Server listening IP
121    fn server_ip(&self) -> String {
122        panic!("server_ip is not implemented.");
123    }
124
125    /// Server listening port
126    fn server_port(&self) -> u16 {
127        panic!("server_port is not implemented.");
128    }
129
130    /// Server URL that can be accessed from the outside
131    fn public_server_url(&self) -> Option<String> {
132        panic!("public_server_url is not implemented.");
133    }
134
135    /// Run Interval is the interval between two runtime cycles in ms
136    fn run_interval(&self) -> u64 {
137        panic!("run_interval is not implemented.");
138    }
139
140    /// Directory of the Cardano node store.
141    fn db_directory(&self) -> PathBuf {
142        panic!("db_directory is not implemented.");
143    }
144
145    /// Directory to store snapshot
146    fn snapshot_directory(&self) -> PathBuf {
147        panic!("snapshot_directory is not implemented.");
148    }
149
150    /// Directory to store aggregator databases
151    fn data_stores_directory(&self) -> PathBuf {
152        panic!("data_stores_directory is not implemented.");
153    }
154
155    /// Genesis verification key
156    fn genesis_verification_key(&self) -> HexEncodedGenesisVerificationKey {
157        panic!("genesis_verification_key is not implemented.");
158    }
159
160    /// Should the immutable cache be reset or not
161    fn reset_digests_cache(&self) -> bool {
162        panic!("reset_digests_cache is not implemented.");
163    }
164
165    /// Use the digest caching strategy
166    fn disable_digests_cache(&self) -> bool {
167        panic!("disable_digests_cache is not implemented.");
168    }
169
170    /// Max number of records in stores.
171    /// When new records are added, oldest records are automatically deleted so
172    /// there can always be at max the number of records specified by this
173    /// setting.
174    fn store_retention_limit(&self) -> Option<usize> {
175        panic!("store_retention_limit is not implemented.");
176    }
177
178    /// Era reader adapter type
179    fn era_reader_adapter_type(&self) -> EraReaderAdapterType {
180        panic!("era_reader_adapter_type is not implemented.");
181    }
182
183    /// Era reader adapter parameters
184    fn era_reader_adapter_params(&self) -> Option<String> {
185        panic!("era_reader_adapter_params is not implemented.");
186    }
187
188    /// Configuration of the ancillary files signer
189    ///
190    /// **IMPORTANT**: The cryptographic scheme used is ED25519
191    fn ancillary_files_signer_config(&self) -> AncillaryFilesSignerConfig {
192        panic!("ancillary_files_signer_config is not implemented.");
193    }
194
195    /// Signed entity types parameters (discriminants names in an ordered, case-sensitive, comma
196    /// separated list).
197    ///
198    /// The values `MithrilStakeDistribution` and `CardanoImmutableFilesFull` are prepended
199    /// automatically to the list.
200    fn signed_entity_types(&self) -> Option<String> {
201        panic!("signed_entity_types is not implemented.");
202    }
203
204    /// Compression algorithm used for the snapshot archive artifacts.
205    fn snapshot_compression_algorithm(&self) -> CompressionAlgorithm {
206        panic!("snapshot_compression_algorithm is not implemented.");
207    }
208
209    /// Specific parameters when [CompressionAlgorithm] is set to
210    /// [zstandard][CompressionAlgorithm::Zstandard].
211    fn zstandard_parameters(&self) -> Option<ZstandardCompressionParameters> {
212        panic!("zstandard_parameters is not implemented.");
213    }
214
215    /// Url to CExplorer list of pools to import as signer in the database.
216    fn cexplorer_pools_url(&self) -> Option<String> {
217        panic!("cexplorer_pools_url is not implemented.");
218    }
219
220    /// Time interval at which the signers in `cexplorer_pools_url` will be imported (in minutes).
221    fn signer_importer_run_interval(&self) -> u64 {
222        panic!("signer_importer_run_interval is not implemented.");
223    }
224
225    /// If set no error is returned in case of unparsable block and an error log is written instead.
226    ///
227    /// Will be ignored on (pre)production networks.
228    fn allow_unparsable_block(&self) -> bool {
229        panic!("allow_unparsable_block is not implemented.");
230    }
231
232    /// Cardano transactions prover cache pool size
233    fn cardano_transactions_prover_cache_pool_size(&self) -> usize {
234        panic!("cardano_transactions_prover_cache_pool_size is not implemented.");
235    }
236
237    /// Cardano transactions database connection pool size
238    fn cardano_transactions_database_connection_pool_size(&self) -> usize {
239        panic!("cardano_transactions_database_connection_pool_size is not implemented.");
240    }
241
242    /// Cardano transactions signing configuration
243    fn cardano_transactions_signing_config(&self) -> CardanoTransactionsSigningConfig {
244        panic!("cardano_transactions_signing_config is not implemented.");
245    }
246
247    /// Maximum number of transactions hashes allowed by request to the prover of the Cardano transactions
248    fn cardano_transactions_prover_max_hashes_allowed_by_request(&self) -> usize {
249        panic!("cardano_transactions_prover_max_hashes_allowed_by_request is not implemented.");
250    }
251
252    /// The maximum number of roll forwards during a poll of the block streamer when importing transactions.
253    fn cardano_transactions_block_streamer_max_roll_forwards_per_poll(&self) -> usize {
254        panic!(
255            "cardano_transactions_block_streamer_max_roll_forwards_per_poll is not implemented."
256        );
257    }
258
259    /// Enable metrics server (Prometheus endpoint on /metrics).
260    fn enable_metrics_server(&self) -> bool {
261        panic!("enable_metrics_server is not implemented.");
262    }
263
264    /// Metrics HTTP Server IP.
265    fn metrics_server_ip(&self) -> String {
266        panic!("metrics_server_ip is not implemented.");
267    }
268
269    /// Metrics HTTP Server listening port.
270    fn metrics_server_port(&self) -> u16 {
271        panic!("metrics_server_port is not implemented.");
272    }
273
274    /// Time interval at which usage metrics are persisted in event database (in seconds).
275    fn persist_usage_report_interval_in_seconds(&self) -> u64 {
276        panic!("persist_usage_report_interval_in_seconds is not implemented.");
277    }
278
279    /// Leader aggregator endpoint
280    ///
281    /// This is the endpoint of the aggregator that will be used to fetch the latest epoch settings
282    /// and store the signer registrations when the aggregator is running in a follower mode.
283    /// If this is not set, the aggregator will run in a leader mode.
284    fn leader_aggregator_endpoint(&self) -> Option<String> {
285        panic!("leader_aggregator_endpoint is not implemented.");
286    }
287
288    /// Custom origin tag of client request added to the whitelist (comma
289    /// separated list).
290    fn custom_origin_tag_white_list(&self) -> Option<String> {
291        panic!("custom_origin_tag_white_list is not implemented.");
292    }
293
294    /// Get the server URL.
295    fn get_server_url(&self) -> StdResult<SanitizedUrlWithTrailingSlash> {
296        panic!("get_server_url is not implemented.");
297    }
298
299    /// Get a representation of the Cardano network.
300    fn get_network(&self) -> StdResult<CardanoNetwork> {
301        CardanoNetwork::from_code(self.network(), self.network_magic())
302            .with_context(|| "Invalid network configuration")
303    }
304
305    /// Get the directory of the SQLite stores.
306    fn get_sqlite_dir(&self) -> PathBuf {
307        let store_dir = &self.data_stores_directory();
308
309        if !store_dir.exists() {
310            std::fs::create_dir_all(store_dir).unwrap();
311        }
312
313        self.data_stores_directory()
314    }
315
316    /// Get the snapshots directory.
317    fn get_snapshot_dir(&self) -> StdResult<PathBuf> {
318        if !&self.snapshot_directory().exists() {
319            std::fs::create_dir_all(self.snapshot_directory())?;
320        }
321
322        Ok(self.snapshot_directory())
323    }
324
325    /// Get the safe epoch retention limit.
326    fn safe_epoch_retention_limit(&self) -> Option<u64> {
327        self.store_retention_limit()
328            .map(|limit| if limit > 3 { limit as u64 } else { 3 })
329    }
330
331    /// Compute the list of signed entity discriminants that are allowed to be processed.
332    fn compute_allowed_signed_entity_types_discriminants(
333        &self,
334    ) -> StdResult<BTreeSet<SignedEntityTypeDiscriminants>> {
335        let allowed_discriminants = self
336            .signed_entity_types()
337            .as_ref()
338            .map(SignedEntityTypeDiscriminants::parse_list)
339            .transpose()
340            .with_context(|| "Invalid 'signed_entity_types' configuration")?
341            .unwrap_or_default();
342        let allowed_discriminants =
343            SignedEntityConfig::append_allowed_signed_entity_types_discriminants(
344                allowed_discriminants,
345            );
346
347        Ok(allowed_discriminants)
348    }
349
350    /// Check if the HTTP server can serve static directories.
351    fn allow_http_serve_directory(&self) -> bool {
352        match self.snapshot_uploader_type() {
353            SnapshotUploaderType::Local => true,
354            SnapshotUploaderType::Gcp => false,
355        }
356    }
357
358    /// Infer the [AggregatorEpochSettings] from the configuration.
359    fn get_epoch_settings_configuration(&self) -> AggregatorEpochSettings {
360        AggregatorEpochSettings {
361            protocol_parameters: self.protocol_parameters(),
362            cardano_transactions_signing_config: self.cardano_transactions_signing_config(),
363        }
364    }
365
366    /// Check if the aggregator is running in follower mode.
367    fn is_follower_aggregator(&self) -> bool {
368        self.leader_aggregator_endpoint().is_some()
369    }
370
371    /// White list for origin client request.
372    fn compute_origin_tag_white_list(&self) -> HashSet<String> {
373        let mut white_list = HashSet::from([
374            "EXPLORER".to_string(),
375            "BENCHMARK".to_string(),
376            "CI".to_string(),
377            "NA".to_string(),
378        ]);
379        if let Some(custom_tags) = &self.custom_origin_tag_white_list() {
380            white_list.extend(custom_tags.split(',').map(|tag| tag.trim().to_string()));
381        }
382
383        white_list
384    }
385}
386
387/// Serve command configuration
388#[derive(Debug, Clone, Serialize, Deserialize, Documenter)]
389pub struct ServeCommandConfiguration {
390    /// What kind of runtime environment the configuration is meant to.
391    pub environment: ExecutionEnvironment,
392
393    /// Cardano CLI tool path
394    #[example = "`cardano-cli`"]
395    pub cardano_cli_path: PathBuf,
396
397    /// Path of the socket opened by the Cardano node
398    #[example = "`/ipc/node.socket`"]
399    pub cardano_node_socket_path: PathBuf,
400
401    /// Path of the socket opened by the DMQ node
402    #[example = "`/ipc/dmq.socket`"]
403    pub dmq_node_socket_path: Option<PathBuf>,
404
405    /// Cardano node version.
406    ///
407    /// **NOTE**: This cannot be verified for now (see [this
408    /// issue](https://github.com/input-output-hk/cardano-cli/issues/224)). This
409    /// is why it has to be manually given to the Aggregator
410    pub cardano_node_version: String,
411
412    /// Cardano Network Magic number
413    ///
414    /// useful for TestNet & DevNet
415    #[example = "`1097911063` or `42`"]
416    pub network_magic: Option<u64>,
417
418    /// Cardano network
419    #[example = "`testnet` or `mainnet` or `devnet`"]
420    pub network: String,
421
422    /// Cardano chain observer type
423    pub chain_observer_type: ChainObserverType,
424
425    /// Protocol parameters
426    #[example = "`{ k: 5, m: 100, phi_f: 0.65 }`"]
427    pub protocol_parameters: ProtocolParameters,
428
429    /// Type of snapshot uploader to use
430    #[example = "`gcp` or `local`"]
431    pub snapshot_uploader_type: SnapshotUploaderType,
432
433    /// Bucket name where the snapshots are stored if snapshot_uploader_type is Gcp
434    pub snapshot_bucket_name: Option<String>,
435
436    /// Use CDN domain to construct snapshot urls if snapshot_uploader_type is Gcp
437    pub snapshot_use_cdn_domain: bool,
438
439    /// Server listening IP
440    pub server_ip: String,
441
442    /// Server listening port
443    pub server_port: u16,
444
445    /// Server URL that can be accessed from the outside
446    pub public_server_url: Option<String>,
447
448    /// Run Interval is the interval between two runtime cycles in ms
449    #[example = "`60000`"]
450    pub run_interval: u64,
451
452    /// Directory of the Cardano node store.
453    pub db_directory: PathBuf,
454
455    /// Directory to store snapshot
456    pub snapshot_directory: PathBuf,
457
458    /// Directory to store aggregator databases
459    #[example = "`./mithril-aggregator/stores`"]
460    pub data_stores_directory: PathBuf,
461
462    /// Genesis verification key
463    pub genesis_verification_key: HexEncodedGenesisVerificationKey,
464
465    /// Should the immutable cache be reset or not
466    pub reset_digests_cache: bool,
467
468    /// Use the digest caching strategy
469    pub disable_digests_cache: bool,
470
471    /// Max number of records in stores.
472    /// When new records are added, oldest records are automatically deleted so
473    /// there can always be at max the number of records specified by this
474    /// setting.
475    pub store_retention_limit: Option<usize>,
476
477    /// Era reader adapter type
478    pub era_reader_adapter_type: EraReaderAdapterType,
479
480    /// Era reader adapter parameters
481    pub era_reader_adapter_params: Option<String>,
482
483    /// Configuration of the ancillary files signer
484    ///
485    /// Can either be a secret key or a key stored in a Google Cloud Platform KMS account.
486    ///
487    /// **IMPORTANT**: The cryptographic scheme used is ED25519
488    #[example = "\
489    - secret-key:<br/>`{ \"type\": \"secret-key\", \"secret_key\": \"136372c3138312c3138382c3130352c3233312c3135\" }`<br/>\
490    - Gcp kms:<br/>`{ \"type\": \"gcp-kms\", \"resource_name\": \"projects/project_name/locations/_location_name/keyRings/key_ring_name/cryptoKeys/key_name/cryptoKeyVersions/key_version\" }`\
491    "]
492    #[serde(deserialize_with = "serde_deserialization::string_or_struct")]
493    pub ancillary_files_signer_config: AncillaryFilesSignerConfig,
494
495    /// Signed entity types parameters (discriminants names in an ordered, case-sensitive, comma
496    /// separated list).
497    ///
498    /// The value `MithrilStakeDistribution` is prepended is automatically to the list.
499    #[example = "`CardanoImmutableFilesFull,CardanoStakeDistribution,CardanoDatabase,CardanoTransactions`"]
500    pub signed_entity_types: Option<String>,
501
502    /// Compression algorithm used for the snapshot archive artifacts.
503    #[example = "`gzip` or `zstandard`"]
504    pub snapshot_compression_algorithm: CompressionAlgorithm,
505
506    /// Specific parameters when [snapshot_compression_algorithm][Self::snapshot_compression_algorithm]
507    /// is set to [zstandard][CompressionAlgorithm::Zstandard].
508    #[example = "`{ level: 9, number_of_workers: 4 }`"]
509    pub zstandard_parameters: Option<ZstandardCompressionParameters>,
510
511    /// Url to CExplorer list of pools to import as signer in the database.
512    pub cexplorer_pools_url: Option<String>,
513
514    /// Time interval at which the signers in [Self::cexplorer_pools_url] will be imported (in minutes).
515    pub signer_importer_run_interval: u64,
516
517    /// If set no error is returned in case of unparsable block and an error log is written instead.
518    ///
519    /// Will be ignored on (pre)production networks.
520    pub allow_unparsable_block: bool,
521
522    /// Cardano transactions prover cache pool size
523    pub cardano_transactions_prover_cache_pool_size: usize,
524
525    /// Cardano transactions database connection pool size
526    pub cardano_transactions_database_connection_pool_size: usize,
527
528    /// Cardano transactions signing configuration
529    #[example = "`{ security_parameter: 3000, step: 120 }`"]
530    pub cardano_transactions_signing_config: CardanoTransactionsSigningConfig,
531
532    /// Maximum number of transactions hashes allowed by request to the prover of the Cardano transactions
533    pub cardano_transactions_prover_max_hashes_allowed_by_request: usize,
534
535    /// The maximum number of roll forwards during a poll of the block streamer when importing transactions.
536    pub cardano_transactions_block_streamer_max_roll_forwards_per_poll: usize,
537
538    /// Enable metrics server (Prometheus endpoint on /metrics).
539    pub enable_metrics_server: bool,
540
541    /// Metrics HTTP Server IP.
542    pub metrics_server_ip: String,
543
544    /// Metrics HTTP Server listening port.
545    pub metrics_server_port: u16,
546
547    /// Time interval at which usage metrics are persisted in event database (in seconds).
548    pub persist_usage_report_interval_in_seconds: u64,
549
550    // Leader aggregator endpoint
551    ///
552    /// This is the endpoint of the aggregator that will be used to fetch the latest epoch settings
553    /// and store the signer registrations when the aggregator is running in a follower mode.
554    /// If this is not set, the aggregator will run in a leader mode.
555    pub leader_aggregator_endpoint: Option<String>,
556
557    /// Custom origin tag of client request added to the whitelist (comma
558    /// separated list).
559    pub custom_origin_tag_white_list: Option<String>,
560}
561
562/// Uploader needed to copy the snapshot once computed.
563#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
564#[serde(rename_all = "lowercase")]
565pub enum SnapshotUploaderType {
566    /// Uploader to GCP storage.
567    Gcp,
568    /// Uploader to local storage.
569    Local,
570}
571
572/// [Zstandard][CompressionAlgorithm::Zstandard] specific parameters
573#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
574pub struct ZstandardCompressionParameters {
575    /// Level of compression, default to 9.
576    pub level: i32,
577
578    /// Number of workers when compressing, 0 will disable multithreading, default to 4.
579    pub number_of_workers: u32,
580}
581
582impl Default for ZstandardCompressionParameters {
583    fn default() -> Self {
584        Self {
585            level: 9,
586            number_of_workers: 4,
587        }
588    }
589}
590
591/// Configuration of the ancillary files signer
592///
593/// **IMPORTANT**: The cryptographic scheme used is ED25519
594#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
595#[serde(rename_all = "kebab-case", tag = "type")]
596pub enum AncillaryFilesSignerConfig {
597    /// Sign with a secret key
598    SecretKey {
599        /// Hex encoded secret key
600        secret_key: HexEncodedKey,
601    },
602    /// Sign with a key stored in a Google Cloud Platform KMS account
603    GcpKms {
604        /// GCP KMS resource name
605        resource_name: GcpCryptoKeyVersionResourceName,
606        /// Environment variable containing the credentials JSON, if not set `GOOGLE_APPLICATION_CREDENTIALS_JSON` will be used
607        #[serde(default = "default_gcp_kms_credentials_json_env_var")]
608        credentials_json_env_var: String,
609    },
610}
611
612fn default_gcp_kms_credentials_json_env_var() -> String {
613    DEFAULT_GCP_CREDENTIALS_JSON_ENV_VAR.to_string()
614}
615
616impl FromStr for AncillaryFilesSignerConfig {
617    type Err = serde_json::Error;
618
619    fn from_str(s: &str) -> Result<Self, Self::Err> {
620        serde_json::from_str(s)
621    }
622}
623
624impl ServeCommandConfiguration {
625    /// Create a sample configuration mainly for tests
626    pub fn new_sample(tmp_path: PathBuf) -> Self {
627        let genesis_verification_key = ProtocolGenesisSigner::create_deterministic_signer()
628            .create_verifier()
629            .to_verification_key();
630        let ancillary_files_signer_secret_key =
631            ManifestSigner::create_deterministic_signer().secret_key();
632
633        Self {
634            environment: ExecutionEnvironment::Test,
635            cardano_cli_path: PathBuf::new(),
636            cardano_node_socket_path: PathBuf::new(),
637            dmq_node_socket_path: None,
638            cardano_node_version: "0.0.1".to_string(),
639            network_magic: Some(42),
640            network: "devnet".to_string(),
641            chain_observer_type: ChainObserverType::Fake,
642            protocol_parameters: ProtocolParameters {
643                k: 5,
644                m: 100,
645                phi_f: 0.95,
646            },
647            snapshot_uploader_type: SnapshotUploaderType::Local,
648            snapshot_bucket_name: None,
649            snapshot_use_cdn_domain: false,
650            server_ip: "0.0.0.0".to_string(),
651            server_port: 8000,
652            public_server_url: None,
653            run_interval: 5000,
654            db_directory: PathBuf::new(),
655            // Note: this is a band-aid solution to avoid IO operations in the `mithril-aggregator`
656            // crate directory.
657            // Know issue:
658            // - There may be collision of the `snapshot_directory` between tests. Tests that
659            // depend on the `snapshot_directory` should specify their own,
660            // and they can use the `temp_dir` macro for that.
661            snapshot_directory: tmp_path,
662            data_stores_directory: PathBuf::from(":memory:"),
663            genesis_verification_key: genesis_verification_key.to_json_hex().unwrap(),
664            reset_digests_cache: false,
665            disable_digests_cache: false,
666            store_retention_limit: None,
667            era_reader_adapter_type: EraReaderAdapterType::Bootstrap,
668            era_reader_adapter_params: None,
669            ancillary_files_signer_config: AncillaryFilesSignerConfig::SecretKey {
670                secret_key: ancillary_files_signer_secret_key.to_json_hex().unwrap(),
671            },
672            signed_entity_types: None,
673            snapshot_compression_algorithm: CompressionAlgorithm::Zstandard,
674            zstandard_parameters: Some(ZstandardCompressionParameters::default()),
675            cexplorer_pools_url: None,
676            signer_importer_run_interval: 1,
677            allow_unparsable_block: false,
678            cardano_transactions_prover_cache_pool_size: 3,
679            cardano_transactions_database_connection_pool_size: 5,
680            cardano_transactions_signing_config: CardanoTransactionsSigningConfig {
681                security_parameter: BlockNumber(120),
682                step: BlockNumber(15),
683            },
684            cardano_transactions_prover_max_hashes_allowed_by_request: 100,
685            cardano_transactions_block_streamer_max_roll_forwards_per_poll: 1000,
686            enable_metrics_server: true,
687            metrics_server_ip: "0.0.0.0".to_string(),
688            metrics_server_port: 9090,
689            persist_usage_report_interval_in_seconds: 10,
690            leader_aggregator_endpoint: None,
691            custom_origin_tag_white_list: None,
692        }
693    }
694
695    /// Build the local server URL from configuration.
696    pub fn get_local_server_url(&self) -> StdResult<SanitizedUrlWithTrailingSlash> {
697        SanitizedUrlWithTrailingSlash::parse(&format!(
698            "http://{}:{}/{SERVER_BASE_PATH}/",
699            self.server_ip, self.server_port
700        ))
701    }
702}
703
704impl ConfigurationSource for ServeCommandConfiguration {
705    fn environment(&self) -> ExecutionEnvironment {
706        self.environment.clone()
707    }
708
709    fn cardano_cli_path(&self) -> PathBuf {
710        self.cardano_cli_path.clone()
711    }
712
713    fn cardano_node_socket_path(&self) -> PathBuf {
714        self.cardano_node_socket_path.clone()
715    }
716
717    fn dmq_node_socket_path(&self) -> Option<PathBuf> {
718        self.dmq_node_socket_path.clone()
719    }
720
721    fn cardano_node_version(&self) -> String {
722        self.cardano_node_version.clone()
723    }
724
725    fn network_magic(&self) -> Option<u64> {
726        self.network_magic
727    }
728
729    fn network(&self) -> String {
730        self.network.clone()
731    }
732
733    fn chain_observer_type(&self) -> ChainObserverType {
734        self.chain_observer_type.clone()
735    }
736
737    fn protocol_parameters(&self) -> ProtocolParameters {
738        self.protocol_parameters.clone()
739    }
740
741    fn snapshot_uploader_type(&self) -> SnapshotUploaderType {
742        self.snapshot_uploader_type
743    }
744
745    fn snapshot_bucket_name(&self) -> Option<String> {
746        self.snapshot_bucket_name.clone()
747    }
748
749    fn snapshot_use_cdn_domain(&self) -> bool {
750        self.snapshot_use_cdn_domain
751    }
752
753    fn server_ip(&self) -> String {
754        self.server_ip.clone()
755    }
756
757    fn server_port(&self) -> u16 {
758        self.server_port
759    }
760
761    fn public_server_url(&self) -> Option<String> {
762        self.public_server_url.clone()
763    }
764
765    fn run_interval(&self) -> u64 {
766        self.run_interval
767    }
768
769    fn db_directory(&self) -> PathBuf {
770        self.db_directory.clone()
771    }
772
773    fn snapshot_directory(&self) -> PathBuf {
774        self.snapshot_directory.clone()
775    }
776
777    fn data_stores_directory(&self) -> PathBuf {
778        self.data_stores_directory.clone()
779    }
780
781    fn genesis_verification_key(&self) -> HexEncodedGenesisVerificationKey {
782        self.genesis_verification_key.clone()
783    }
784
785    fn reset_digests_cache(&self) -> bool {
786        self.reset_digests_cache
787    }
788
789    fn disable_digests_cache(&self) -> bool {
790        self.disable_digests_cache
791    }
792
793    fn store_retention_limit(&self) -> Option<usize> {
794        self.store_retention_limit
795    }
796
797    fn era_reader_adapter_type(&self) -> EraReaderAdapterType {
798        self.era_reader_adapter_type.clone()
799    }
800
801    fn era_reader_adapter_params(&self) -> Option<String> {
802        self.era_reader_adapter_params.clone()
803    }
804
805    fn ancillary_files_signer_config(&self) -> AncillaryFilesSignerConfig {
806        self.ancillary_files_signer_config.clone()
807    }
808
809    fn signed_entity_types(&self) -> Option<String> {
810        self.signed_entity_types.clone()
811    }
812
813    fn snapshot_compression_algorithm(&self) -> CompressionAlgorithm {
814        self.snapshot_compression_algorithm
815    }
816
817    fn zstandard_parameters(&self) -> Option<ZstandardCompressionParameters> {
818        self.zstandard_parameters
819    }
820
821    fn cexplorer_pools_url(&self) -> Option<String> {
822        self.cexplorer_pools_url.clone()
823    }
824
825    fn signer_importer_run_interval(&self) -> u64 {
826        self.signer_importer_run_interval
827    }
828
829    fn allow_unparsable_block(&self) -> bool {
830        self.allow_unparsable_block
831    }
832
833    fn cardano_transactions_prover_cache_pool_size(&self) -> usize {
834        self.cardano_transactions_prover_cache_pool_size
835    }
836
837    fn cardano_transactions_database_connection_pool_size(&self) -> usize {
838        self.cardano_transactions_database_connection_pool_size
839    }
840
841    fn cardano_transactions_signing_config(&self) -> CardanoTransactionsSigningConfig {
842        self.cardano_transactions_signing_config.clone()
843    }
844
845    fn cardano_transactions_prover_max_hashes_allowed_by_request(&self) -> usize {
846        self.cardano_transactions_prover_max_hashes_allowed_by_request
847    }
848
849    fn cardano_transactions_block_streamer_max_roll_forwards_per_poll(&self) -> usize {
850        self.cardano_transactions_block_streamer_max_roll_forwards_per_poll
851    }
852
853    fn enable_metrics_server(&self) -> bool {
854        self.enable_metrics_server
855    }
856
857    fn metrics_server_ip(&self) -> String {
858        self.metrics_server_ip.clone()
859    }
860
861    fn metrics_server_port(&self) -> u16 {
862        self.metrics_server_port
863    }
864
865    fn persist_usage_report_interval_in_seconds(&self) -> u64 {
866        self.persist_usage_report_interval_in_seconds
867    }
868
869    fn leader_aggregator_endpoint(&self) -> Option<String> {
870        self.leader_aggregator_endpoint.clone()
871    }
872
873    fn custom_origin_tag_white_list(&self) -> Option<String> {
874        self.custom_origin_tag_white_list.clone()
875    }
876
877    fn get_server_url(&self) -> StdResult<SanitizedUrlWithTrailingSlash> {
878        match &self.public_server_url {
879            Some(url) => SanitizedUrlWithTrailingSlash::parse(url),
880            None => self.get_local_server_url(),
881        }
882    }
883}
884
885/// Default configuration with all the default values for configurations.
886#[derive(Debug, Clone, DocumenterDefault)]
887pub struct DefaultConfiguration {
888    /// Execution environment
889    pub environment: ExecutionEnvironment,
890
891    /// Server listening IP
892    pub server_ip: String,
893
894    /// Server listening port
895    pub server_port: String,
896
897    /// Directory of the Cardano node database
898    pub db_directory: String,
899
900    /// Directory to store snapshot
901    pub snapshot_directory: String,
902
903    /// Type of snapshot uploader to use
904    pub snapshot_uploader_type: String,
905
906    /// Era reader adapter type
907    pub era_reader_adapter_type: String,
908
909    /// Chain observer type
910    pub chain_observer_type: String,
911
912    /// ImmutableDigesterCacheProvider default setting
913    pub reset_digests_cache: String,
914
915    /// ImmutableDigesterCacheProvider default setting
916    pub disable_digests_cache: String,
917
918    /// Snapshot compression algorithm default setting
919    pub snapshot_compression_algorithm: String,
920
921    /// Use CDN domain to construct snapshot urls default setting (if snapshot_uploader_type is Gcp)
922    pub snapshot_use_cdn_domain: String,
923
924    /// Signer importer run interval default setting
925    pub signer_importer_run_interval: u64,
926
927    /// If set no error is returned in case of unparsable block and an error log is written instead.
928    ///
929    /// Will be ignored on (pre)production networks.
930    pub allow_unparsable_block: String,
931
932    /// Cardano transactions prover cache pool size
933    pub cardano_transactions_prover_cache_pool_size: u32,
934
935    /// Cardano transactions database connection pool size
936    pub cardano_transactions_database_connection_pool_size: u32,
937
938    /// Cardano transactions signing configuration
939    pub cardano_transactions_signing_config: CardanoTransactionsSigningConfig,
940
941    /// Maximum number of transactions hashes allowed by request to the prover of the Cardano transactions
942    pub cardano_transactions_prover_max_hashes_allowed_by_request: u32,
943
944    /// The maximum number of roll forwards during a poll of the block streamer when importing transactions.
945    pub cardano_transactions_block_streamer_max_roll_forwards_per_poll: u32,
946
947    /// Enable metrics server (Prometheus endpoint on /metrics).
948    pub enable_metrics_server: String,
949
950    /// Metrics HTTP server IP.
951    pub metrics_server_ip: String,
952
953    /// Metrics HTTP server listening port.
954    pub metrics_server_port: u16,
955
956    /// Time interval at which metrics are persisted in event database (in seconds).
957    pub persist_usage_report_interval_in_seconds: u64,
958}
959
960impl Default for DefaultConfiguration {
961    fn default() -> Self {
962        Self {
963            environment: ExecutionEnvironment::Production,
964            server_ip: "0.0.0.0".to_string(),
965            server_port: "8080".to_string(),
966            db_directory: "/db".to_string(),
967            snapshot_directory: ".".to_string(),
968            snapshot_uploader_type: "gcp".to_string(),
969            era_reader_adapter_type: "bootstrap".to_string(),
970            chain_observer_type: "pallas".to_string(),
971            reset_digests_cache: "false".to_string(),
972            disable_digests_cache: "false".to_string(),
973            snapshot_compression_algorithm: "zstandard".to_string(),
974            snapshot_use_cdn_domain: "false".to_string(),
975            signer_importer_run_interval: 720,
976            allow_unparsable_block: "false".to_string(),
977            cardano_transactions_prover_cache_pool_size: 10,
978            cardano_transactions_database_connection_pool_size: 10,
979            cardano_transactions_signing_config: CardanoTransactionsSigningConfig {
980                security_parameter: BlockNumber(3000),
981                step: BlockNumber(120),
982            },
983            cardano_transactions_prover_max_hashes_allowed_by_request: 100,
984            cardano_transactions_block_streamer_max_roll_forwards_per_poll: 10000,
985            enable_metrics_server: "false".to_string(),
986            metrics_server_ip: "0.0.0.0".to_string(),
987            metrics_server_port: 9090,
988            persist_usage_report_interval_in_seconds: 10,
989        }
990    }
991}
992
993impl DefaultConfiguration {
994    fn namespace() -> String {
995        "default configuration".to_string()
996    }
997}
998
999impl From<ExecutionEnvironment> for ValueKind {
1000    fn from(value: ExecutionEnvironment) -> Self {
1001        match value {
1002            ExecutionEnvironment::Production => ValueKind::String("Production".to_string()),
1003            ExecutionEnvironment::Test => ValueKind::String("Test".to_string()),
1004        }
1005    }
1006}
1007
1008impl Source for DefaultConfiguration {
1009    fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
1010        Box::new(self.clone())
1011    }
1012
1013    fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
1014        let mut result = Map::new();
1015
1016        let namespace = DefaultConfiguration::namespace();
1017
1018        let myself = self.clone();
1019        register_config_value!(result, &namespace, myself.environment);
1020        register_config_value!(result, &namespace, myself.server_ip);
1021        register_config_value!(result, &namespace, myself.server_port);
1022        register_config_value!(result, &namespace, myself.db_directory);
1023        register_config_value!(result, &namespace, myself.snapshot_directory);
1024        register_config_value!(result, &namespace, myself.snapshot_uploader_type);
1025        register_config_value!(result, &namespace, myself.era_reader_adapter_type);
1026        register_config_value!(result, &namespace, myself.reset_digests_cache);
1027        register_config_value!(result, &namespace, myself.disable_digests_cache);
1028        register_config_value!(result, &namespace, myself.snapshot_compression_algorithm);
1029        register_config_value!(result, &namespace, myself.snapshot_use_cdn_domain);
1030        register_config_value!(result, &namespace, myself.signer_importer_run_interval);
1031        register_config_value!(result, &namespace, myself.allow_unparsable_block);
1032        register_config_value!(
1033            result,
1034            &namespace,
1035            myself.cardano_transactions_prover_cache_pool_size
1036        );
1037        register_config_value!(
1038            result,
1039            &namespace,
1040            myself.cardano_transactions_database_connection_pool_size
1041        );
1042        register_config_value!(
1043            result,
1044            &namespace,
1045            myself.cardano_transactions_prover_max_hashes_allowed_by_request
1046        );
1047        register_config_value!(
1048            result,
1049            &namespace,
1050            myself.cardano_transactions_block_streamer_max_roll_forwards_per_poll
1051        );
1052        register_config_value!(result, &namespace, myself.enable_metrics_server);
1053        register_config_value!(result, &namespace, myself.metrics_server_ip);
1054        register_config_value!(result, &namespace, myself.metrics_server_port);
1055        register_config_value!(
1056            result,
1057            &namespace,
1058            myself.persist_usage_report_interval_in_seconds
1059        );
1060        register_config_value!(
1061            result,
1062            &namespace,
1063            myself.cardano_transactions_signing_config,
1064            |v: CardanoTransactionsSigningConfig| HashMap::from([
1065                (
1066                    "security_parameter".to_string(),
1067                    ValueKind::from(*v.security_parameter,),
1068                ),
1069                ("step".to_string(), ValueKind::from(*v.step),)
1070            ])
1071        );
1072        Ok(result)
1073    }
1074}
1075
1076#[cfg(test)]
1077mod test {
1078    use mithril_common::temp_dir;
1079
1080    use super::*;
1081
1082    #[test]
1083    fn safe_epoch_retention_limit_wont_change_a_value_higher_than_three() {
1084        for limit in 4..=10u64 {
1085            let configuration = ServeCommandConfiguration {
1086                store_retention_limit: Some(limit as usize),
1087                ..ServeCommandConfiguration::new_sample(temp_dir!())
1088            };
1089            assert_eq!(configuration.safe_epoch_retention_limit(), Some(limit));
1090        }
1091    }
1092
1093    #[test]
1094    fn safe_epoch_retention_limit_wont_change_a_none_value() {
1095        let configuration = ServeCommandConfiguration {
1096            store_retention_limit: None,
1097            ..ServeCommandConfiguration::new_sample(temp_dir!())
1098        };
1099        assert_eq!(configuration.safe_epoch_retention_limit(), None);
1100    }
1101
1102    #[test]
1103    fn safe_epoch_retention_limit_wont_yield_a_value_lower_than_three() {
1104        for limit in 0..=3 {
1105            let configuration = ServeCommandConfiguration {
1106                store_retention_limit: Some(limit),
1107                ..ServeCommandConfiguration::new_sample(temp_dir!())
1108            };
1109            assert_eq!(configuration.safe_epoch_retention_limit(), Some(3));
1110        }
1111    }
1112
1113    #[test]
1114    fn can_build_config_with_ctx_signing_config_from_default_configuration() {
1115        #[derive(Debug, Deserialize)]
1116        struct TargetConfig {
1117            cardano_transactions_signing_config: CardanoTransactionsSigningConfig,
1118        }
1119
1120        let config_builder = config::Config::builder().add_source(DefaultConfiguration::default());
1121        let target: TargetConfig = config_builder.build().unwrap().try_deserialize().unwrap();
1122
1123        assert_eq!(
1124            target.cardano_transactions_signing_config,
1125            DefaultConfiguration::default().cardano_transactions_signing_config
1126        );
1127    }
1128
1129    #[test]
1130    fn compute_allowed_signed_entity_types_discriminants_append_default_discriminants() {
1131        let config = ServeCommandConfiguration {
1132            signed_entity_types: None,
1133            ..ServeCommandConfiguration::new_sample(temp_dir!())
1134        };
1135
1136        assert_eq!(
1137            config.compute_allowed_signed_entity_types_discriminants().unwrap(),
1138            BTreeSet::from(SignedEntityConfig::DEFAULT_ALLOWED_DISCRIMINANTS)
1139        );
1140    }
1141
1142    #[test]
1143    fn allow_http_serve_directory() {
1144        let config = ServeCommandConfiguration {
1145            snapshot_uploader_type: SnapshotUploaderType::Local,
1146            ..ServeCommandConfiguration::new_sample(temp_dir!())
1147        };
1148
1149        assert!(config.allow_http_serve_directory());
1150
1151        let config = ServeCommandConfiguration {
1152            snapshot_uploader_type: SnapshotUploaderType::Gcp,
1153            ..ServeCommandConfiguration::new_sample(temp_dir!())
1154        };
1155
1156        assert!(!config.allow_http_serve_directory());
1157    }
1158
1159    #[test]
1160    fn get_server_url_return_local_url_with_server_base_path_if_public_url_is_not_set() {
1161        let config = ServeCommandConfiguration {
1162            server_ip: "1.2.3.4".to_string(),
1163            server_port: 5678,
1164            public_server_url: None,
1165            ..ServeCommandConfiguration::new_sample(temp_dir!())
1166        };
1167
1168        assert_eq!(
1169            config.get_server_url().unwrap().as_str(),
1170            &format!("http://1.2.3.4:5678/{SERVER_BASE_PATH}/")
1171        );
1172    }
1173
1174    #[test]
1175    fn get_server_url_return_sanitized_public_url_if_it_is_set() {
1176        let config = ServeCommandConfiguration {
1177            server_ip: "1.2.3.4".to_string(),
1178            server_port: 5678,
1179            public_server_url: Some("https://example.com".to_string()),
1180            ..ServeCommandConfiguration::new_sample(temp_dir!())
1181        };
1182
1183        assert_eq!(
1184            config.get_server_url().unwrap().as_str(),
1185            "https://example.com/"
1186        );
1187    }
1188
1189    #[test]
1190    fn joining_to_local_server_url_keep_base_path() {
1191        let config = ServeCommandConfiguration {
1192            server_ip: "1.2.3.4".to_string(),
1193            server_port: 6789,
1194            public_server_url: None,
1195            ..ServeCommandConfiguration::new_sample(temp_dir!())
1196        };
1197
1198        let joined_url = config.get_local_server_url().unwrap().join("some/path").unwrap();
1199        assert!(
1200            joined_url.as_str().contains(SERVER_BASE_PATH),
1201            "Joined URL `{joined_url}`, does not contain base path `{SERVER_BASE_PATH}`"
1202        );
1203    }
1204
1205    #[test]
1206    fn joining_to_public_server_url_without_trailing_slash() {
1207        let subpath_without_trailing_slash = "subpath_without_trailing_slash";
1208        let config = ServeCommandConfiguration {
1209            public_server_url: Some(format!(
1210                "https://example.com/{subpath_without_trailing_slash}"
1211            )),
1212            ..ServeCommandConfiguration::new_sample(temp_dir!())
1213        };
1214
1215        let joined_url = config.get_server_url().unwrap().join("some/path").unwrap();
1216        assert!(
1217            joined_url.as_str().contains(subpath_without_trailing_slash),
1218            "Joined URL `{joined_url}`, does not contain subpath `{subpath_without_trailing_slash}`"
1219        );
1220    }
1221
1222    #[test]
1223    fn is_follower_aggregator_returns_true_when_in_follower_mode() {
1224        let config = ServeCommandConfiguration {
1225            leader_aggregator_endpoint: Some("some_endpoint".to_string()),
1226            ..ServeCommandConfiguration::new_sample(temp_dir!())
1227        };
1228
1229        assert!(config.is_follower_aggregator());
1230    }
1231
1232    #[test]
1233    fn is_follower_aggregator_returns_false_when_in_leader_mode() {
1234        let config = ServeCommandConfiguration {
1235            leader_aggregator_endpoint: None,
1236            ..ServeCommandConfiguration::new_sample(temp_dir!())
1237        };
1238
1239        assert!(!config.is_follower_aggregator());
1240    }
1241
1242    #[test]
1243    fn serialized_ancillary_files_signer_config_use_snake_case_for_keys_and_kebab_case_for_type_value()
1244     {
1245        let serialized_json = r#"{
1246            "type": "secret-key",
1247            "secret_key": "whatever"
1248        }"#;
1249
1250        let deserialized: AncillaryFilesSignerConfig =
1251            serde_json::from_str(serialized_json).unwrap();
1252        assert_eq!(
1253            deserialized,
1254            AncillaryFilesSignerConfig::SecretKey {
1255                secret_key: "whatever".to_string()
1256            }
1257        );
1258    }
1259
1260    #[test]
1261    fn deserializing_ancillary_signing_gcp_kms_configuration() {
1262        let serialized_json = r#"{
1263            "type": "gcp-kms",
1264            "resource_name": "projects/123456789/locations/global/keyRings/my-keyring/cryptoKeys/my-key/cryptoKeyVersions/1",
1265            "credentials_json_env_var": "CUSTOM_ENV_VAR"
1266        }"#;
1267
1268        let deserialized: AncillaryFilesSignerConfig =
1269            serde_json::from_str(serialized_json).unwrap();
1270        assert_eq!(
1271            deserialized,
1272            AncillaryFilesSignerConfig::GcpKms {
1273                resource_name: GcpCryptoKeyVersionResourceName {
1274                    project: "123456789".to_string(),
1275                    location: "global".to_string(),
1276                    key_ring: "my-keyring".to_string(),
1277                    key_name: "my-key".to_string(),
1278                    version: "1".to_string(),
1279                },
1280                credentials_json_env_var: "CUSTOM_ENV_VAR".to_string()
1281            }
1282        );
1283    }
1284
1285    #[test]
1286    fn deserializing_ancillary_signing_gcp_kms_configuration_without_credentials_json_env_var_fallback_to_default()
1287     {
1288        let serialized_json = r#"{
1289            "type": "gcp-kms",
1290            "resource_name": "projects/123456789/locations/global/keyRings/my-keyring/cryptoKeys/my-key/cryptoKeyVersions/1"
1291        }"#;
1292
1293        let deserialized: AncillaryFilesSignerConfig =
1294            serde_json::from_str(serialized_json).unwrap();
1295        if let AncillaryFilesSignerConfig::GcpKms {
1296            credentials_json_env_var,
1297            ..
1298        } = deserialized
1299        {
1300            assert_eq!(
1301                credentials_json_env_var,
1302                DEFAULT_GCP_CREDENTIALS_JSON_ENV_VAR
1303            );
1304        } else {
1305            panic!("Expected GcpKms variant but got {deserialized:?}");
1306        }
1307    }
1308
1309    mod origin_tag {
1310        use super::*;
1311
1312        #[test]
1313        fn default_origin_tag_white_list_is_not_empty() {
1314            let config = ServeCommandConfiguration {
1315                custom_origin_tag_white_list: None,
1316                ..ServeCommandConfiguration::new_sample(temp_dir!())
1317            };
1318            assert_ne!(config.compute_origin_tag_white_list().len(), 0,);
1319        }
1320
1321        #[test]
1322        fn custom_origin_tag_are_added_to_default_white_list() {
1323            let config = ServeCommandConfiguration {
1324                custom_origin_tag_white_list: Some("TAG_A,TAG_B , TAG_C".to_string()),
1325                ..ServeCommandConfiguration::new_sample(temp_dir!())
1326            };
1327
1328            let default_white_list = ServeCommandConfiguration {
1329                custom_origin_tag_white_list: None,
1330                ..ServeCommandConfiguration::new_sample(temp_dir!())
1331            }
1332            .compute_origin_tag_white_list();
1333
1334            let mut expected_white_list = default_white_list.clone();
1335            assert!(expected_white_list.insert("TAG_A".to_string()));
1336            assert!(expected_white_list.insert("TAG_B".to_string()));
1337            assert!(expected_white_list.insert("TAG_C".to_string()));
1338
1339            assert_eq!(expected_white_list, config.compute_origin_tag_white_list());
1340        }
1341    }
1342}