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: u16,
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 #[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 #[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: 60_000,
374 include_unsafe: false,
375 jsonrpc: None,
376 }
377 }
378}
379
380#[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 grpc: default_grpc_limits(),
507 tx_sender_limits: default_tx_sender_limits(),
508 }
509 }
510}
511
512pub use clementine_config::TelemetryConfig;
514
515pub trait TelemetryConfigExt {
517 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 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 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 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 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 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 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 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 let _regtest = create_regtest_rpc(&mut config).await;
662
663 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 pub const BAD_PARAMSET: crate::config::ProtocolParamset = crate::config::ProtocolParamset {
676 genesis_chain_state_hash: [1u8; 32],
678 start_height: 0,
680 genesis_height: 1,
681 finality_depth: 0,
683 ..REGTEST_PARAMSET
684 };
685
686 let mut config = create_test_config_with_thread_name().await;
687 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}