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::errors::BridgeError;
18use crate::extended_bitcoin_rpc::ExtendedBitcoinRpc;
19use crate::header_chain_prover::HeaderChainProver;
20use bitcoin::address::NetworkUnchecked;
21use bitcoin::secp256k1::SecretKey;
22use bitcoin::{Address, Amount, Network, OutPoint, XOnlyPublicKey};
23use bridge_circuit_host::utils::is_dev_mode;
24use circuits_lib::bridge_circuit::constants::is_test_vk;
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: usize,
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#[derive(Debug, Clone, Deserialize, PartialEq)]
174pub struct GrpcLimits {
175    pub max_message_size: usize,
176    pub timeout_secs: u64,
177    pub tcp_keepalive_secs: u64,
178    pub req_concurrency_limit: usize,
179    pub ratelimit_req_count: usize,
180    pub ratelimit_req_interval_secs: u64,
181}
182
183fn default_grpc_limits() -> GrpcLimits {
184    GrpcLimits {
185        max_message_size: 4 * 1024 * 1024,
186        timeout_secs: 12 * 60 * 60, // 12 hours
187        tcp_keepalive_secs: 60,
188        req_concurrency_limit: 300, // 100 deposits at the same time
189        ratelimit_req_count: 1000,
190        ratelimit_req_interval_secs: 60,
191    }
192}
193
194#[derive(Debug, Clone, Deserialize, PartialEq)]
195pub struct TxSenderLimits {
196    pub fee_rate_hard_cap: u64,
197    pub mempool_fee_rate_multiplier: u64,
198    pub mempool_fee_rate_offset_sat_kvb: u64,
199    /// The time to wait before bumping the fee of a fee payer UTXO
200    /// We wait a bit because after bumping the fee, the unconfirmed change utxo that is in the bumped tx will not be able to be spent (so won't be used to create new fee payer utxos) until that fee payer tx confirms
201    pub cpfp_fee_payer_bump_wait_time_seconds: u64,
202}
203
204fn default_tx_sender_limits() -> TxSenderLimits {
205    TxSenderLimits {
206        fee_rate_hard_cap: 100,
207        mempool_fee_rate_multiplier: 1,
208        mempool_fee_rate_offset_sat_kvb: 0,
209        cpfp_fee_payer_bump_wait_time_seconds: 60 * 60, // 1 hour in seconds
210    }
211}
212
213impl BridgeConfig {
214    /// Create a new `BridgeConfig` with default values.
215    pub fn new() -> Self {
216        BridgeConfig {
217            ..Default::default()
218        }
219    }
220
221    /// Get the protocol paramset defined by the paramset name.
222    pub fn protocol_paramset(&self) -> &'static ProtocolParamset {
223        self.protocol_paramset
224    }
225
226    /// Read contents of a TOML file and generate a `BridgeConfig`.
227    pub fn try_parse_file(path: PathBuf) -> Result<Self, BridgeError> {
228        let mut contents = String::new();
229
230        let mut file = match File::open(path.clone()) {
231            Ok(f) => f,
232            Err(e) => return Err(BridgeError::ConfigError(e.to_string())),
233        };
234
235        if let Err(e) = file.read_to_string(&mut contents) {
236            return Err(BridgeError::ConfigError(e.to_string()));
237        }
238
239        tracing::trace!("Using configuration file: {:?}", path);
240
241        BridgeConfig::try_parse_from(contents)
242    }
243
244    /// Try to parse a `BridgeConfig` from given TOML formatted string and
245    /// generate a `BridgeConfig`.
246    pub fn try_parse_from(input: String) -> Result<Self, BridgeError> {
247        match toml::from_str::<BridgeConfig>(&input) {
248            Ok(c) => Ok(c),
249            Err(e) => Err(BridgeError::ConfigError(e.to_string())),
250        }
251    }
252
253    /// Check general requirements for the configuration irrespective of the network.
254    pub async fn check_general_requirements(&self) -> Result<(), BridgeError> {
255        // check genesis state hash
256        let rpc = ExtendedBitcoinRpc::connect(
257            self.bitcoin_rpc_url.clone(),
258            self.bitcoin_rpc_user.clone(),
259            self.bitcoin_rpc_password.clone(),
260            None,
261        )
262        .await
263        .wrap_err("Failed to connect to Bitcoin RPC while checking general requirements")?;
264
265        let genesis_chain_state = HeaderChainProver::get_chain_state_from_height(
266            &rpc,
267            self.protocol_paramset().genesis_height.into(),
268            self.protocol_paramset().network,
269        )
270        .await
271        .wrap_err("Failed to get genesis chain state while checking general requirements")?;
272
273        let mut reasons = Vec::new();
274
275        if genesis_chain_state.to_hash() != self.protocol_paramset().genesis_chain_state_hash {
276            reasons.push(format!(
277                "Genesis chain state hash mismatch, state hash generated from Bitcoin RPC ({}) does not match value in config ({})",
278                hex::encode(genesis_chain_state.to_hash()),
279                hex::encode(self.protocol_paramset().genesis_chain_state_hash)
280            ));
281        }
282
283        if self.protocol_paramset().start_height < self.protocol_paramset().genesis_height {
284            reasons.push(format!(
285                "Start height is less than genesis height: {} < {}",
286                self.protocol_paramset().start_height,
287                self.protocol_paramset().genesis_height
288            ));
289        }
290
291        if self.protocol_paramset().finality_depth < 1 {
292            reasons.push(format!(
293                "Finality depth ({}) cannot be less than 1",
294                self.protocol_paramset().finality_depth
295            ));
296        }
297
298        if !reasons.is_empty() {
299            return Err(BridgeError::ConfigError(format!(
300                "Invalid configuration due to: {}",
301                reasons.join(" - ")
302            )));
303        }
304
305        Ok(())
306    }
307
308    /// Checks various variables if they are correct for mainnet deployment.
309    pub fn check_mainnet_requirements(&self, actor_type: cli::Actors) -> Result<(), BridgeError> {
310        if self.protocol_paramset().network != Network::Bitcoin {
311            return Ok(());
312        }
313
314        let mut misconfigs = Vec::new();
315
316        if actor_type == cli::Actors::Operator {
317            if self.operator_collateral_funding_outpoint.is_none() {
318                misconfigs.push("OPERATOR_COLLATERAL_FUNDING_OUTPOINT is not set".to_string());
319            }
320            if self.operator_reimbursement_address.is_none() {
321                misconfigs.push("OPERATOR_REIMBURSEMENT_ADDRESS is not set".to_string());
322            }
323        }
324
325        if matches!(actor_type, cli::Actors::Verifier | cli::Actors::Operator)
326            && !self.client_verification
327        {
328            misconfigs.push("CLIENT_VERIFICATION=false".to_string());
329        }
330
331        /// Checks if an env var is set to a non 0 value.
332        fn check_env_var(env_var: &str, misconfigs: &mut Vec<String>) {
333            if let Ok(var) = std::env::var(env_var) {
334                if var == "0" || var.eq_ignore_ascii_case("false") {
335                    return;
336                }
337
338                misconfigs.push(format!("{env_var}={var}"));
339            }
340        }
341
342        check_env_var("DISABLE_NOFN_CHECK", &mut misconfigs);
343
344        if is_dev_mode() {
345            misconfigs.push("Risc0 dev mode is enabled (RISC0_DEV_MODE=1)".to_string());
346        }
347
348        if is_test_vk() {
349            misconfigs.push("use-test-vk feature is enabled".to_string());
350        }
351
352        if !misconfigs.is_empty() {
353            return Err(BridgeError::ConfigError(format!(
354                "Following configs can't be used on Mainnet: {misconfigs:?}",
355            )));
356        }
357
358        Ok(())
359    }
360}
361
362// only needed for one test
363#[cfg(test)]
364impl PartialEq for BridgeConfig {
365    fn eq(&self, other: &Self) -> bool {
366        use secrecy::ExposeSecret;
367
368        let all_eq = self.protocol_paramset == other.protocol_paramset
369            && self.host == other.host
370            && self.port == other.port
371            && self.secret_key == other.secret_key
372            && self.operator_withdrawal_fee_sats == other.operator_withdrawal_fee_sats
373            && self.bitcoin_rpc_url == other.bitcoin_rpc_url
374            && self.bitcoin_rpc_user.expose_secret() == other.bitcoin_rpc_user.expose_secret()
375            && self.bitcoin_rpc_password.expose_secret()
376                == other.bitcoin_rpc_password.expose_secret()
377            && self.db_host == other.db_host
378            && self.db_port == other.db_port
379            && self.db_user.expose_secret() == other.db_user.expose_secret()
380            && self.db_password.expose_secret() == other.db_password.expose_secret()
381            && self.db_name == other.db_name
382            && self.citrea_rpc_url == other.citrea_rpc_url
383            && self.citrea_light_client_prover_url == other.citrea_light_client_prover_url
384            && self.citrea_chain_id == other.citrea_chain_id
385            && self.bridge_contract_address == other.bridge_contract_address
386            && self.header_chain_proof_path == other.header_chain_proof_path
387            && self.security_council == other.security_council
388            && self.verifier_endpoints == other.verifier_endpoints
389            && self.operator_endpoints == other.operator_endpoints
390            && self.operator_reimbursement_address == other.operator_reimbursement_address
391            && self.operator_collateral_funding_outpoint
392                == other.operator_collateral_funding_outpoint
393            && self.server_cert_path == other.server_cert_path
394            && self.server_key_path == other.server_key_path
395            && self.client_cert_path == other.client_cert_path
396            && self.client_key_path == other.client_key_path
397            && self.ca_cert_path == other.ca_cert_path
398            && self.client_verification == other.client_verification
399            && self.aggregator_cert_path == other.aggregator_cert_path
400            && self.test_params == other.test_params
401            && self.grpc == other.grpc;
402
403        all_eq
404    }
405}
406
407impl Default for BridgeConfig {
408    fn default() -> Self {
409        Self {
410            protocol_paramset: Default::default(),
411            host: "127.0.0.1".to_string(),
412            port: 17000,
413
414            secret_key: SecretKey::from_str(
415                "1111111111111111111111111111111111111111111111111111111111111111",
416            )
417            .expect("known valid input"),
418
419            operator_withdrawal_fee_sats: Some(Amount::from_sat(100000)),
420
421            bitcoin_rpc_url: "http://127.0.0.1:18443/wallet/admin".to_string(),
422            bitcoin_rpc_user: "admin".to_string().into(),
423            bitcoin_rpc_password: "admin".to_string().into(),
424            mempool_api_host: None,
425            mempool_api_endpoint: None,
426
427            db_host: "127.0.0.1".to_string(),
428            db_port: 5432,
429            db_user: "clementine".to_string().into(),
430            db_password: "clementine".to_string().into(),
431            db_name: "clementine".to_string(),
432
433            citrea_rpc_url: "".to_string(),
434            citrea_light_client_prover_url: "".to_string(),
435            citrea_chain_id: 5655,
436            bridge_contract_address: "3100000000000000000000000000000000000002".to_string(),
437            citrea_request_timeout: None,
438
439            header_chain_proof_path: None,
440            header_chain_proof_batch_size: 100,
441
442            operator_reimbursement_address: None,
443            operator_collateral_funding_outpoint: None,
444
445            security_council: SecurityCouncil {
446                pks: vec![
447                    XOnlyPublicKey::from_str(
448                        "9ac20335eb38768d2052be1dbbc3c8f6178407458e51e6b4ad22f1d91758895b",
449                    )
450                    .expect("valid xonly"),
451                    XOnlyPublicKey::from_str(
452                        "5ab4689e400a4a160cf01cd44730845a54768df8547dcdf073d964f109f18c30",
453                    )
454                    .expect("valid xonly"),
455                ],
456                threshold: 1,
457            },
458
459            verifier_endpoints: None,
460            operator_endpoints: None,
461
462            server_cert_path: PathBuf::from("certs/server/server.pem"),
463            server_key_path: PathBuf::from("certs/server/server.key"),
464            client_cert_path: PathBuf::from("certs/client/client.pem"),
465            client_key_path: PathBuf::from("certs/client/client.key"),
466            ca_cert_path: PathBuf::from("certs/ca/ca.pem"),
467            aggregator_cert_path: PathBuf::from("certs/aggregator/aggregator.pem"),
468            client_verification: true,
469            aggregator_verification_address: Some(
470                alloy::primitives::Address::from_str("0x242fbec93465ce42b3d7c0e1901824a2697193fd")
471                    .expect("valid address"),
472            ),
473            emergency_stop_encryption_public_key: Some(
474                hex::decode("025d32d10ec7b899df4eeb4d80918b7f0a1f2a28f6af24f71aa2a59c69c0d531")
475                    .expect("valid hex")
476                    .try_into()
477                    .expect("valid key"),
478            ),
479
480            telemetry: Some(TelemetryConfig::default()),
481
482            time_to_send_watchtower_challenge: 4 * BLOCKS_PER_HOUR * 3 / 2,
483
484            #[cfg(test)]
485            test_params: test::TestParams::default(),
486
487            // New hardening parameters, optional so they don't break existing configs.
488            grpc: default_grpc_limits(),
489            tx_sender_limits: default_tx_sender_limits(),
490        }
491    }
492}
493
494#[derive(Debug, Clone, Deserialize)]
495pub struct TelemetryConfig {
496    pub host: String,
497    pub port: u16,
498}
499
500impl Default for TelemetryConfig {
501    fn default() -> Self {
502        Self {
503            host: "0.0.0.0".to_string(),
504            port: 8081,
505        }
506    }
507}
508
509impl TelemetryConfig {
510    pub fn from_env() -> Result<Self, BridgeError> {
511        let host = read_string_from_env("TELEMETRY_HOST")?;
512        let port = read_string_from_env_then_parse::<u16>("TELEMETRY_PORT")?;
513        Ok(Self { host, port })
514    }
515}
516
517#[cfg(test)]
518mod tests {
519    use super::BridgeConfig;
520    use crate::{cli, config::protocol::REGTEST_PARAMSET};
521    use bitcoin::{hashes::Hash, Network, OutPoint, Txid};
522    use std::{
523        fs::{self, File},
524        io::Write,
525    };
526
527    #[test]
528    fn parse_from_string() {
529        // In case of a incorrect file content, we should receive an error.
530        let content = "brokenfilecontent";
531        assert!(BridgeConfig::try_parse_from(content.to_string()).is_err());
532    }
533
534    #[test]
535    fn parse_from_file() {
536        let file_name = "parse_from_file";
537        let content = "invalid file content";
538        let mut file = File::create(file_name).unwrap();
539        file.write_all(content.as_bytes()).unwrap();
540
541        assert!(BridgeConfig::try_parse_file(file_name.into()).is_err());
542
543        // Read first example test file use for this test.
544        let base_path = env!("CARGO_MANIFEST_DIR");
545        let config_path = format!("{base_path}/src/test/data/bridge_config.toml");
546        let content = fs::read_to_string(config_path).unwrap();
547        let mut file = File::create(file_name).unwrap();
548        file.write_all(content.as_bytes()).unwrap();
549
550        BridgeConfig::try_parse_file(file_name.into()).unwrap();
551
552        fs::remove_file(file_name).unwrap();
553    }
554
555    #[test]
556    fn parse_from_file_with_invalid_headers() {
557        let file_name = "parse_from_file_with_invalid_headers";
558        let content = "[header1]
559        num_verifiers = 4
560
561        [header2]
562        confirmation_threshold = 1
563        network = \"regtest\"
564        bitcoin_rpc_url = \"http://localhost:18443\"
565        bitcoin_rpc_user = \"admin\"
566        bitcoin_rpc_password = \"admin\"\n";
567        let mut file = File::create(file_name).unwrap();
568        file.write_all(content.as_bytes()).unwrap();
569
570        assert!(BridgeConfig::try_parse_file(file_name.into()).is_err());
571
572        fs::remove_file(file_name).unwrap();
573    }
574
575    #[test]
576    fn test_test_config_parseable() {
577        let content = include_str!("../test/data/bridge_config.toml");
578        BridgeConfig::try_parse_from(content.to_string()).unwrap();
579    }
580
581    pub const INVALID_PARAMSET: crate::config::ProtocolParamset = crate::config::ProtocolParamset {
582        network: Network::Bitcoin,
583        ..REGTEST_PARAMSET
584    };
585    #[ignore = "Fails if bridge-circuit-host has use-test-vk feature! Which it will, if --all-features is specified at Cargo invocation."]
586    #[serial_test::serial]
587    #[test]
588    fn check_mainnet_reqs() {
589        let env_vars = vec!["DISABLE_NOFN_CHECK", "RISC0_DEV_MODE"];
590
591        // Nothing illegal is set.
592        for var in env_vars.clone() {
593            std::env::remove_var(var);
594        }
595        let mainnet_config = BridgeConfig {
596            protocol_paramset: &INVALID_PARAMSET,
597            client_verification: true,
598            operator_collateral_funding_outpoint: Some(OutPoint {
599                txid: Txid::all_zeros(),
600                vout: 0,
601            }),
602            ..Default::default()
603        };
604        let checks = mainnet_config.check_mainnet_requirements(cli::Actors::Operator);
605        println!("checks: {checks:?}");
606        assert!(checks.is_ok());
607
608        // Nothing illegal is set while illegal env vars set to 0 specifically.
609        for var in env_vars.clone() {
610            std::env::set_var(var, "0");
611        }
612        let checks = mainnet_config.check_mainnet_requirements(cli::Actors::Operator);
613        println!("checks: {checks:?}");
614        assert!(checks.is_ok());
615
616        // Illegal configs, no illegal env vars.
617        let incorrect_mainnet_config = BridgeConfig {
618            client_verification: false,
619            operator_collateral_funding_outpoint: None,
620            ..mainnet_config.clone()
621        };
622        let checks = incorrect_mainnet_config.check_mainnet_requirements(cli::Actors::Operator);
623        println!("checks: {checks:?}");
624        assert!(checks.is_err());
625
626        // No illegal configs, illegal env vars.
627        for var in env_vars.clone() {
628            std::env::set_var(var, "1");
629        }
630        let checks = mainnet_config.check_mainnet_requirements(cli::Actors::Operator);
631        println!("checks: {checks:?}");
632        assert!(checks.is_err());
633
634        // Illegal everything.
635        for var in env_vars.clone() {
636            std::env::set_var(var, "1");
637        }
638        let checks = incorrect_mainnet_config.check_mainnet_requirements(cli::Actors::Operator);
639        println!("checks: {checks:?}");
640        assert!(checks.is_err());
641    }
642
643    #[tokio::test]
644    async fn test_check_general_requirements_ok() {
645        use crate::test::common::{create_regtest_rpc, create_test_config_with_thread_name};
646
647        let mut config = create_test_config_with_thread_name().await;
648        // Point config to an isolated regtest bitcoind
649        let _regtest = create_regtest_rpc(&mut config).await;
650
651        // Should pass with proper paramset and node
652        config
653            .check_general_requirements()
654            .await
655            .expect("general requirements should pass on regtest");
656    }
657
658    #[tokio::test]
659    async fn test_check_general_requirements_multiple_errors() {
660        use crate::test::common::{create_regtest_rpc, create_test_config_with_thread_name};
661
662        // Paramset that intentionally violates multiple constraints to aggregate errors
663        pub const BAD_PARAMSET: crate::config::ProtocolParamset = crate::config::ProtocolParamset {
664            // Force hash mismatch
665            genesis_chain_state_hash: [1u8; 32],
666            // Make start height lower than genesis height to trigger that error
667            start_height: 0,
668            genesis_height: 1,
669            // Make finality depth invalid
670            finality_depth: 0,
671            ..REGTEST_PARAMSET
672        };
673
674        let mut config = create_test_config_with_thread_name().await;
675        // Use regtest node but set wrong/invalid paramset values to trigger multiple errors
676        let _regtest = create_regtest_rpc(&mut config).await;
677        config.protocol_paramset = &BAD_PARAMSET;
678
679        let res = config.check_general_requirements().await;
680        assert!(res.is_err());
681        let err = format!("{}", res.unwrap_err());
682        assert!(
683            err.contains("Genesis chain state hash"),
684            "unexpected error: {err}"
685        );
686        assert!(err.contains("Start height"), "unexpected error: {err}");
687        assert!(err.contains("Finality depth"), "unexpected error: {err}");
688    }
689}