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::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#[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
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, tcp_keepalive_secs: 60,
188 req_concurrency_limit: 300, 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 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, }
211}
212
213impl BridgeConfig {
214 pub fn new() -> Self {
216 BridgeConfig {
217 ..Default::default()
218 }
219 }
220
221 pub fn protocol_paramset(&self) -> &'static ProtocolParamset {
223 self.protocol_paramset
224 }
225
226 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 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 pub async fn check_general_requirements(&self) -> Result<(), BridgeError> {
255 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 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 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#[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 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 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 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 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 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 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 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 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 let _regtest = create_regtest_rpc(&mut config).await;
650
651 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 pub const BAD_PARAMSET: crate::config::ProtocolParamset = crate::config::ProtocolParamset {
664 genesis_chain_state_hash: [1u8; 32],
666 start_height: 0,
668 genesis_height: 1,
669 finality_depth: 0,
671 ..REGTEST_PARAMSET
672 };
673
674 let mut config = create_test_config_with_thread_name().await;
675 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}