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