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: 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// 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    pub fn mempool_config(&self) -> clementine_tx_sender::MempoolConfig {
336        clementine_tx_sender::MempoolConfig {
337            host: self.mempool_api_host.clone(),
338            endpoint: self.mempool_api_endpoint.clone(),
339        }
340    }
341}
342
343// only needed for one test
344#[cfg(test)]
345impl PartialEq for BridgeConfig {
346    fn eq(&self, other: &Self) -> bool {
347        use secrecy::ExposeSecret;
348
349        let all_eq = self.protocol_paramset == other.protocol_paramset
350            && self.host == other.host
351            && self.port == other.port
352            && self.secret_key == other.secret_key
353            && self.operator_withdrawal_fee_sats == other.operator_withdrawal_fee_sats
354            && self.bitcoin_rpc_url == other.bitcoin_rpc_url
355            && self.bitcoin_rpc_user.expose_secret() == other.bitcoin_rpc_user.expose_secret()
356            && self.bitcoin_rpc_password.expose_secret()
357                == other.bitcoin_rpc_password.expose_secret()
358            && self.db_host == other.db_host
359            && self.db_port == other.db_port
360            && self.db_user.expose_secret() == other.db_user.expose_secret()
361            && self.db_password.expose_secret() == other.db_password.expose_secret()
362            && self.db_name == other.db_name
363            && self.citrea_rpc_url == other.citrea_rpc_url
364            && self.citrea_light_client_prover_url == other.citrea_light_client_prover_url
365            && self.citrea_chain_id == other.citrea_chain_id
366            && self.bridge_contract_address == other.bridge_contract_address
367            && self.header_chain_proof_path == other.header_chain_proof_path
368            && self.security_council == other.security_council
369            && self.verifier_endpoints == other.verifier_endpoints
370            && self.operator_endpoints == other.operator_endpoints
371            && self.operator_reimbursement_address == other.operator_reimbursement_address
372            && self.operator_collateral_funding_outpoint
373                == other.operator_collateral_funding_outpoint
374            && self.server_cert_path == other.server_cert_path
375            && self.server_key_path == other.server_key_path
376            && self.client_cert_path == other.client_cert_path
377            && self.client_key_path == other.client_key_path
378            && self.ca_cert_path == other.ca_cert_path
379            && self.client_verification == other.client_verification
380            && self.aggregator_cert_path == other.aggregator_cert_path
381            && self.test_params == other.test_params
382            && self.grpc == other.grpc;
383
384        all_eq
385    }
386}
387
388impl Default for BridgeConfig {
389    fn default() -> Self {
390        Self {
391            protocol_paramset: Default::default(),
392            host: "127.0.0.1".to_string(),
393            port: 17000,
394
395            secret_key: SecretKey::from_str(
396                "1111111111111111111111111111111111111111111111111111111111111111",
397            )
398            .expect("known valid input"),
399
400            operator_withdrawal_fee_sats: Some(Amount::from_sat(100000)),
401
402            bitcoin_rpc_url: "http://127.0.0.1:18443/wallet/admin".to_string(),
403            bitcoin_rpc_user: "admin".to_string().into(),
404            bitcoin_rpc_password: "admin".to_string().into(),
405            mempool_api_host: None,
406            mempool_api_endpoint: None,
407
408            db_host: "127.0.0.1".to_string(),
409            db_port: 5432,
410            db_user: "clementine".to_string().into(),
411            db_password: "clementine".to_string().into(),
412            db_name: "clementine".to_string(),
413
414            citrea_rpc_url: "".to_string(),
415            citrea_light_client_prover_url: "".to_string(),
416            citrea_chain_id: 5655,
417            bridge_contract_address: "3100000000000000000000000000000000000002".to_string(),
418            citrea_request_timeout: None,
419
420            header_chain_proof_path: None,
421            header_chain_proof_batch_size: 100,
422
423            operator_reimbursement_address: None,
424            operator_collateral_funding_outpoint: None,
425
426            security_council: SecurityCouncil {
427                pks: vec![
428                    XOnlyPublicKey::from_str(
429                        "9ac20335eb38768d2052be1dbbc3c8f6178407458e51e6b4ad22f1d91758895b",
430                    )
431                    .expect("valid xonly"),
432                    XOnlyPublicKey::from_str(
433                        "5ab4689e400a4a160cf01cd44730845a54768df8547dcdf073d964f109f18c30",
434                    )
435                    .expect("valid xonly"),
436                ],
437                threshold: 1,
438            },
439
440            verifier_endpoints: None,
441            operator_endpoints: None,
442
443            server_cert_path: PathBuf::from("certs/server/server.pem"),
444            server_key_path: PathBuf::from("certs/server/server.key"),
445            client_cert_path: PathBuf::from("certs/client/client.pem"),
446            client_key_path: PathBuf::from("certs/client/client.key"),
447            ca_cert_path: PathBuf::from("certs/ca/ca.pem"),
448            aggregator_cert_path: PathBuf::from("certs/aggregator/aggregator.pem"),
449            client_verification: true,
450            aggregator_verification_address: Some(
451                alloy::primitives::Address::from_str("0x242fbec93465ce42b3d7c0e1901824a2697193fd")
452                    .expect("valid address"),
453            ),
454            emergency_stop_encryption_public_key: Some(
455                hex::decode("025d32d10ec7b899df4eeb4d80918b7f0a1f2a28f6af24f71aa2a59c69c0d531")
456                    .expect("valid hex")
457                    .try_into()
458                    .expect("valid key"),
459            ),
460
461            telemetry: Some(TelemetryConfig::default()),
462
463            time_to_send_watchtower_challenge: 4 * BLOCKS_PER_HOUR * 3 / 2,
464
465            #[cfg(test)]
466            test_params: test::TestParams::default(),
467
468            // New hardening parameters, optional so they don't break existing configs.
469            grpc: default_grpc_limits(),
470            tx_sender_limits: default_tx_sender_limits(),
471        }
472    }
473}
474
475// Re-export TelemetryConfig from clementine-config
476pub use clementine_config::TelemetryConfig;
477
478/// Extension trait for TelemetryConfig to add from_env method
479pub trait TelemetryConfigExt {
480    /// Create a TelemetryConfig from environment variables.
481    fn from_env() -> Result<TelemetryConfig, BridgeError>;
482}
483
484impl TelemetryConfigExt for TelemetryConfig {
485    fn from_env() -> Result<TelemetryConfig, BridgeError> {
486        let host = read_string_from_env("TELEMETRY_HOST")?;
487        let port = read_string_from_env_then_parse::<u16>("TELEMETRY_PORT")?;
488        Ok(TelemetryConfig { host, port })
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use super::BridgeConfig;
495    use crate::{cli, config::protocol::REGTEST_PARAMSET};
496    use bitcoin::{hashes::Hash, Network, OutPoint, Txid};
497    use std::{
498        fs::{self, File},
499        io::Write,
500    };
501
502    #[test]
503    fn parse_from_string() {
504        // In case of a incorrect file content, we should receive an error.
505        let content = "brokenfilecontent";
506        assert!(BridgeConfig::try_parse_from(content.to_string()).is_err());
507    }
508
509    #[test]
510    fn parse_from_file() {
511        let file_name = "parse_from_file";
512        let content = "invalid file content";
513        let mut file = File::create(file_name).unwrap();
514        file.write_all(content.as_bytes()).unwrap();
515
516        assert!(BridgeConfig::try_parse_file(file_name.into()).is_err());
517
518        // Read first example test file use for this test.
519        let base_path = env!("CARGO_MANIFEST_DIR");
520        let config_path = format!("{base_path}/src/test/data/bridge_config.toml");
521        let content = fs::read_to_string(config_path).unwrap();
522        let mut file = File::create(file_name).unwrap();
523        file.write_all(content.as_bytes()).unwrap();
524
525        BridgeConfig::try_parse_file(file_name.into()).unwrap();
526
527        fs::remove_file(file_name).unwrap();
528    }
529
530    #[test]
531    fn parse_from_file_with_invalid_headers() {
532        let file_name = "parse_from_file_with_invalid_headers";
533        let content = "[header1]
534        num_verifiers = 4
535
536        [header2]
537        confirmation_threshold = 1
538        network = \"regtest\"
539        bitcoin_rpc_url = \"http://localhost:18443\"
540        bitcoin_rpc_user = \"admin\"
541        bitcoin_rpc_password = \"admin\"\n";
542        let mut file = File::create(file_name).unwrap();
543        file.write_all(content.as_bytes()).unwrap();
544
545        assert!(BridgeConfig::try_parse_file(file_name.into()).is_err());
546
547        fs::remove_file(file_name).unwrap();
548    }
549
550    #[test]
551    fn test_test_config_parseable() {
552        let content = include_str!("../test/data/bridge_config.toml");
553        BridgeConfig::try_parse_from(content.to_string()).unwrap();
554    }
555
556    pub const INVALID_PARAMSET: crate::config::ProtocolParamset = crate::config::ProtocolParamset {
557        network: Network::Bitcoin,
558        ..REGTEST_PARAMSET
559    };
560    #[ignore = "Fails if bridge-circuit-host has use-test-vk feature! Which it will, if --all-features is specified at Cargo invocation."]
561    #[serial_test::serial]
562    #[test]
563    fn check_mainnet_reqs() {
564        let env_vars = vec!["DISABLE_NOFN_CHECK", "RISC0_DEV_MODE"];
565
566        // Nothing illegal is set.
567        for var in env_vars.clone() {
568            std::env::remove_var(var);
569        }
570        let mainnet_config = BridgeConfig {
571            protocol_paramset: &INVALID_PARAMSET,
572            client_verification: true,
573            operator_collateral_funding_outpoint: Some(OutPoint {
574                txid: Txid::all_zeros(),
575                vout: 0,
576            }),
577            ..Default::default()
578        };
579        let checks = mainnet_config.check_mainnet_requirements(cli::Actor::Operator);
580        println!("checks: {checks:?}");
581        assert!(checks.is_ok());
582
583        // Nothing illegal is set while illegal env vars set to 0 specifically.
584        for var in env_vars.clone() {
585            std::env::set_var(var, "0");
586        }
587        let checks = mainnet_config.check_mainnet_requirements(cli::Actor::Operator);
588        println!("checks: {checks:?}");
589        assert!(checks.is_ok());
590
591        // Illegal configs, no illegal env vars.
592        let incorrect_mainnet_config = BridgeConfig {
593            client_verification: false,
594            operator_collateral_funding_outpoint: None,
595            ..mainnet_config.clone()
596        };
597        let checks = incorrect_mainnet_config.check_mainnet_requirements(cli::Actor::Operator);
598        println!("checks: {checks:?}");
599        assert!(checks.is_err());
600
601        // No illegal configs, illegal env vars.
602        for var in env_vars.clone() {
603            std::env::set_var(var, "1");
604        }
605        let checks = mainnet_config.check_mainnet_requirements(cli::Actor::Operator);
606        println!("checks: {checks:?}");
607        assert!(checks.is_err());
608
609        // Illegal everything.
610        for var in env_vars.clone() {
611            std::env::set_var(var, "1");
612        }
613        let checks = incorrect_mainnet_config.check_mainnet_requirements(cli::Actor::Operator);
614        println!("checks: {checks:?}");
615        assert!(checks.is_err());
616    }
617
618    #[tokio::test]
619    async fn test_check_general_requirements_ok() {
620        use crate::test::common::{create_regtest_rpc, create_test_config_with_thread_name};
621
622        let mut config = create_test_config_with_thread_name().await;
623        // Point config to an isolated regtest bitcoind
624        let _regtest = create_regtest_rpc(&mut config).await;
625
626        // Should pass with proper paramset and node
627        config
628            .check_general_requirements()
629            .await
630            .expect("general requirements should pass on regtest");
631    }
632
633    #[tokio::test]
634    async fn test_check_general_requirements_multiple_errors() {
635        use crate::test::common::{create_regtest_rpc, create_test_config_with_thread_name};
636
637        // Paramset that intentionally violates multiple constraints to aggregate errors
638        pub const BAD_PARAMSET: crate::config::ProtocolParamset = crate::config::ProtocolParamset {
639            // Force hash mismatch
640            genesis_chain_state_hash: [1u8; 32],
641            // Make start height lower than genesis height to trigger that error
642            start_height: 0,
643            genesis_height: 1,
644            // Make finality depth invalid
645            finality_depth: 0,
646            ..REGTEST_PARAMSET
647        };
648
649        let mut config = create_test_config_with_thread_name().await;
650        // Use regtest node but set wrong/invalid paramset values to trigger multiple errors
651        let _regtest = create_regtest_rpc(&mut config).await;
652        config.protocol_paramset = &BAD_PARAMSET;
653
654        let res = config.check_general_requirements().await;
655        assert!(res.is_err());
656        let err = format!("{}", res.unwrap_err());
657        assert!(
658            err.contains("Genesis chain state hash"),
659            "unexpected error: {err}"
660        );
661        assert!(err.contains("Start height"), "unexpected error: {err}");
662        assert!(err.contains("Finality depth"), "unexpected error: {err}");
663    }
664}