use crate::config::env::{read_string_from_env, read_string_from_env_then_parse};
use crate::deposit::SecurityCouncil;
use crate::errors::BridgeError;
use bitcoin::address::NetworkUnchecked;
use bitcoin::secp256k1::SecretKey;
use bitcoin::{Address, Amount, OutPoint, XOnlyPublicKey};
use protocol::ProtocolParamset;
use secrecy::SecretString;
use serde::Deserialize;
use std::str::FromStr;
use std::time::Duration;
use std::{fs::File, io::Read, path::PathBuf};
pub mod env;
pub mod protocol;
#[cfg(test)]
mod test;
#[cfg(test)]
pub use test::*;
#[derive(Debug, Clone, Deserialize)]
pub struct BridgeConfig {
#[serde(skip)]
pub protocol_paramset: &'static ProtocolParamset,
pub host: String,
pub port: u16,
pub secret_key: SecretKey,
pub winternitz_secret_key: Option<SecretKey>,
pub operator_withdrawal_fee_sats: Option<Amount>,
pub bitcoin_rpc_url: String,
pub bitcoin_rpc_user: SecretString,
pub bitcoin_rpc_password: SecretString,
pub mempool_api_host: Option<String>,
pub mempool_api_endpoint: Option<String>,
pub db_host: String,
pub db_port: usize,
pub db_user: SecretString,
pub db_password: SecretString,
pub db_name: String,
pub citrea_rpc_url: String,
pub citrea_light_client_prover_url: String,
pub citrea_chain_id: u32,
pub citrea_request_timeout: Option<Duration>,
pub bridge_contract_address: String,
pub header_chain_proof_path: Option<PathBuf>,
pub security_council: SecurityCouncil,
pub verifier_endpoints: Option<Vec<String>>,
pub operator_endpoints: Option<Vec<String>>,
pub operator_reimbursement_address: Option<Address<NetworkUnchecked>>,
pub operator_collateral_funding_outpoint: Option<OutPoint>,
pub server_cert_path: PathBuf,
pub server_key_path: PathBuf,
pub client_cert_path: PathBuf,
pub client_key_path: PathBuf,
pub ca_cert_path: PathBuf,
pub client_verification: bool,
pub aggregator_cert_path: PathBuf,
pub telemetry: Option<TelemetryConfig>,
pub aggregator_verification_address: Option<alloy::primitives::Address>,
pub emergency_stop_encryption_public_key: Option<[u8; 32]>,
#[cfg(test)]
#[serde(skip)]
pub test_params: test::TestParams,
#[serde(default = "default_grpc_limits")]
pub grpc: GrpcLimits,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct GrpcLimits {
pub max_message_size: usize,
pub timeout_secs: u64,
pub tcp_keepalive_secs: u64,
pub req_concurrency_limit: usize,
pub ratelimit_req_count: usize,
pub ratelimit_req_interval_secs: u64,
}
fn default_grpc_limits() -> GrpcLimits {
GrpcLimits {
max_message_size: 4 * 1024 * 1024,
timeout_secs: 12 * 60 * 60, tcp_keepalive_secs: 60,
req_concurrency_limit: 300, ratelimit_req_count: 1000,
ratelimit_req_interval_secs: 60,
}
}
impl BridgeConfig {
pub fn new() -> Self {
BridgeConfig {
..Default::default()
}
}
pub fn protocol_paramset(&self) -> &'static ProtocolParamset {
self.protocol_paramset
}
pub fn try_parse_file(path: PathBuf) -> Result<Self, BridgeError> {
let mut contents = String::new();
let mut file = match File::open(path.clone()) {
Ok(f) => f,
Err(e) => return Err(BridgeError::ConfigError(e.to_string())),
};
if let Err(e) = file.read_to_string(&mut contents) {
return Err(BridgeError::ConfigError(e.to_string()));
}
tracing::trace!("Using configuration file: {:?}", path);
BridgeConfig::try_parse_from(contents)
}
pub fn try_parse_from(input: String) -> Result<Self, BridgeError> {
match toml::from_str::<BridgeConfig>(&input) {
Ok(c) => Ok(c),
Err(e) => Err(BridgeError::ConfigError(e.to_string())),
}
}
}
#[cfg(test)]
impl PartialEq for BridgeConfig {
fn eq(&self, other: &Self) -> bool {
use secrecy::ExposeSecret;
let all_eq = self.protocol_paramset == other.protocol_paramset
&& self.host == other.host
&& self.port == other.port
&& self.secret_key == other.secret_key
&& self.winternitz_secret_key == other.winternitz_secret_key
&& self.operator_withdrawal_fee_sats == other.operator_withdrawal_fee_sats
&& self.bitcoin_rpc_url == other.bitcoin_rpc_url
&& self.bitcoin_rpc_user.expose_secret() == other.bitcoin_rpc_user.expose_secret()
&& self.bitcoin_rpc_password.expose_secret()
== other.bitcoin_rpc_password.expose_secret()
&& self.db_host == other.db_host
&& self.db_port == other.db_port
&& self.db_user.expose_secret() == other.db_user.expose_secret()
&& self.db_password.expose_secret() == other.db_password.expose_secret()
&& self.db_name == other.db_name
&& self.citrea_rpc_url == other.citrea_rpc_url
&& self.citrea_light_client_prover_url == other.citrea_light_client_prover_url
&& self.citrea_chain_id == other.citrea_chain_id
&& self.bridge_contract_address == other.bridge_contract_address
&& self.header_chain_proof_path == other.header_chain_proof_path
&& self.security_council == other.security_council
&& self.verifier_endpoints == other.verifier_endpoints
&& self.operator_endpoints == other.operator_endpoints
&& self.operator_reimbursement_address == other.operator_reimbursement_address
&& self.operator_collateral_funding_outpoint
== other.operator_collateral_funding_outpoint
&& self.server_cert_path == other.server_cert_path
&& self.server_key_path == other.server_key_path
&& self.client_cert_path == other.client_cert_path
&& self.client_key_path == other.client_key_path
&& self.ca_cert_path == other.ca_cert_path
&& self.client_verification == other.client_verification
&& self.aggregator_cert_path == other.aggregator_cert_path
&& self.test_params == other.test_params
&& self.grpc == other.grpc;
all_eq
}
}
impl Default for BridgeConfig {
fn default() -> Self {
Self {
protocol_paramset: Default::default(),
host: "127.0.0.1".to_string(),
port: 17000,
secret_key: SecretKey::from_str(
"1111111111111111111111111111111111111111111111111111111111111111",
)
.expect("known valid input"),
operator_withdrawal_fee_sats: Some(Amount::from_sat(100000)),
bitcoin_rpc_url: "http://127.0.0.1:18443/wallet/admin".to_string(),
bitcoin_rpc_user: "admin".to_string().into(),
bitcoin_rpc_password: "admin".to_string().into(),
mempool_api_host: None,
mempool_api_endpoint: None,
db_host: "127.0.0.1".to_string(),
db_port: 5432,
db_user: "clementine".to_string().into(),
db_password: "clementine".to_string().into(),
db_name: "clementine".to_string(),
citrea_rpc_url: "".to_string(),
citrea_light_client_prover_url: "".to_string(),
citrea_chain_id: 5655,
bridge_contract_address: "3100000000000000000000000000000000000002".to_string(),
citrea_request_timeout: None,
header_chain_proof_path: None,
operator_reimbursement_address: None,
operator_collateral_funding_outpoint: None,
security_council: SecurityCouncil {
pks: vec![
XOnlyPublicKey::from_str(
"9ac20335eb38768d2052be1dbbc3c8f6178407458e51e6b4ad22f1d91758895b",
)
.expect("valid xonly"),
XOnlyPublicKey::from_str(
"5ab4689e400a4a160cf01cd44730845a54768df8547dcdf073d964f109f18c30",
)
.expect("valid xonly"),
],
threshold: 1,
},
winternitz_secret_key: Some(
SecretKey::from_str(
"2222222222222222222222222222222222222222222222222222222222222222",
)
.expect("known valid input"),
),
verifier_endpoints: None,
operator_endpoints: None,
server_cert_path: PathBuf::from("certs/server/server.pem"),
server_key_path: PathBuf::from("certs/server/server.key"),
client_cert_path: PathBuf::from("certs/client/client.pem"),
client_key_path: PathBuf::from("certs/client/client.key"),
ca_cert_path: PathBuf::from("certs/ca/ca.pem"),
aggregator_cert_path: PathBuf::from("certs/aggregator/aggregator.pem"),
client_verification: true,
aggregator_verification_address: Some(
alloy::primitives::Address::from_str("0x242fbec93465ce42b3d7c0e1901824a2697193fd")
.expect("valid address"),
),
emergency_stop_encryption_public_key: Some(
hex::decode("025d32d10ec7b899df4eeb4d80918b7f0a1f2a28f6af24f71aa2a59c69c0d531")
.expect("valid hex")
.try_into()
.expect("valid key"),
),
telemetry: Some(TelemetryConfig::default()),
#[cfg(test)]
test_params: test::TestParams::default(),
grpc: default_grpc_limits(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct TelemetryConfig {
pub host: String,
pub port: u16,
}
impl Default for TelemetryConfig {
fn default() -> Self {
Self {
host: "0.0.0.0".to_string(),
port: 8081,
}
}
}
impl TelemetryConfig {
pub fn from_env() -> Result<Self, BridgeError> {
let host = read_string_from_env("TELEMETRY_HOST")?;
let port = read_string_from_env_then_parse::<u16>("TELEMETRY_PORT")?;
Ok(Self { host, port })
}
}
#[cfg(test)]
mod tests {
use super::BridgeConfig;
use std::{
fs::{self, File},
io::Write,
};
#[test]
fn parse_from_string() {
let content = "brokenfilecontent";
assert!(BridgeConfig::try_parse_from(content.to_string()).is_err());
}
#[test]
fn parse_from_file() {
let file_name = "parse_from_file";
let content = "invalid file content";
let mut file = File::create(file_name).unwrap();
file.write_all(content.as_bytes()).unwrap();
assert!(BridgeConfig::try_parse_file(file_name.into()).is_err());
let base_path = env!("CARGO_MANIFEST_DIR");
let config_path = format!("{}/src/test/data/bridge_config.toml", base_path);
let content = fs::read_to_string(config_path).unwrap();
let mut file = File::create(file_name).unwrap();
file.write_all(content.as_bytes()).unwrap();
BridgeConfig::try_parse_file(file_name.into()).unwrap();
fs::remove_file(file_name).unwrap();
}
#[test]
fn parse_from_file_with_invalid_headers() {
let file_name = "parse_from_file_with_invalid_headers";
let content = "[header1]
num_verifiers = 4
[header2]
confirmation_threshold = 1
network = \"regtest\"
bitcoin_rpc_url = \"http://localhost:18443\"
bitcoin_rpc_user = \"admin\"
bitcoin_rpc_password = \"admin\"\n";
let mut file = File::create(file_name).unwrap();
file.write_all(content.as_bytes()).unwrap();
assert!(BridgeConfig::try_parse_file(file_name.into()).is_err());
fs::remove_file(file_name).unwrap();
}
#[test]
fn test_test_config_parseable() {
let content = include_str!("../test/data/bridge_config.toml");
BridgeConfig::try_parse_from(content.to_string()).unwrap();
}
}