1use crate::config::protocol::ProtocolParamset;
7use crate::config::BridgeConfig;
8use crate::errors::BridgeError;
9use crate::errors::ErrorExt;
10use crate::utils::delayed_panic;
11use clap::Parser;
12use clap::ValueEnum;
13use eyre::Context;
14use std::env;
15use std::ffi::OsString;
16use std::path::PathBuf;
17use std::process;
18
19#[derive(Debug, Clone, Copy, ValueEnum, Eq, PartialEq)]
20pub enum Actors {
21 Verifier,
22 Operator,
23 Aggregator,
24 TestActor,
25}
26
27#[derive(Parser, Debug, Clone)]
29#[command(version, about, long_about = None)]
30pub struct Args {
31 pub actor: Actors,
33 #[arg(short, long)]
35 pub config: Option<PathBuf>,
36 #[arg(short, long)]
38 pub protocol_params: Option<PathBuf>,
39 #[arg(short, long, default_value_t = 3)]
41 pub verbose: u8,
42}
43
44fn parse_from<I, T>(itr: I) -> Result<Args, BridgeError>
46where
47 I: IntoIterator<Item = T>,
48 T: Into<OsString> + Clone,
49{
50 match Args::try_parse_from(itr) {
51 Ok(c) => Ok(c),
52 Err(e)
53 if matches!(
54 e.kind(),
55 clap::error::ErrorKind::DisplayHelp
56 | clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
57 | clap::error::ErrorKind::DisplayVersion
58 ) =>
59 {
60 Err(BridgeError::CLIDisplayAndExit(e.render()))
61 }
62 Err(e) => Err(BridgeError::ConfigError(e.to_string())),
63 }
64}
65
66#[derive(Debug, Clone, Eq, PartialEq)]
67pub enum ConfigSource {
68 File(PathBuf),
69 Env,
70}
71pub fn get_config_source(
113 read_from_env_name: &'static str,
114 provided_arg: Option<PathBuf>,
115) -> Result<ConfigSource, BridgeError> {
116 Ok(match std::env::var(read_from_env_name) {
117 Err(_) => ConfigSource::File(provided_arg.ok_or(BridgeError::ConfigError(
118 "No file path or environment variable provided for config file.".to_string(),
119 ))?),
120 Ok(str) if str == "0" || str == "off" => ConfigSource::File(provided_arg.ok_or(
121 BridgeError::ConfigError("No file path provided for config file.".to_string()),
122 )?),
123 Ok(str) => {
124 if str != "1" && str != "on" {
125 tracing::warn!("Unknown value for {read_from_env_name}: {str}. Expected 1/0/off/on. Defaulting to environment variables.");
126 }
127
128 if provided_arg.is_some() {
129 tracing::warn!("File path provided in CLI arguments while {read_from_env_name} is set to 1. Ignoring provided file path and reading from environment variables.");
130 }
131
132 ConfigSource::Env
133 }
134 })
135}
136
137pub fn get_cli_config() -> (BridgeConfig, Args) {
156 let args = env::args();
157
158 match get_cli_config_from_args(args) {
159 Ok(config) => config,
160 Err(e) => {
161 let e = e.into_eyre();
162 match e.root_cause().downcast_ref::<BridgeError>() {
163 Some(BridgeError::CLIDisplayAndExit(msg)) => {
164 println!("{msg}");
165 process::exit(0);
166 }
167 _ => delayed_panic!("Failed to get CLI config: {e:?}"),
168 }
169 }
170 }
171}
172
173fn get_cli_config_from_args<I, T>(itr: I) -> Result<(BridgeConfig, Args), BridgeError>
175where
176 I: IntoIterator<Item = T>,
177 T: Into<OsString> + Clone,
178{
179 let args = parse_from(itr).wrap_err("Failed to parse CLI arguments.")?;
180
181 let config_source = get_config_source("READ_CONFIG_FROM_ENV", args.config.clone());
182
183 let mut config =
184 match config_source.wrap_err("Failed to determine source for configuration.")? {
185 ConfigSource::File(config_file) => {
186 BridgeConfig::try_parse_file(config_file)
188 .wrap_err("Failed to read configuration from file.")?
189 }
190 ConfigSource::Env => BridgeConfig::from_env()
191 .wrap_err("Failed to read configuration from environment variables.")?,
192 };
193
194 let protocol_params_source =
195 get_config_source("READ_PARAMSET_FROM_ENV", args.protocol_params.clone())
196 .wrap_err("Failed to determine source for protocol parameters.")?;
197
198 let paramset: &'static ProtocolParamset = Box::leak(Box::new(match protocol_params_source {
202 ConfigSource::File(path) => ProtocolParamset::from_toml_file(path.as_path())
203 .wrap_err("Failed to read protocol parameters from file.")?,
204 ConfigSource::Env => ProtocolParamset::from_env()
205 .wrap_err("Failed to read protocol parameters from environment.")?,
206 }));
207
208 config.protocol_paramset = paramset;
210
211 Ok((config, args))
212}
213
214#[cfg(test)]
215mod tests {
216 use super::{get_cli_config_from_args, get_config_source, parse_from, ConfigSource};
217 use crate::cli::Actors;
218 use crate::errors::BridgeError;
219 use std::env;
220 use std::fs::File;
221 use std::io::Write;
222 use std::path::PathBuf;
223
224 #[test]
226 fn help_message() {
227 match parse_from(vec!["clementine-core", "--help"]) {
228 Ok(_) => panic!("expected configuration error"),
229 Err(BridgeError::CLIDisplayAndExit(_)) => {}
230 e => panic!("unexpected error {e:#?}"),
231 }
232 }
233
234 #[test]
237 fn version() {
238 match parse_from(vec!["clementine-core", "--version"]) {
239 Ok(_) => panic!("expected configuration error"),
240 Err(BridgeError::CLIDisplayAndExit(_)) => {}
241 e => panic!("unexpected error {e:#?}"),
242 }
243 }
244
245 fn with_env_var<F, T>(name: &str, value: Option<&str>, test: F) -> T
247 where
248 F: FnOnce() -> T,
249 {
250 let prev_value = env::var(name).ok();
251 match value {
252 Some(val) => env::set_var(name, val),
253 None => env::remove_var(name),
254 }
255 let result = test();
256 match prev_value {
257 Some(val) => env::set_var(name, val),
258 None => env::remove_var(name),
259 }
260 result
261 }
262
263 #[test]
264 #[serial_test::serial]
265 fn test_get_config_source_env_not_set() {
266 with_env_var("TEST_READ_FROM_ENV", None, || {
267 let path = PathBuf::from("/path/to/config");
268 let result = get_config_source("TEST_READ_FROM_ENV", Some(path.clone()));
269 assert_eq!(result.unwrap(), ConfigSource::File(path));
270
271 let result = get_config_source("TEST_READ_FROM_ENV", None);
273 assert!(result.is_err());
274 assert!(matches!(result.unwrap_err(), BridgeError::ConfigError(_)));
275 })
276 }
277
278 #[test]
279 #[serial_test::serial]
280 fn test_get_config_source_env_set_to_off() {
281 with_env_var("TEST_READ_FROM_ENV", Some("0"), || {
283 let path = PathBuf::from("/path/to/config");
284 let result = get_config_source("TEST_READ_FROM_ENV", Some(path.clone()));
285 assert_eq!(result.unwrap(), ConfigSource::File(path));
286
287 let result = get_config_source("TEST_READ_FROM_ENV", None);
289 assert!(result.is_err());
290 });
291
292 with_env_var("TEST_READ_FROM_ENV", Some("off"), || {
294 let path = PathBuf::from("/path/to/config");
295 let result = get_config_source("TEST_READ_FROM_ENV", Some(path.clone()));
296 assert_eq!(result.unwrap(), ConfigSource::File(path));
297 })
298 }
299
300 #[test]
301 #[serial_test::serial]
302 fn test_get_config_source_env_set_to_on() {
303 with_env_var("TEST_READ_FROM_ENV", Some("1"), || {
305 let result = get_config_source("TEST_READ_FROM_ENV", None);
306 assert_eq!(result.unwrap(), ConfigSource::Env);
307
308 let path = PathBuf::from("/path/to/config");
310 let result = get_config_source("TEST_READ_FROM_ENV", Some(path));
311 assert_eq!(result.unwrap(), ConfigSource::Env);
312 });
313
314 with_env_var("TEST_READ_FROM_ENV", Some("on"), || {
316 let result = get_config_source("TEST_READ_FROM_ENV", None);
317 assert_eq!(result.unwrap(), ConfigSource::Env);
318 })
319 }
320
321 #[test]
322 #[serial_test::serial]
323 fn test_get_config_source_env_unknown_value() {
324 with_env_var("TEST_READ_FROM_ENV", Some("invalid"), || {
325 let result = get_config_source("TEST_READ_FROM_ENV", None);
326 assert_eq!(result.unwrap(), ConfigSource::Env);
327 })
328 }
329
330 fn with_temp_config_file<F, T>(content: &str, test: F) -> T
332 where
333 F: FnOnce(PathBuf) -> T,
334 {
335 let temp_dir = tempfile::tempdir().unwrap();
336 let file_path = temp_dir.path().join("bridge_config.toml");
337
338 let mut file = File::create(&file_path).unwrap();
339 file.write_all(content.as_bytes()).unwrap();
340
341 let result = test(file_path);
342 temp_dir.close().unwrap();
343 result
344 }
345
346 fn setup_config_env_vars() {
348 env::set_var("HOST", "127.0.0.1");
349 env::set_var("PORT", "17000");
350 env::set_var(
351 "SECRET_KEY",
352 "1111111111111111111111111111111111111111111111111111111111111111",
353 );
354 env::set_var("OPERATOR_WITHDRAWAL_FEE_SATS", "100000");
355 env::set_var("BITCOIN_RPC_URL", "http://127.0.0.1:18443/wallet/admin");
356 env::set_var("BITCOIN_RPC_USER", "admin");
357 env::set_var("BITCOIN_RPC_PASSWORD", "admin");
358 env::set_var("DB_HOST", "127.0.0.1");
359 env::set_var("DB_PORT", "5432");
360 env::set_var("DB_USER", "clementine");
361 env::set_var("DB_PASSWORD", "clementine");
362 env::set_var("DB_NAME", "clementine");
363 env::set_var("CITREA_RPC_URL", "");
364 env::set_var("CITREA_LIGHT_CLIENT_PROVER_URL", "");
365 env::set_var("CITREA_CHAIN_ID", "5655");
366 env::set_var(
367 "BRIDGE_CONTRACT_ADDRESS",
368 "3100000000000000000000000000000000000002",
369 );
370 env::set_var("SERVER_CERT_PATH", "certs/server/server.pem");
371 env::set_var("SERVER_KEY_PATH", "certs/server/server.key");
372 env::set_var("CA_CERT_PATH", "certs/ca/ca.pem");
373 env::set_var("CLIENT_CERT_PATH", "certs/client/client.pem");
374 env::set_var("CLIENT_KEY_PATH", "certs/client/client.key");
375 env::set_var("AGGREGATOR_CERT_PATH", "certs/aggregator/aggregator.pem");
376 env::set_var("CLIENT_VERIFICATION", "true");
377 env::set_var(
378 "SECURITY_COUNCIL",
379 "1:50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
380 );
381
382 env::set_var("TELEMETRY_HOST", "0.0.0.0");
383 env::set_var("TELEMETRY_PORT", "8081");
384 env::set_var("TX_SENDER_FEE_RATE_HARD_CAP", "100");
385 env::set_var("TX_SENDER_MEMPOOL_FEE_RATE_MULTIPLIER", "1");
386 env::set_var("TX_SENDER_MEMPOOL_FEE_RATE_OFFSET_SAT_KVB", "0");
387 env::set_var("TX_SENDER_CPFP_FEE_PAYER_BUMP_WAIT_TIME_SECONDS", "3600");
388 env::set_var("TIME_TO_SEND_WATCHTOWER_CHALLENGE", "216");
389 }
390
391 fn setup_protocol_paramset_env_vars() {
393 env::set_var("NETWORK", "regtest");
394 env::set_var("NUM_ROUND_TXS", "2");
395 env::set_var("NUM_KICKOFFS_PER_ROUND", "10");
396 env::set_var("NUM_SIGNED_KICKOFFS", "2");
397 env::set_var("BRIDGE_AMOUNT", "1000000000");
398 env::set_var("KICKOFF_AMOUNT", "0");
399 env::set_var("OPERATOR_CHALLENGE_AMOUNT", "200000000");
400 env::set_var("COLLATERAL_FUNDING_AMOUNT", "99000000");
401 env::set_var("KICKOFF_BLOCKHASH_COMMIT_LENGTH", "40");
402 env::set_var("WATCHTOWER_CHALLENGE_BYTES", "144");
403 env::set_var("WINTERNITZ_LOG_D", "4");
404 env::set_var("USER_TAKES_AFTER", "200");
405 env::set_var("OPERATOR_CHALLENGE_TIMEOUT_TIMELOCK", "144");
406 env::set_var("OPERATOR_CHALLENGE_NACK_TIMELOCK", "432");
407 env::set_var("DISPROVE_TIMEOUT_TIMELOCK", "720");
408 env::set_var("ASSERT_TIMEOUT_TIMELOCK", "576");
409 env::set_var("OPERATOR_REIMBURSE_TIMELOCK", "12");
410 env::set_var("WATCHTOWER_CHALLENGE_TIMEOUT_TIMELOCK", "288");
411 env::set_var("LATEST_BLOCKHASH_TIMEOUT_TIMELOCK", "360");
412 env::set_var("FINALITY_DEPTH", "1");
413 env::set_var("START_HEIGHT", "8148");
414 env::set_var("GENESIS_HEIGHT", "0");
415 env::set_var(
416 "GENESIS_CHAIN_STATE_HASH",
417 "5f7302ad16c8bd9ef2f3be00c8199a86f9e0ba861484abb4af5f7e457f8c2216",
418 );
419 env::set_var("HEADER_CHAIN_PROOF_BATCH_SIZE", "100");
420 env::set_var("BRIDGE_NONSTANDARD", "true");
421 }
422
423 fn cleanup_config_env_vars() {
425 env::remove_var("HOST");
426 env::remove_var("PORT");
427 env::remove_var("SECRET_KEY");
428 env::remove_var("OPERATOR_WITHDRAWAL_FEE_SATS");
429 env::remove_var("BITCOIN_RPC_URL");
430 env::remove_var("BITCOIN_RPC_USER");
431 env::remove_var("BITCOIN_RPC_PASSWORD");
432 env::remove_var("DB_HOST");
433 env::remove_var("DB_PORT");
434 env::remove_var("DB_USER");
435 env::remove_var("DB_PASSWORD");
436 env::remove_var("DB_NAME");
437 env::remove_var("CITREA_RPC_URL");
438 env::remove_var("CITREA_LIGHT_CLIENT_PROVER_URL");
439 env::remove_var("BRIDGE_CONTRACT_ADDRESS");
440 env::remove_var("SERVER_CERT_PATH");
441 env::remove_var("SERVER_KEY_PATH");
442 env::remove_var("CA_CERT_PATH");
443 env::remove_var("CLIENT_CERT_PATH");
444 env::remove_var("CLIENT_KEY_PATH");
445 env::remove_var("AGGREGATOR_CERT_PATH");
446 env::remove_var("CLIENT_VERIFICATION");
447 env::remove_var("SECURITY_COUNCIL");
448 env::remove_var("TELEMETRY_HOST");
449 env::remove_var("TELEMETRY_PORT");
450 env::remove_var("TIME_TO_SEND_WATCHTOWER_CHALLENGE");
451 }
452
453 fn cleanup_protocol_paramset_env_vars() {
455 env::remove_var("NETWORK");
456 env::remove_var("NUM_ROUND_TXS");
457 env::remove_var("NUM_KICKOFFS_PER_ROUND");
458 env::remove_var("NUM_SIGNED_KICKOFFS");
459 env::remove_var("BRIDGE_AMOUNT");
460 env::remove_var("KICKOFF_AMOUNT");
461 env::remove_var("OPERATOR_CHALLENGE_AMOUNT");
462 env::remove_var("COLLATERAL_FUNDING_AMOUNT");
463 env::remove_var("KICKOFF_BLOCKHASH_COMMIT_LENGTH");
464 env::remove_var("WATCHTOWER_CHALLENGE_BYTES");
465 env::remove_var("WINTERNITZ_LOG_D");
466 env::remove_var("USER_TAKES_AFTER");
467 env::remove_var("OPERATOR_CHALLENGE_TIMEOUT_TIMELOCK");
468 env::remove_var("OPERATOR_CHALLENGE_NACK_TIMELOCK");
469 env::remove_var("DISPROVE_TIMEOUT_TIMELOCK");
470 env::remove_var("ASSERT_TIMEOUT_TIMELOCK");
471 env::remove_var("OPERATOR_REIMBURSE_TIMELOCK");
472 env::remove_var("WATCHTOWER_CHALLENGE_TIMEOUT_TIMELOCK");
473 env::remove_var("FINALITY_DEPTH");
474 env::remove_var("START_HEIGHT");
475 env::remove_var("HEADER_CHAIN_PROOF_BATCH_SIZE");
476 }
477
478 const MINIMAL_CONFIG_CONTENT: &str = include_str!("test/data/bridge_config.toml");
480
481 #[test]
482 #[serial_test::serial]
483 fn test_get_cli_config_file_mode() {
484 with_env_var("READ_CONFIG_FROM_ENV", Some("0"), || {
485 with_temp_config_file(MINIMAL_CONFIG_CONTENT, |config_path| {
486 with_temp_config_file(
488 include_str!("./test/data/protocol_paramset.toml"),
489 |protocol_path| {
490 let args = vec![
491 "clementine-core",
492 "verifier",
493 "--config",
494 config_path.to_str().unwrap(),
495 "--protocol-params",
496 protocol_path.to_str().unwrap(),
497 ];
498
499 let result = get_cli_config_from_args(args);
500
501 let (config, cli_args) = result.expect("Failed to get CLI config");
502 assert_eq!(config.host, "127.0.0.1");
503 assert_eq!(config.port, 17000);
504 assert_eq!(cli_args.actor, Actors::Verifier);
505
506 assert_eq!(config.protocol_paramset.network.to_string(), "regtest");
508 assert_eq!(config.protocol_paramset.num_round_txs, 2);
509 assert_eq!(config.protocol_paramset.winternitz_log_d, 4);
510 },
511 )
512 })
513 })
514 }
515
516 #[test]
517 #[serial_test::serial]
518 fn test_get_cli_config_env_mode() {
519 setup_config_env_vars();
520 setup_protocol_paramset_env_vars();
521
522 with_env_var("READ_CONFIG_FROM_ENV", Some("1"), || {
523 with_env_var("READ_PARAMSET_FROM_ENV", Some("1"), || {
524 let args = vec!["clementine-core", "operator"];
525
526 let result = get_cli_config_from_args(args);
527
528 let (config, cli_args) = result.expect("Failed to get CLI config");
529 assert_eq!(config.host, "127.0.0.1");
530 assert_eq!(config.port, 17000);
531 assert_eq!(cli_args.actor, Actors::Operator);
532
533 assert_eq!(config.protocol_paramset.network.to_string(), "regtest");
535 assert_eq!(config.protocol_paramset.num_round_txs, 2);
536 assert_eq!(config.protocol_paramset.winternitz_log_d, 4);
537 assert_eq!(config.protocol_paramset.start_height, 8148); });
539 });
540
541 cleanup_config_env_vars();
542 cleanup_protocol_paramset_env_vars();
543 }
544
545 #[test]
546 #[serial_test::serial]
547 fn test_mixed_config_sources() {
548 setup_protocol_paramset_env_vars();
550
551 with_env_var("READ_CONFIG_FROM_ENV", Some("0"), || {
552 with_env_var("READ_PARAMSET_FROM_ENV", Some("1"), || {
553 with_temp_config_file(MINIMAL_CONFIG_CONTENT, |config_path| {
554 let args = vec![
555 "clementine-core",
556 "verifier",
557 "--config",
558 config_path.to_str().unwrap(),
559 ];
560
561 let result = get_cli_config_from_args(args);
562
563 let (config, cli_args) = result.expect("Failed to get CLI config");
564 assert_eq!(config.host, "127.0.0.1");
565 assert_eq!(config.port, 17000);
566 assert_eq!(cli_args.actor, Actors::Verifier);
567
568 assert_eq!(config.protocol_paramset.network.to_string(), "regtest");
570 assert_eq!(config.protocol_paramset.start_height, 8148); })
572 })
573 });
574
575 cleanup_protocol_paramset_env_vars();
576 }
577
578 #[test]
579 #[serial_test::serial]
580 fn test_get_cli_config_file_without_path() {
581 with_env_var("READ_CONFIG_FROM_ENV", Some("0"), || {
582 let args = vec!["clementine-core", "verifier"];
583
584 let result = get_cli_config_from_args(args);
585 result.expect_err("Expected error when config file path is not provided");
586 })
587 }
588}