1use super::BridgeConfig;
4use crate::{
5 config::{
6 default_grpc_limits, default_tx_sender_limits, GrpcLimits, GrpcLimitsExt, TelemetryConfig,
7 TelemetryConfigExt, TxSenderLimits, TxSenderLimitsExt,
8 },
9 deposit::SecurityCouncil,
10};
11use bitcoin::{address::NetworkUnchecked, secp256k1::SecretKey, Amount};
12use clementine_errors::BridgeError;
13use eyre::Context;
14use std::{path::PathBuf, str::FromStr, time::Duration};
15
16pub(crate) fn read_string_from_env(env_var: &'static str) -> Result<String, BridgeError> {
17 std::env::var(env_var).map_err(|e| BridgeError::EnvVarNotSet(e, env_var))
18}
19
20pub(crate) fn read_string_from_env_then_parse<T: std::str::FromStr>(
21 env_var: &'static str,
22) -> Result<T, BridgeError>
23where
24 <T as FromStr>::Err: std::fmt::Debug,
25{
26 read_string_from_env(env_var)?
27 .parse::<T>()
28 .map_err(|e| BridgeError::EnvVarMalformed(env_var, format!("{e:?}")))
29}
30
31impl GrpcLimitsExt for GrpcLimits {
32 fn from_env() -> Result<GrpcLimits, BridgeError> {
33 let defaults = default_grpc_limits();
34 Ok(GrpcLimits {
35 max_message_size: read_string_from_env_then_parse::<usize>("GRPC_MAX_MESSAGE_SIZE")
36 .unwrap_or(defaults.max_message_size),
37 timeout_secs: read_string_from_env_then_parse::<u64>("GRPC_TIMEOUT_SECS")
38 .unwrap_or(defaults.timeout_secs),
39 tcp_keepalive_secs: read_string_from_env_then_parse::<u64>("GRPC_TCP_KEEPALIVE_SECS")
40 .unwrap_or(defaults.tcp_keepalive_secs),
41 req_concurrency_limit: read_string_from_env_then_parse::<usize>(
42 "GRPC_REQ_CONCURRENCY_LIMIT",
43 )
44 .unwrap_or(defaults.req_concurrency_limit),
45 ratelimit_req_count: read_string_from_env_then_parse::<usize>(
46 "GRPC_RATELIMIT_REQ_COUNT",
47 )
48 .unwrap_or(defaults.ratelimit_req_count),
49 ratelimit_req_interval_secs: read_string_from_env_then_parse::<u64>(
50 "GRPC_RATELIMIT_REQ_INTERVAL_SECS",
51 )
52 .unwrap_or(defaults.ratelimit_req_interval_secs),
53 })
54 }
55}
56
57impl TxSenderLimitsExt for TxSenderLimits {
58 fn from_env() -> Result<TxSenderLimits, BridgeError> {
59 let defaults = default_tx_sender_limits();
60 Ok(TxSenderLimits {
61 fee_rate_hard_cap: read_string_from_env_then_parse::<u64>(
62 "TX_SENDER_FEE_RATE_HARD_CAP",
63 )
64 .unwrap_or(defaults.fee_rate_hard_cap),
65 mempool_fee_rate_multiplier: read_string_from_env_then_parse::<u64>(
66 "TX_SENDER_MEMPOOL_FEE_RATE_MULTIPLIER",
67 )
68 .unwrap_or(defaults.mempool_fee_rate_multiplier),
69 mempool_fee_rate_offset_sat_kvb: read_string_from_env_then_parse::<u64>(
70 "TX_SENDER_MEMPOOL_FEE_RATE_OFFSET_SAT_KVB",
71 )
72 .unwrap_or(defaults.mempool_fee_rate_offset_sat_kvb),
73 cpfp_fee_payer_bump_wait_time_seconds: read_string_from_env_then_parse::<u64>(
74 "TX_SENDER_CPFP_FEE_PAYER_BUMP_WAIT_TIME_SECONDS",
75 )
76 .unwrap_or(defaults.cpfp_fee_payer_bump_wait_time_seconds),
77 fee_bump_after_blocks: read_string_from_env_then_parse::<u32>(
78 "TX_SENDER_FEE_BUMP_AFTER_BLOCKS",
79 )
80 .unwrap_or(defaults.fee_bump_after_blocks),
81 min_bump_kvb: read_string_from_env_then_parse::<u64>("TX_SENDER_MIN_BUMP_KVB")
82 .unwrap_or(defaults.min_bump_kvb),
83 })
84 }
85}
86
87impl BridgeConfig {
88 pub fn from_env() -> Result<Self, BridgeError> {
89 let verifier_endpoints =
90 std::env::var("VERIFIER_ENDPOINTS")
91 .ok()
92 .map(|verifier_endpoints| {
93 verifier_endpoints
94 .split(",")
95 .collect::<Vec<&str>>()
96 .iter()
97 .map(|x| x.to_string())
98 .collect::<Vec<String>>()
99 });
100 let operator_endpoints =
101 std::env::var("OPERATOR_ENDPOINTS")
102 .ok()
103 .map(|operator_endpoints| {
104 operator_endpoints
105 .split(",")
106 .collect::<Vec<&str>>()
107 .iter()
108 .map(|x| x.to_string())
109 .collect::<Vec<String>>()
110 });
111
112 let operator_withdrawal_fee_sats = if let Ok(operator_withdrawal_fee_sats) =
113 std::env::var("OPERATOR_WITHDRAWAL_FEE_SATS")
114 {
115 Some(Amount::from_sat(
116 operator_withdrawal_fee_sats.parse::<u64>().map_err(|e| {
117 BridgeError::EnvVarMalformed("OPERATOR_WITHDRAWAL_FEE_SATS", e.to_string())
118 })?,
119 ))
120 } else {
121 None
122 };
123
124 let header_chain_proof_path =
125 if let Ok(header_chain_proof_path) = std::env::var("HEADER_CHAIN_PROOF_PATH") {
126 Some(PathBuf::from(header_chain_proof_path))
127 } else {
128 None
129 };
130
131 let operator_reimbursement_address = if let Ok(operator_reimbursement_address) =
132 std::env::var("OPERATOR_REIMBURSEMENT_ADDRESS")
133 {
134 Some(
135 operator_reimbursement_address
136 .parse::<bitcoin::Address<NetworkUnchecked>>()
137 .map_err(|e| {
138 BridgeError::EnvVarMalformed(
139 "OPERATOR_REIMBURSEMENT_ADDRESS",
140 e.to_string(),
141 )
142 })?,
143 )
144 } else {
145 None
146 };
147
148 let operator_collateral_funding_outpoint = if let Ok(operator_collateral_funding_outpoint) =
149 std::env::var("OPERATOR_COLLATERAL_FUNDING_OUTPOINT")
150 {
151 Some(
152 operator_collateral_funding_outpoint
153 .parse::<bitcoin::OutPoint>()
154 .map_err(|e| {
155 BridgeError::EnvVarMalformed(
156 "OPERATOR_COLLATERAL_FUNDING_OUTPOINT",
157 e.to_string(),
158 )
159 })?,
160 )
161 } else {
162 None
163 };
164
165 let aggregator_verification_address = std::env::var("AGGREGATOR_VERIFICATION_ADDRESS")
166 .ok()
167 .map(|addr| {
168 addr.parse::<alloy::primitives::Address>()
169 .wrap_err("Failed to parse AGGREGATOR_VERIFICATION_ADDRESS")
170 })
171 .transpose()?;
172
173 let server_cert_path = read_string_from_env("SERVER_CERT_PATH").map(PathBuf::from)?;
175 let server_key_path = read_string_from_env("SERVER_KEY_PATH").map(PathBuf::from)?;
176 let client_cert_path = read_string_from_env("CLIENT_CERT_PATH").map(PathBuf::from)?;
177 let ca_cert_path = read_string_from_env("CA_CERT_PATH").map(PathBuf::from)?;
178 let client_key_path = read_string_from_env("CLIENT_KEY_PATH").map(PathBuf::from)?;
179 let aggregator_cert_path =
180 read_string_from_env("AGGREGATOR_CERT_PATH").map(PathBuf::from)?;
181 let client_verification =
182 read_string_from_env("CLIENT_VERIFICATION").is_ok_and(|s| s == "true" || s == "1");
183
184 let security_council_string = read_string_from_env("SECURITY_COUNCIL")?;
185
186 let security_council = SecurityCouncil::from_str(&security_council_string)?;
187
188 let citrea_request_timeout = std::env::var("CITREA_REQUEST_TIMEOUT")
189 .ok()
190 .and_then(|timeout| timeout.parse::<u64>().ok())
191 .map(Duration::from_secs);
192
193 let config = BridgeConfig {
194 protocol_paramset: Default::default(),
196 host: read_string_from_env("HOST")?,
197 port: read_string_from_env_then_parse::<u16>("PORT")?,
198 secret_key: read_string_from_env_then_parse::<SecretKey>("SECRET_KEY")?,
199 operator_withdrawal_fee_sats,
200 operator_reimbursement_address,
201 operator_collateral_funding_outpoint,
202 bitcoin_rpc_url: read_string_from_env("BITCOIN_RPC_URL")?,
203 bitcoin_rpc_user: read_string_from_env("BITCOIN_RPC_USER")?.into(),
204 bitcoin_rpc_password: read_string_from_env("BITCOIN_RPC_PASSWORD")?.into(),
205 mempool_api_host: read_string_from_env("MEMPOOL_API_HOST").ok(),
206 mempool_api_endpoint: read_string_from_env("MEMPOOL_API_ENDPOINT").ok(),
207 db_host: read_string_from_env("DB_HOST")?,
208 db_port: read_string_from_env_then_parse::<u16>("DB_PORT")?,
209 db_user: read_string_from_env("DB_USER")?.into(),
210 db_password: read_string_from_env("DB_PASSWORD")?.into(),
211 db_name: read_string_from_env("DB_NAME")?,
212 citrea_rpc_url: read_string_from_env("CITREA_RPC_URL")?,
213 citrea_light_client_prover_url: read_string_from_env("CITREA_LIGHT_CLIENT_PROVER_URL")?,
214 citrea_chain_id: read_string_from_env_then_parse::<u32>("CITREA_CHAIN_ID")?,
215 citrea_request_timeout,
216 bridge_contract_address: read_string_from_env("BRIDGE_CONTRACT_ADDRESS")?,
217 header_chain_proof_batch_size: read_string_from_env_then_parse::<u32>(
218 "HEADER_CHAIN_PROOF_BATCH_SIZE",
219 )?,
220 header_chain_proof_path,
221 verifier_endpoints,
222 operator_endpoints,
223 security_council,
224 aggregator_verification_address,
225 client_verification,
226 server_cert_path,
227 server_key_path,
228 ca_cert_path,
229 client_cert_path,
230 client_key_path,
231 aggregator_cert_path,
232 emergency_stop_encryption_public_key: read_string_from_env(
233 "EMERGENCY_STOP_ENCRYPTION_PUBLIC_KEY",
234 )
235 .ok()
236 .map(|key| {
237 hex::decode(key)
238 .expect("valid hex")
239 .try_into()
240 .expect("valid key")
241 }),
242
243 telemetry: TelemetryConfig::from_env().ok(),
244 grpc: GrpcLimits::from_env()?,
245 tx_sender_limits: TxSenderLimits::from_env()?,
246
247 time_to_send_watchtower_challenge: read_string_from_env_then_parse::<u16>(
248 "TIME_TO_SEND_WATCHTOWER_CHALLENGE",
249 )?,
250
251 #[cfg(test)]
252 test_params: super::TestParams::default(),
253 };
254
255 tracing::debug!("BridgeConfig from env: {:?}", config);
256 Ok(config)
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use secrecy::ExposeSecret;
263
264 use crate::config::protocol::ProtocolParamsetExt;
265 use crate::config::{
266 protocol::{ProtocolParamset, REGTEST_PARAMSET},
267 BridgeConfig,
268 };
269
270 #[test]
271 #[serial_test::serial]
272 fn get_config_from_env_vars() {
273 let default_config = BridgeConfig::default();
274
275 std::env::set_var("HOST", &default_config.host);
276 std::env::set_var("PORT", default_config.port.to_string());
277 std::env::set_var(
278 "SECRET_KEY",
279 default_config.secret_key.display_secret().to_string(),
280 );
281 if let Some(ref operator_withdrawal_fee_sats) = default_config.operator_withdrawal_fee_sats
282 {
283 std::env::set_var(
284 "OPERATOR_WITHDRAWAL_FEE_SATS",
285 operator_withdrawal_fee_sats.to_sat().to_string(),
286 );
287 }
288 std::env::set_var("BITCOIN_RPC_URL", &default_config.bitcoin_rpc_url);
289 std::env::set_var(
290 "BITCOIN_RPC_USER",
291 default_config.bitcoin_rpc_user.expose_secret(),
292 );
293 std::env::set_var(
294 "BITCOIN_RPC_PASSWORD",
295 default_config.bitcoin_rpc_password.expose_secret(),
296 );
297 std::env::set_var("DB_HOST", default_config.db_host.clone());
298 std::env::set_var("DB_PORT", default_config.db_port.to_string());
299 std::env::set_var("DB_USER", default_config.db_user.expose_secret());
300 std::env::set_var("DB_PASSWORD", default_config.db_password.expose_secret());
301 std::env::set_var("DB_NAME", &default_config.db_name);
302 std::env::set_var("CITREA_RPC_URL", &default_config.citrea_rpc_url);
303 std::env::set_var(
304 "CITREA_LIGHT_CLIENT_PROVER_URL",
305 &default_config.citrea_light_client_prover_url,
306 );
307 std::env::set_var(
308 "CITREA_CHAIN_ID",
309 default_config.citrea_chain_id.to_string(),
310 );
311 std::env::set_var(
312 "BRIDGE_CONTRACT_ADDRESS",
313 &default_config.bridge_contract_address,
314 );
315 std::env::set_var(
316 "AGGREGATOR_CERT_PATH",
317 default_config.aggregator_cert_path.clone(),
318 );
319 std::env::set_var("CLIENT_CERT_PATH", default_config.client_cert_path.clone());
320 std::env::set_var("CLIENT_KEY_PATH", default_config.client_key_path.clone());
321 std::env::set_var("SERVER_CERT_PATH", default_config.server_cert_path.clone());
322 std::env::set_var("SERVER_KEY_PATH", default_config.server_key_path.clone());
323 std::env::set_var("CA_CERT_PATH", default_config.ca_cert_path.clone());
324 std::env::set_var(
325 "CLIENT_VERIFICATION",
326 default_config.client_verification.to_string(),
327 );
328
329 std::env::set_var(
330 "SECURITY_COUNCIL",
331 default_config.security_council.to_string(),
332 );
333
334 if let Some(ref header_chain_proof_path) = default_config.header_chain_proof_path {
335 std::env::set_var("HEADER_CHAIN_PROOF_PATH", header_chain_proof_path);
336 }
337 if let Some(ref verifier_endpoints) = default_config.verifier_endpoints {
338 std::env::set_var("VERIFIER_ENDPOINTS", verifier_endpoints.join(","));
339 }
340 if let Some(ref operator_endpoints) = default_config.operator_endpoints {
341 std::env::set_var("OPERATOR_ENDPOINTS", operator_endpoints.join(","));
342 }
343
344 if let Some(ref operator_reimbursement_address) =
345 default_config.operator_reimbursement_address
346 {
347 std::env::set_var(
348 "OPERATOR_REIMBURSEMENT_ADDRESS",
349 operator_reimbursement_address
350 .to_owned()
351 .assume_checked()
352 .to_string(),
353 );
354 }
355
356 if let Some(ref operator_collateral_funding_outpoint) =
357 default_config.operator_collateral_funding_outpoint
358 {
359 std::env::set_var(
360 "OPERATOR_COLLATERAL_FUNDING_OUTPOINT",
361 operator_collateral_funding_outpoint.to_string(),
362 );
363 }
364
365 std::env::set_var(
366 "TELEMETRY_HOST",
367 default_config.telemetry.as_ref().unwrap().host.clone(),
368 );
369 std::env::set_var(
370 "TELEMETRY_PORT",
371 default_config.telemetry.as_ref().unwrap().port.to_string(),
372 );
373
374 std::env::set_var(
375 "TX_SENDER_FEE_RATE_HARD_CAP",
376 default_config
377 .tx_sender_limits
378 .fee_rate_hard_cap
379 .to_string(),
380 );
381
382 std::env::set_var(
383 "TX_SENDER_MEMPOOL_FEE_RATE_MULTIPLIER",
384 default_config
385 .tx_sender_limits
386 .mempool_fee_rate_multiplier
387 .to_string(),
388 );
389 std::env::set_var(
390 "TX_SENDER_MEMPOOL_FEE_RATE_OFFSET_SAT_KVB",
391 default_config
392 .tx_sender_limits
393 .mempool_fee_rate_offset_sat_kvb
394 .to_string(),
395 );
396 std::env::set_var(
397 "TX_SENDER_CPFP_FEE_PAYER_BUMP_WAIT_TIME_SECONDS",
398 default_config
399 .tx_sender_limits
400 .cpfp_fee_payer_bump_wait_time_seconds
401 .to_string(),
402 );
403 std::env::set_var(
404 "GRPC_MAX_MESSAGE_SIZE",
405 default_config.grpc.max_message_size.to_string(),
406 );
407 std::env::set_var(
408 "GRPC_TIMEOUT_SECS",
409 default_config.grpc.timeout_secs.to_string(),
410 );
411 std::env::set_var(
412 "GRPC_TCP_KEEPALIVE_SECS",
413 default_config.grpc.tcp_keepalive_secs.to_string(),
414 );
415 std::env::set_var(
416 "GRPC_CONCURRENCY_LIMIT",
417 default_config.grpc.req_concurrency_limit.to_string(),
418 );
419 std::env::set_var(
420 "GRPC_RATELIMIT_REQ_COUNT",
421 default_config.grpc.ratelimit_req_count.to_string(),
422 );
423 if let Some(ref aggregator_verification_address) =
424 default_config.aggregator_verification_address
425 {
426 std::env::set_var(
427 "AGGREGATOR_VERIFICATION_ADDRESS",
428 aggregator_verification_address.to_string(),
429 );
430 }
431
432 if let Some(ref emergency_stop_encryption_public_key) =
433 default_config.emergency_stop_encryption_public_key
434 {
435 std::env::set_var(
436 "EMERGENCY_STOP_ENCRYPTION_PUBLIC_KEY",
437 hex::encode(emergency_stop_encryption_public_key),
438 );
439 }
440
441 std::env::set_var(
442 "HEADER_CHAIN_PROOF_BATCH_SIZE",
443 default_config.header_chain_proof_batch_size.to_string(),
444 );
445
446 std::env::set_var(
447 "TIME_TO_SEND_WATCHTOWER_CHALLENGE",
448 default_config.time_to_send_watchtower_challenge.to_string(),
449 );
450
451 assert_eq!(super::BridgeConfig::from_env().unwrap(), default_config);
452 }
453
454 #[test]
455 #[serial_test::serial]
456 fn get_protocol_paramset_from_env_vars() {
457 let default_config = REGTEST_PARAMSET;
458
459 std::env::set_var("NETWORK", default_config.network.to_string());
460 std::env::set_var("NUM_ROUND_TXS", default_config.num_round_txs.to_string());
461 std::env::set_var(
462 "NUM_KICKOFFS_PER_ROUND",
463 default_config.num_kickoffs_per_round.to_string(),
464 );
465 std::env::set_var(
466 "NUM_SIGNED_KICKOFFS",
467 default_config.num_signed_kickoffs.to_string(),
468 );
469 std::env::set_var(
470 "BRIDGE_AMOUNT",
471 default_config.bridge_amount.to_sat().to_string(),
472 );
473 std::env::set_var(
474 "KICKOFF_AMOUNT",
475 default_config.kickoff_amount.to_sat().to_string(),
476 );
477 std::env::set_var(
478 "OPERATOR_CHALLENGE_AMOUNT",
479 default_config
480 .operator_challenge_amount
481 .to_sat()
482 .to_string(),
483 );
484 std::env::set_var(
485 "COLLATERAL_FUNDING_AMOUNT",
486 default_config
487 .collateral_funding_amount
488 .to_sat()
489 .to_string(),
490 );
491 std::env::set_var(
492 "KICKOFF_BLOCKHASH_COMMIT_LENGTH",
493 default_config.kickoff_blockhash_commit_length.to_string(),
494 );
495 std::env::set_var(
496 "WATCHTOWER_CHALLENGE_BYTES",
497 default_config.watchtower_challenge_bytes.to_string(),
498 );
499 std::env::set_var(
500 "WINTERNITZ_LOG_D",
501 default_config.winternitz_log_d.to_string(),
502 );
503 std::env::set_var(
504 "USER_TAKES_AFTER",
505 default_config.user_takes_after.to_string(),
506 );
507 std::env::set_var(
508 "OPERATOR_CHALLENGE_TIMEOUT_TIMELOCK",
509 default_config
510 .operator_challenge_timeout_timelock
511 .to_string(),
512 );
513 std::env::set_var(
514 "OPERATOR_CHALLENGE_NACK_TIMELOCK",
515 default_config.operator_challenge_nack_timelock.to_string(),
516 );
517 std::env::set_var(
518 "DISPROVE_TIMEOUT_TIMELOCK",
519 default_config.disprove_timeout_timelock.to_string(),
520 );
521 std::env::set_var(
522 "ASSERT_TIMEOUT_TIMELOCK",
523 default_config.assert_timeout_timelock.to_string(),
524 );
525 std::env::set_var(
526 "OPERATOR_REIMBURSE_TIMELOCK",
527 default_config.operator_reimburse_timelock.to_string(),
528 );
529 std::env::set_var(
530 "WATCHTOWER_CHALLENGE_TIMEOUT_TIMELOCK",
531 default_config
532 .watchtower_challenge_timeout_timelock
533 .to_string(),
534 );
535 std::env::set_var("FINALITY_DEPTH", default_config.finality_depth.to_string());
536 std::env::set_var("START_HEIGHT", default_config.start_height.to_string());
537 std::env::set_var("GENESIS_HEIGHT", default_config.genesis_height.to_string());
538 std::env::set_var(
539 "GENESIS_CHAIN_STATE_HASH",
540 hex::encode(default_config.genesis_chain_state_hash),
541 );
542 std::env::set_var(
543 "LATEST_BLOCKHASH_TIMEOUT_TIMELOCK",
544 default_config.latest_blockhash_timeout_timelock.to_string(),
545 );
546
547 std::env::set_var(
548 "BRIDGE_NONSTANDARD",
549 default_config.bridge_nonstandard.to_string(),
550 );
551
552 assert_eq!(ProtocolParamset::from_env().unwrap(), default_config);
553 }
554}