clementine_core/config/
mod.rs

1//! # Configuration Options
2//!
3//! This module defines configuration options.
4//!
5//! This module is base for `cli` module and not dependent on it. Therefore,
6//! this module can be used independently.
7//!
8//! ## Configuration File
9//!
10//! Configuration options can be read from a TOML file. File contents are
11//! described in `BridgeConfig` struct.
12
13use crate::cli;
14use crate::config::env::{read_string_from_env, read_string_from_env_then_parse};
15use crate::config::protocol::BLOCKS_PER_HOUR;
16use crate::deposit::SecurityCouncil;
17use crate::extended_bitcoin_rpc::ExtendedBitcoinRpc;
18use crate::header_chain_prover::HeaderChainProver;
19use bitcoin::address::NetworkUnchecked;
20use bitcoin::secp256k1::SecretKey;
21use bitcoin::{Address, Amount, Network, OutPoint, XOnlyPublicKey};
22use bridge_circuit_host::utils::is_dev_mode;
23use circuits_lib::bridge_circuit::constants::is_test_vk;
24use clementine_errors::BridgeError;
25use eyre::Context;
26use protocol::ProtocolParamset;
27use secrecy::SecretString;
28use serde::Deserialize;
29use std::str::FromStr;
30use std::time::Duration;
31use std::{fs::File, io::Read, path::PathBuf};
32
33pub mod env;
34pub mod protocol;
35
36#[cfg(test)]
37mod test;
38
39#[cfg(test)]
40pub use test::*;
41
42/// Configuration options for any Clementine target (tests, binaries etc.).
43#[derive(Debug, Clone, Deserialize)]
44pub struct BridgeConfig {
45    /// Protocol paramset
46    ///
47    /// Sourced from either a file or the environment, is set to REGTEST_PARAMSET in tests
48    ///
49    /// Skipped in deserialization and replaced by either file/environment source. See [`crate::cli::get_cli_config`]
50    #[serde(skip)]
51    pub protocol_paramset: &'static ProtocolParamset,
52    /// Host of the operator or the verifier
53    pub host: String,
54    /// Port of the operator or the verifier
55    pub port: u16,
56    /// Secret key for the operator or the verifier.
57    pub secret_key: SecretKey,
58    /// Operator's fee for withdrawal, in satoshis.
59    pub operator_withdrawal_fee_sats: Option<Amount>,
60    /// Bitcoin remote procedure call URL.
61    pub bitcoin_rpc_url: String,
62    /// Bitcoin RPC user.
63    pub bitcoin_rpc_user: SecretString,
64    /// Bitcoin RPC user password.
65    pub bitcoin_rpc_password: SecretString,
66    /// mempool.space API host for retrieving the fee rate. If None, Bitcoin Core RPC will be used.
67    pub mempool_api_host: Option<String>,
68    /// mempool.space API endpoint for retrieving the fee rate. If None, Bitcoin Core RPC will be used.
69    pub mempool_api_endpoint: Option<String>,
70
71    /// PostgreSQL database host address.
72    pub db_host: String,
73    /// PostgreSQL database port.
74    pub db_port: u16,
75    /// PostgreSQL database user name.
76    pub db_user: SecretString,
77    /// PostgreSQL database user password.
78    pub db_password: SecretString,
79    /// PostgreSQL database name.
80    pub db_name: String,
81    /// Citrea RPC URL.
82    pub citrea_rpc_url: String,
83    /// Citrea light client prover RPC URL.
84    pub citrea_light_client_prover_url: String,
85    /// Citrea's EVM Chain ID.
86    pub citrea_chain_id: u32,
87    /// Timeout in seconds for Citrea RPC calls.
88    pub citrea_request_timeout: Option<Duration>,
89    /// Bridge contract address.
90    pub bridge_contract_address: String,
91    // Initial header chain proof receipt's file path.
92    pub header_chain_proof_path: Option<PathBuf>,
93    /// Batch size of the header chain proofs
94    pub header_chain_proof_batch_size: u32,
95
96    /// Security council.
97    pub security_council: SecurityCouncil,
98
99    /// Verifier endpoints. For the aggregator only
100    pub verifier_endpoints: Option<Vec<String>>,
101    /// Operator endpoint. For the aggregator only
102    pub operator_endpoints: Option<Vec<String>>,
103
104    /// Own operator's reimbursement address.
105    pub operator_reimbursement_address: Option<Address<NetworkUnchecked>>,
106
107    /// Own operator's collateral funding outpoint.
108    pub operator_collateral_funding_outpoint: Option<OutPoint>,
109
110    // TLS certificates
111    /// Path to the server certificate file.
112    ///
113    /// Required for all entities.
114    pub server_cert_path: PathBuf,
115    /// Path to the server key file.
116    pub server_key_path: PathBuf,
117
118    /// Path to the client certificate file. (used to communicate with other gRPC services)
119    ///
120    /// Required for all entities. This is used to authenticate requests.
121    /// Aggregator's client certificate should match the expected aggregator
122    /// certificate in other entities.
123    ///
124    /// Aggregator needs this to call other entities, other entities need this
125    /// to call their own internal endpoints.
126    pub client_cert_path: PathBuf,
127    /// Path to the client key file.
128    pub client_key_path: PathBuf,
129
130    /// Path to the CA certificate file which is used to verify client
131    /// certificates.
132    pub ca_cert_path: PathBuf,
133
134    /// Whether client certificates should be restricted to Aggregator and Self certificates.
135    ///
136    /// Client certificates are always validated against the CA certificate
137    /// according to mTLS regardless of this setting.
138    pub client_verification: bool,
139
140    /// Path to the aggregator certificate file. (used to authenticate requests from aggregator)
141    ///
142    /// Aggregator's client cert should be equal to the this certificate.
143    pub aggregator_cert_path: PathBuf,
144
145    /// Telemetry configuration
146    pub telemetry: Option<TelemetryConfig>,
147
148    /// The ECDSA address of the citrea/aggregator that will sign the withdrawal params
149    /// after manual verification of the optimistic payout and operator's withdrawal.
150    /// Used for both an extra verification of aggregator's identity and to force citrea
151    /// to check withdrawal params manually during some time after launch.
152    pub aggregator_verification_address: Option<alloy::primitives::Address>,
153
154    /// The X25519 public key that will be used to encrypt the emergency stop message.
155    pub emergency_stop_encryption_public_key: Option<[u8; 32]>,
156
157    /// Time to wait after a kickoff to send a watchtower challenge
158    pub time_to_send_watchtower_challenge: u16,
159
160    #[cfg(test)]
161    #[serde(skip)]
162    pub test_params: test::TestParams,
163
164    /// gRPC client/server limits
165    #[serde(default = "default_grpc_limits")]
166    pub grpc: GrpcLimits,
167
168    /// Hard cap on tx sender fee rate (sat/vB).
169    #[serde(default = "default_tx_sender_limits")]
170    pub tx_sender_limits: TxSenderLimits,
171}
172
173// Re-export types from clementine-config
174pub use clementine_config::{GrpcLimits, TxSenderLimits};
175
176fn default_grpc_limits() -> GrpcLimits {
177    GrpcLimits::default()
178}
179
180fn default_tx_sender_limits() -> TxSenderLimits {
181    TxSenderLimits::default()
182}
183
184/// Extension trait for GrpcLimits to add from_env method
185pub trait GrpcLimitsExt {
186    /// Create GrpcLimits from environment variables.
187    fn from_env() -> Result<GrpcLimits, BridgeError>;
188}
189
190/// Extension trait for TxSenderLimits to add from_env method
191pub trait TxSenderLimitsExt {
192    /// Create TxSenderLimits from environment variables.
193    fn from_env() -> Result<TxSenderLimits, BridgeError>;
194}
195
196impl BridgeConfig {
197    /// Create a new `BridgeConfig` with default values.
198    pub fn new() -> Self {
199        BridgeConfig {
200            ..Default::default()
201        }
202    }
203
204    /// Get the protocol paramset defined by the paramset name.
205    pub fn protocol_paramset(&self) -> &'static ProtocolParamset {
206        self.protocol_paramset
207    }
208
209    /// Read contents of a TOML file and generate a `BridgeConfig`.
210    pub fn try_parse_file(path: PathBuf) -> Result<Self, BridgeError> {
211        let mut contents = String::new();
212
213        let mut file = match File::open(path.clone()) {
214            Ok(f) => f,
215            Err(e) => return Err(BridgeError::ConfigError(e.to_string())),
216        };
217
218        if let Err(e) = file.read_to_string(&mut contents) {
219            return Err(BridgeError::ConfigError(e.to_string()));
220        }
221
222        tracing::trace!("Using configuration file: {:?}", path);
223
224        BridgeConfig::try_parse_from(contents)
225    }
226
227    /// Try to parse a `BridgeConfig` from given TOML formatted string and
228    /// generate a `BridgeConfig`.
229    pub fn try_parse_from(input: String) -> Result<Self, BridgeError> {
230        match toml::from_str::<BridgeConfig>(&input) {
231            Ok(c) => Ok(c),
232            Err(e) => Err(BridgeError::ConfigError(e.to_string())),
233        }
234    }
235
236    /// Check general requirements for the configuration irrespective of the network.
237    pub async fn check_general_requirements(&self) -> Result<(), BridgeError> {
238        // check genesis state hash
239        let rpc = ExtendedBitcoinRpc::connect(
240            self.bitcoin_rpc_url.clone(),
241            self.bitcoin_rpc_user.clone(),
242            self.bitcoin_rpc_password.clone(),
243            None,
244        )
245        .await
246        .wrap_err("Failed to connect to Bitcoin RPC while checking general requirements")?;
247
248        let genesis_chain_state = HeaderChainProver::get_chain_state_from_height(
249            &rpc,
250            self.protocol_paramset().genesis_height.into(),
251            self.protocol_paramset().network,
252        )
253        .await
254        .wrap_err("Failed to get genesis chain state while checking general requirements")?;
255
256        let mut reasons = Vec::new();
257
258        if genesis_chain_state.to_hash() != self.protocol_paramset().genesis_chain_state_hash {
259            reasons.push(format!(
260                "Genesis chain state hash mismatch, state hash generated from Bitcoin RPC ({}) does not match value in config ({})",
261                hex::encode(genesis_chain_state.to_hash()),
262                hex::encode(self.protocol_paramset().genesis_chain_state_hash)
263            ));
264        }
265
266        if self.protocol_paramset().start_height < self.protocol_paramset().genesis_height {
267            reasons.push(format!(
268                "Start height is less than genesis height: {} < {}",
269                self.protocol_paramset().start_height,
270                self.protocol_paramset().genesis_height
271            ));
272        }
273
274        if self.protocol_paramset().finality_depth < 1 {
275            reasons.push(format!(
276                "Finality depth ({}) cannot be less than 1",
277                self.protocol_paramset().finality_depth
278            ));
279        }
280
281        if !reasons.is_empty() {
282            return Err(BridgeError::ConfigError(format!(
283                "Invalid configuration due to: {}",
284                reasons.join(" - ")
285            )));
286        }
287
288        Ok(())
289    }
290
291    /// Checks various variables if they are correct for mainnet deployment.
292    pub fn check_mainnet_requirements(&self, actor_type: cli::Actor) -> Result<(), BridgeError> {
293        if self.protocol_paramset().network != Network::Bitcoin {
294            return Ok(());
295        }
296
297        let mut misconfigs = Vec::new();
298
299        if matches!(actor_type, cli::Actor::Verifier | cli::Actor::Operator)
300            && !self.client_verification
301        {
302            misconfigs.push("CLIENT_VERIFICATION=false".to_string());
303        }
304
305        /// Checks if an env var is set to a non 0 value.
306        fn check_env_var(env_var: &str, misconfigs: &mut Vec<String>) {
307            if let Ok(var) = std::env::var(env_var) {
308                if var == "0" || var.eq_ignore_ascii_case("false") {
309                    return;
310                }
311
312                misconfigs.push(format!("{env_var}={var}"));
313            }
314        }
315
316        check_env_var("DISABLE_NOFN_CHECK", &mut misconfigs);
317
318        if is_dev_mode() {
319            misconfigs.push("Risc0 dev mode is enabled (RISC0_DEV_MODE=1)".to_string());
320        }
321
322        if is_test_vk() {
323            misconfigs.push("use-test-vk feature is enabled".to_string());
324        }
325
326        if !misconfigs.is_empty() {
327            return Err(BridgeError::ConfigError(format!(
328                "Following configs can't be used on Mainnet: {misconfigs:?}",
329            )));
330        }
331
332        Ok(())
333    }
334
335    #[cfg(feature = "automation")]
336    pub fn mempool_config(&self) -> clementine_tx_sender::MempoolConfig {
337        clementine_tx_sender::MempoolConfig {
338            host: self.mempool_api_host.clone(),
339            endpoint: self.mempool_api_endpoint.clone(),
340        }
341    }
342
343    /// Build a tx-sender standalone config from this bridge config.
344    ///
345    /// This keeps tx-sender wiring centralized in the config module, so core can
346    /// run tx-sender using a single derived config object.
347    #[cfg(feature = "automation")]
348    pub fn tx_sender_config(&self) -> clementine_tx_sender::config::TxSenderConfig {
349        use clementine_tx_sender::config::{
350            TxSenderBitcoinRpcConfig, TxSenderConfig, TxSenderPostgresConfig,
351        };
352
353        TxSenderConfig {
354            network: self.protocol_paramset.network,
355            secret_key: self.secret_key,
356            private_da_key: None,
357            postgres: TxSenderPostgresConfig {
358                host: self.db_host.clone(),
359                port: self.db_port,
360                user: self.db_user.clone(),
361                password: self.db_password.clone(),
362                dbname: self.db_name.clone(),
363            },
364            bitcoin_rpc: TxSenderBitcoinRpcConfig {
365                url: self.bitcoin_rpc_url.clone(),
366                user: self.bitcoin_rpc_user.clone(),
367                password: self.bitcoin_rpc_password.clone(),
368            },
369            mempool: self.mempool_config(),
370            limits: self.tx_sender_limits.clone(),
371            finality_depth: self.protocol_paramset.finality_depth,
372            // poll_delay_ms not used in clementine, poll delay for txsender is defined in core/src/task/tx_sender.rs
373            poll_delay_ms: 60_000,
374            include_unsafe: false,
375            jsonrpc: None,
376        }
377    }
378}
379
380// only needed for one test
381#[cfg(test)]
382impl PartialEq for BridgeConfig {
383    fn eq(&self, other: &Self) -> bool {
384        use secrecy::ExposeSecret;
385
386        let all_eq = self.protocol_paramset == other.protocol_paramset
387            && self.host == other.host
388            && self.port == other.port
389            && self.secret_key == other.secret_key
390            && self.operator_withdrawal_fee_sats == other.operator_withdrawal_fee_sats
391            && self.bitcoin_rpc_url == other.bitcoin_rpc_url
392            && self.bitcoin_rpc_user.expose_secret() == other.bitcoin_rpc_user.expose_secret()
393            && self.bitcoin_rpc_password.expose_secret()
394                == other.bitcoin_rpc_password.expose_secret()
395            && self.db_host == other.db_host
396            && self.db_port == other.db_port
397            && self.db_user.expose_secret() == other.db_user.expose_secret()
398            && self.db_password.expose_secret() == other.db_password.expose_secret()
399            && self.db_name == other.db_name
400            && self.citrea_rpc_url == other.citrea_rpc_url
401            && self.citrea_light_client_prover_url == other.citrea_light_client_prover_url
402            && self.citrea_chain_id == other.citrea_chain_id
403            && self.bridge_contract_address == other.bridge_contract_address
404            && self.header_chain_proof_path == other.header_chain_proof_path
405            && self.security_council == other.security_council
406            && self.verifier_endpoints == other.verifier_endpoints
407            && self.operator_endpoints == other.operator_endpoints
408            && self.operator_reimbursement_address == other.operator_reimbursement_address
409            && self.operator_collateral_funding_outpoint
410                == other.operator_collateral_funding_outpoint
411            && self.server_cert_path == other.server_cert_path
412            && self.server_key_path == other.server_key_path
413            && self.client_cert_path == other.client_cert_path
414            && self.client_key_path == other.client_key_path
415            && self.ca_cert_path == other.ca_cert_path
416            && self.client_verification == other.client_verification
417            && self.aggregator_cert_path == other.aggregator_cert_path
418            && self.test_params == other.test_params
419            && self.grpc == other.grpc;
420
421        all_eq
422    }
423}
424
425impl Default for BridgeConfig {
426    fn default() -> Self {
427        Self {
428            protocol_paramset: Default::default(),
429            host: "127.0.0.1".to_string(),
430            port: 17000,
431
432            secret_key: SecretKey::from_str(
433                "1111111111111111111111111111111111111111111111111111111111111111",
434            )
435            .expect("known valid input"),
436
437            operator_withdrawal_fee_sats: Some(Amount::from_sat(100000)),
438
439            bitcoin_rpc_url: "http://127.0.0.1:18443/wallet/admin".to_string(),
440            bitcoin_rpc_user: "admin".to_string().into(),
441            bitcoin_rpc_password: "admin".to_string().into(),
442            mempool_api_host: None,
443            mempool_api_endpoint: None,
444
445            db_host: "127.0.0.1".to_string(),
446            db_port: 5432,
447            db_user: "clementine".to_string().into(),
448            db_password: "clementine".to_string().into(),
449            db_name: "clementine".to_string(),
450
451            citrea_rpc_url: "".to_string(),
452            citrea_light_client_prover_url: "".to_string(),
453            citrea_chain_id: 5655,
454            bridge_contract_address: "3100000000000000000000000000000000000002".to_string(),
455            citrea_request_timeout: None,
456
457            header_chain_proof_path: None,
458            header_chain_proof_batch_size: 100,
459
460            operator_reimbursement_address: None,
461            operator_collateral_funding_outpoint: None,
462
463            security_council: SecurityCouncil {
464                pks: vec![
465                    XOnlyPublicKey::from_str(
466                        "9ac20335eb38768d2052be1dbbc3c8f6178407458e51e6b4ad22f1d91758895b",
467                    )
468                    .expect("valid xonly"),
469                    XOnlyPublicKey::from_str(
470                        "5ab4689e400a4a160cf01cd44730845a54768df8547dcdf073d964f109f18c30",
471                    )
472                    .expect("valid xonly"),
473                ],
474                threshold: 1,
475            },
476
477            verifier_endpoints: None,
478            operator_endpoints: None,
479
480            server_cert_path: PathBuf::from("certs/server/server.pem"),
481            server_key_path: PathBuf::from("certs/server/server.key"),
482            client_cert_path: PathBuf::from("certs/client/client.pem"),
483            client_key_path: PathBuf::from("certs/client/client.key"),
484            ca_cert_path: PathBuf::from("certs/ca/ca.pem"),
485            aggregator_cert_path: PathBuf::from("certs/aggregator/aggregator.pem"),
486            client_verification: true,
487            aggregator_verification_address: Some(
488                alloy::primitives::Address::from_str("0x242fbec93465ce42b3d7c0e1901824a2697193fd")
489                    .expect("valid address"),
490            ),
491            emergency_stop_encryption_public_key: Some(
492                hex::decode("025d32d10ec7b899df4eeb4d80918b7f0a1f2a28f6af24f71aa2a59c69c0d531")
493                    .expect("valid hex")
494                    .try_into()
495                    .expect("valid key"),
496            ),
497
498            telemetry: Some(TelemetryConfig::default()),
499
500            time_to_send_watchtower_challenge: 4 * BLOCKS_PER_HOUR * 3 / 2,
501
502            #[cfg(test)]
503            test_params: test::TestParams::default(),
504
505            // New hardening parameters, optional so they don't break existing configs.
506            grpc: default_grpc_limits(),
507            tx_sender_limits: default_tx_sender_limits(),
508        }
509    }
510}
511
512// Re-export TelemetryConfig from clementine-config
513pub use clementine_config::TelemetryConfig;
514
515/// Extension trait for TelemetryConfig to add from_env method
516pub trait TelemetryConfigExt {
517    /// Create a TelemetryConfig from environment variables.
518    fn from_env() -> Result<TelemetryConfig, BridgeError>;
519}
520
521impl TelemetryConfigExt for TelemetryConfig {
522    fn from_env() -> Result<TelemetryConfig, BridgeError> {
523        let host = read_string_from_env("TELEMETRY_HOST")?;
524        let port = read_string_from_env_then_parse::<u16>("TELEMETRY_PORT")?;
525        Ok(TelemetryConfig { host, port })
526    }
527}
528
529#[cfg(test)]
530mod tests {
531    use super::BridgeConfig;
532    use crate::{cli, config::protocol::REGTEST_PARAMSET};
533    use bitcoin::{hashes::Hash, Network, OutPoint, Txid};
534    use std::{
535        fs::{self, File},
536        io::Write,
537    };
538
539    #[test]
540    fn parse_from_string() {
541        // In case of a incorrect file content, we should receive an error.
542        let content = "brokenfilecontent";
543        assert!(BridgeConfig::try_parse_from(content.to_string()).is_err());
544    }
545
546    #[test]
547    fn parse_from_file() {
548        let file_name = "parse_from_file";
549        let content = "invalid file content";
550        let mut file = File::create(file_name).unwrap();
551        file.write_all(content.as_bytes()).unwrap();
552
553        assert!(BridgeConfig::try_parse_file(file_name.into()).is_err());
554
555        // Read first example test file use for this test.
556        let base_path = env!("CARGO_MANIFEST_DIR");
557        let config_path = format!("{base_path}/src/test/data/bridge_config.toml");
558        let content = fs::read_to_string(config_path).unwrap();
559        let mut file = File::create(file_name).unwrap();
560        file.write_all(content.as_bytes()).unwrap();
561
562        BridgeConfig::try_parse_file(file_name.into()).unwrap();
563
564        fs::remove_file(file_name).unwrap();
565    }
566
567    #[test]
568    fn parse_from_file_with_invalid_headers() {
569        let file_name = "parse_from_file_with_invalid_headers";
570        let content = "[header1]
571        num_verifiers = 4
572
573        [header2]
574        confirmation_threshold = 1
575        network = \"regtest\"
576        bitcoin_rpc_url = \"http://localhost:18443\"
577        bitcoin_rpc_user = \"admin\"
578        bitcoin_rpc_password = \"admin\"\n";
579        let mut file = File::create(file_name).unwrap();
580        file.write_all(content.as_bytes()).unwrap();
581
582        assert!(BridgeConfig::try_parse_file(file_name.into()).is_err());
583
584        fs::remove_file(file_name).unwrap();
585    }
586
587    #[test]
588    fn test_test_config_parseable() {
589        let content = include_str!("../test/data/bridge_config.toml");
590        BridgeConfig::try_parse_from(content.to_string()).unwrap();
591    }
592
593    pub const INVALID_PARAMSET: crate::config::ProtocolParamset = crate::config::ProtocolParamset {
594        network: Network::Bitcoin,
595        ..REGTEST_PARAMSET
596    };
597    #[ignore = "Fails if bridge-circuit-host has use-test-vk feature! Which it will, if --all-features is specified at Cargo invocation."]
598    #[serial_test::serial]
599    #[test]
600    fn check_mainnet_reqs() {
601        let env_vars = vec!["DISABLE_NOFN_CHECK", "RISC0_DEV_MODE"];
602
603        // Nothing illegal is set.
604        for var in env_vars.clone() {
605            std::env::remove_var(var);
606        }
607        let mainnet_config = BridgeConfig {
608            protocol_paramset: &INVALID_PARAMSET,
609            client_verification: true,
610            operator_collateral_funding_outpoint: Some(OutPoint {
611                txid: Txid::all_zeros(),
612                vout: 0,
613            }),
614            ..Default::default()
615        };
616        let checks = mainnet_config.check_mainnet_requirements(cli::Actor::Operator);
617        println!("checks: {checks:?}");
618        assert!(checks.is_ok());
619
620        // Nothing illegal is set while illegal env vars set to 0 specifically.
621        for var in env_vars.clone() {
622            std::env::set_var(var, "0");
623        }
624        let checks = mainnet_config.check_mainnet_requirements(cli::Actor::Operator);
625        println!("checks: {checks:?}");
626        assert!(checks.is_ok());
627
628        // Illegal configs, no illegal env vars.
629        let incorrect_mainnet_config = BridgeConfig {
630            client_verification: false,
631            operator_collateral_funding_outpoint: None,
632            ..mainnet_config.clone()
633        };
634        let checks = incorrect_mainnet_config.check_mainnet_requirements(cli::Actor::Operator);
635        println!("checks: {checks:?}");
636        assert!(checks.is_err());
637
638        // No illegal configs, illegal env vars.
639        for var in env_vars.clone() {
640            std::env::set_var(var, "1");
641        }
642        let checks = mainnet_config.check_mainnet_requirements(cli::Actor::Operator);
643        println!("checks: {checks:?}");
644        assert!(checks.is_err());
645
646        // Illegal everything.
647        for var in env_vars.clone() {
648            std::env::set_var(var, "1");
649        }
650        let checks = incorrect_mainnet_config.check_mainnet_requirements(cli::Actor::Operator);
651        println!("checks: {checks:?}");
652        assert!(checks.is_err());
653    }
654
655    #[tokio::test]
656    async fn test_check_general_requirements_ok() {
657        use crate::test::common::{create_regtest_rpc, create_test_config_with_thread_name};
658
659        let mut config = create_test_config_with_thread_name().await;
660        // Point config to an isolated regtest bitcoind
661        let _regtest = create_regtest_rpc(&mut config).await;
662
663        // Should pass with proper paramset and node
664        config
665            .check_general_requirements()
666            .await
667            .expect("general requirements should pass on regtest");
668    }
669
670    #[tokio::test]
671    async fn test_check_general_requirements_multiple_errors() {
672        use crate::test::common::{create_regtest_rpc, create_test_config_with_thread_name};
673
674        // Paramset that intentionally violates multiple constraints to aggregate errors
675        pub const BAD_PARAMSET: crate::config::ProtocolParamset = crate::config::ProtocolParamset {
676            // Force hash mismatch
677            genesis_chain_state_hash: [1u8; 32],
678            // Make start height lower than genesis height to trigger that error
679            start_height: 0,
680            genesis_height: 1,
681            // Make finality depth invalid
682            finality_depth: 0,
683            ..REGTEST_PARAMSET
684        };
685
686        let mut config = create_test_config_with_thread_name().await;
687        // Use regtest node but set wrong/invalid paramset values to trigger multiple errors
688        let _regtest = create_regtest_rpc(&mut config).await;
689        config.protocol_paramset = &BAD_PARAMSET;
690
691        let res = config.check_general_requirements().await;
692        assert!(res.is_err());
693        let err = format!("{}", res.unwrap_err());
694        assert!(
695            err.contains("Genesis chain state hash"),
696            "unexpected error: {err}"
697        );
698        assert!(err.contains("Start height"), "unexpected error: {err}");
699        assert!(err.contains("Finality depth"), "unexpected error: {err}");
700    }
701}