1use 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#[derive(Debug, Clone, Deserialize)]
44pub struct BridgeConfig {
45 #[serde(skip)]
51 pub protocol_paramset: &'static ProtocolParamset,
52 pub host: String,
54 pub port: u16,
56 pub secret_key: SecretKey,
58 pub operator_withdrawal_fee_sats: Option<Amount>,
60 pub bitcoin_rpc_url: String,
62 pub bitcoin_rpc_user: SecretString,
64 pub bitcoin_rpc_password: SecretString,
66 pub mempool_api_host: Option<String>,
68 pub mempool_api_endpoint: Option<String>,
70
71 pub db_host: String,
73 pub db_port: usize,
75 pub db_user: SecretString,
77 pub db_password: SecretString,
79 pub db_name: String,
81 pub citrea_rpc_url: String,
83 pub citrea_light_client_prover_url: String,
85 pub citrea_chain_id: u32,
87 pub citrea_request_timeout: Option<Duration>,
89 pub bridge_contract_address: String,
91 pub header_chain_proof_path: Option<PathBuf>,
93 pub header_chain_proof_batch_size: u32,
95
96 pub security_council: SecurityCouncil,
98
99 pub verifier_endpoints: Option<Vec<String>>,
101 pub operator_endpoints: Option<Vec<String>>,
103
104 pub operator_reimbursement_address: Option<Address<NetworkUnchecked>>,
106
107 pub operator_collateral_funding_outpoint: Option<OutPoint>,
109
110 pub server_cert_path: PathBuf,
115 pub server_key_path: PathBuf,
117
118 pub client_cert_path: PathBuf,
127 pub client_key_path: PathBuf,
129
130 pub ca_cert_path: PathBuf,
133
134 pub client_verification: bool,
139
140 pub aggregator_cert_path: PathBuf,
144
145 pub telemetry: Option<TelemetryConfig>,
147
148 pub aggregator_verification_address: Option<alloy::primitives::Address>,
153
154 pub emergency_stop_encryption_public_key: Option<[u8; 32]>,
156
157 pub time_to_send_watchtower_challenge: u16,
159
160 #[cfg(test)]
161 #[serde(skip)]
162 pub test_params: test::TestParams,
163
164 #[serde(default = "default_grpc_limits")]
166 pub grpc: GrpcLimits,
167
168 #[serde(default = "default_tx_sender_limits")]
170 pub tx_sender_limits: TxSenderLimits,
171}
172
173pub 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
184pub trait GrpcLimitsExt {
186 fn from_env() -> Result<GrpcLimits, BridgeError>;
188}
189
190pub trait TxSenderLimitsExt {
192 fn from_env() -> Result<TxSenderLimits, BridgeError>;
194}
195
196impl BridgeConfig {
197 pub fn new() -> Self {
199 BridgeConfig {
200 ..Default::default()
201 }
202 }
203
204 pub fn protocol_paramset(&self) -> &'static ProtocolParamset {
206 self.protocol_paramset
207 }
208
209 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 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 pub async fn check_general_requirements(&self) -> Result<(), BridgeError> {
238 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 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 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#[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 grpc: default_grpc_limits(),
470 tx_sender_limits: default_tx_sender_limits(),
471 }
472 }
473}
474
475pub use clementine_config::TelemetryConfig;
477
478pub trait TelemetryConfigExt {
480 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 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 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 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 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 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 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 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 let _regtest = create_regtest_rpc(&mut config).await;
625
626 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 pub const BAD_PARAMSET: crate::config::ProtocolParamset = crate::config::ProtocolParamset {
639 genesis_chain_state_hash: [1u8; 32],
641 start_height: 0,
643 genesis_height: 1,
644 finality_depth: 0,
646 ..REGTEST_PARAMSET
647 };
648
649 let mut config = create_test_config_with_thread_name().await;
650 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}