1use crate::config::protocol::ProtocolParamset;
7use crate::config::protocol::ProtocolParamsetExt;
8use crate::config::BridgeConfig;
9use crate::utils::delayed_panic;
10use clap::Parser;
11use clementine_errors::BridgeError;
12use eyre::Context;
13use std::env;
14use std::path::PathBuf;
15use std::process;
16
17#[derive(Debug, Clone, Copy, Eq, PartialEq)]
19pub enum Actor {
20 Verifier,
21 Operator,
22 Aggregator,
23 TestActor,
24}
25
26#[derive(Parser, Debug, Clone)]
28#[command(version, about, long_about = None)]
29pub struct Args {
30 #[arg(short, long, default_value_t = 3, global = true)]
32 pub verbose: u8,
33
34 #[arg(short, long, global = true)]
36 pub config: Option<PathBuf>,
37
38 #[arg(short, long, global = true)]
40 pub protocol_params: Option<PathBuf>,
41
42 #[command(subcommand)]
43 pub command: Command,
44}
45
46#[derive(Debug, Clone, clap::Subcommand)]
47pub enum Command {
48 Verifier,
50 Operator,
52 Aggregator,
54 TestActor,
56 GenerateBitvmCache,
58}
59
60fn parse_cli_args<I, T>(itr: I) -> Result<Args, (i32, String)>
70where
71 I: IntoIterator<Item = T>,
72 T: Into<std::ffi::OsString> + Clone,
73{
74 match Args::try_parse_from(itr) {
75 Ok(c) => Ok(c),
76 Err(e)
77 if matches!(
78 e.kind(),
79 clap::error::ErrorKind::DisplayHelp
80 | clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
81 | clap::error::ErrorKind::DisplayVersion
82 ) =>
83 {
84 Err((0, e.to_string()))
85 }
86 Err(e) => Err((
87 1,
88 format!(
89 "Failed to parse CLI arguments: {}",
90 BridgeError::ConfigError(e.to_string())
91 ),
92 )),
93 }
94}
95
96pub fn get_cli_args() -> Args {
100 match parse_cli_args(env::args()) {
101 Ok(args) => args,
102 Err((code, msg)) => {
103 if code == 0 {
104 println!("{msg}");
105 } else {
106 eprintln!("{msg}");
107 }
108 process::exit(code);
109 }
110 }
111}
112
113#[derive(Debug, Clone, Eq, PartialEq)]
114pub enum ConfigSource {
115 File(PathBuf),
116 Env,
117}
118
119fn get_config_source(
161 read_from_env_name: &'static str,
162 provided_arg: Option<PathBuf>,
163) -> Result<ConfigSource, BridgeError> {
164 Ok(match std::env::var(read_from_env_name) {
165 Err(_) => ConfigSource::File(provided_arg.ok_or(BridgeError::ConfigError(
166 "No file path or environment variable provided for config file.".to_string(),
167 ))?),
168 Ok(str) if str == "0" || str == "off" => ConfigSource::File(provided_arg.ok_or(
169 BridgeError::ConfigError("No file path provided for config file.".to_string()),
170 )?),
171 Ok(str) => {
172 if str != "1" && str != "on" {
173 tracing::warn!("Unknown value for {read_from_env_name}: {str}. Expected 1/0/off/on. Defaulting to environment variables.");
174 }
175
176 if provided_arg.is_some() {
177 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.");
178 }
179
180 ConfigSource::Env
181 }
182 })
183}
184
185pub fn get_config(args: Args) -> BridgeConfig {
204 match get_config_from_args(args) {
205 Ok(config) => config,
206 Err(e) => delayed_panic!("Failed to load configuration: {e:?}"),
207 }
208}
209
210fn get_config_from_args(args: Args) -> Result<BridgeConfig, BridgeError> {
212 let config_source = get_config_source("READ_CONFIG_FROM_ENV", args.config.clone());
213
214 let mut config =
215 match config_source.wrap_err("Failed to determine source for configuration.")? {
216 ConfigSource::File(config_file) => {
217 BridgeConfig::try_parse_file(config_file)
219 .wrap_err("Failed to read configuration from file.")?
220 }
221 ConfigSource::Env => BridgeConfig::from_env()
222 .wrap_err("Failed to read configuration from environment variables.")?,
223 };
224
225 let protocol_params_source =
226 get_config_source("READ_PARAMSET_FROM_ENV", args.protocol_params.clone())
227 .wrap_err("Failed to determine source for protocol parameters.")?;
228
229 let paramset: &'static ProtocolParamset = Box::leak(Box::new(match protocol_params_source {
233 ConfigSource::File(path) => ProtocolParamset::from_toml_file(path.as_path())
234 .wrap_err("Failed to read protocol parameters from file.")?,
235 ConfigSource::Env => ProtocolParamset::from_env()
236 .wrap_err("Failed to read protocol parameters from environment.")?,
237 }));
238
239 config.protocol_paramset = paramset;
241
242 Ok(config)
243}
244
245#[cfg(test)]
246mod tests {
247 use super::{get_config_from_args, get_config_source, parse_cli_args, Command, ConfigSource};
248 use clementine_errors::BridgeError;
249 use std::env;
250 use std::fs::File;
251 use std::io::Write;
252 use std::path::PathBuf;
253
254 #[test]
256 fn help_message() {
257 match parse_cli_args(vec!["clementine-core", "--help"]) {
258 Ok(_) => panic!("expected configuration error"),
259 Err((0, _)) => {}
260 e => panic!("unexpected error {e:#?}"),
261 }
262 }
263
264 #[test]
267 fn version() {
268 match parse_cli_args(vec!["clementine-core", "--version"]) {
269 Ok(_) => panic!("expected configuration error"),
270 Err((0, _)) => {}
271 e => panic!("unexpected error {e:#?}"),
272 }
273 }
274
275 fn with_env_var<F, T>(name: &str, value: Option<&str>, test: F) -> T
277 where
278 F: FnOnce() -> T,
279 {
280 let prev_value = env::var(name).ok();
281 match value {
282 Some(val) => env::set_var(name, val),
283 None => env::remove_var(name),
284 }
285 let result = test();
286 match prev_value {
287 Some(val) => env::set_var(name, val),
288 None => env::remove_var(name),
289 }
290 result
291 }
292
293 #[test]
294 #[serial_test::serial]
295 fn test_get_config_source_env_not_set() {
296 with_env_var("TEST_READ_FROM_ENV", None, || {
297 let path = PathBuf::from("/path/to/config");
298 let result = get_config_source("TEST_READ_FROM_ENV", Some(path.clone()));
299 assert_eq!(result.unwrap(), ConfigSource::File(path));
300
301 let result = get_config_source("TEST_READ_FROM_ENV", None);
303 assert!(result.is_err());
304 assert!(matches!(result.unwrap_err(), BridgeError::ConfigError(_)));
305 })
306 }
307
308 #[test]
309 #[serial_test::serial]
310 fn test_get_config_source_env_set_to_off() {
311 with_env_var("TEST_READ_FROM_ENV", Some("0"), || {
313 let path = PathBuf::from("/path/to/config");
314 let result = get_config_source("TEST_READ_FROM_ENV", Some(path.clone()));
315 assert_eq!(result.unwrap(), ConfigSource::File(path));
316
317 let result = get_config_source("TEST_READ_FROM_ENV", None);
319 assert!(result.is_err());
320 });
321
322 with_env_var("TEST_READ_FROM_ENV", Some("off"), || {
324 let path = PathBuf::from("/path/to/config");
325 let result = get_config_source("TEST_READ_FROM_ENV", Some(path.clone()));
326 assert_eq!(result.unwrap(), ConfigSource::File(path));
327 })
328 }
329
330 #[test]
331 #[serial_test::serial]
332 fn test_get_config_source_env_set_to_on() {
333 with_env_var("TEST_READ_FROM_ENV", Some("1"), || {
335 let result = get_config_source("TEST_READ_FROM_ENV", None);
336 assert_eq!(result.unwrap(), ConfigSource::Env);
337
338 let path = PathBuf::from("/path/to/config");
340 let result = get_config_source("TEST_READ_FROM_ENV", Some(path));
341 assert_eq!(result.unwrap(), ConfigSource::Env);
342 });
343
344 with_env_var("TEST_READ_FROM_ENV", Some("on"), || {
346 let result = get_config_source("TEST_READ_FROM_ENV", None);
347 assert_eq!(result.unwrap(), ConfigSource::Env);
348 })
349 }
350
351 #[test]
352 #[serial_test::serial]
353 fn test_get_config_source_env_unknown_value() {
354 with_env_var("TEST_READ_FROM_ENV", Some("invalid"), || {
355 let result = get_config_source("TEST_READ_FROM_ENV", None);
356 assert_eq!(result.unwrap(), ConfigSource::Env);
357 })
358 }
359
360 fn with_temp_config_file<F, T>(content: &str, test: F) -> T
362 where
363 F: FnOnce(PathBuf) -> T,
364 {
365 let temp_dir = tempfile::tempdir().unwrap();
366 let file_path = temp_dir.path().join("bridge_config.toml");
367
368 let mut file = File::create(&file_path).unwrap();
369 file.write_all(content.as_bytes()).unwrap();
370
371 let result = test(file_path);
372 temp_dir.close().unwrap();
373 result
374 }
375
376 fn setup_config_env_vars() {
378 env::set_var("HOST", "127.0.0.1");
379 env::set_var("PORT", "17000");
380 env::set_var(
381 "SECRET_KEY",
382 "1111111111111111111111111111111111111111111111111111111111111111",
383 );
384 env::set_var("OPERATOR_WITHDRAWAL_FEE_SATS", "100000");
385 env::set_var("BITCOIN_RPC_URL", "http://127.0.0.1:18443/wallet/admin");
386 env::set_var("BITCOIN_RPC_USER", "admin");
387 env::set_var("BITCOIN_RPC_PASSWORD", "admin");
388 env::set_var("DB_HOST", "127.0.0.1");
389 env::set_var("DB_PORT", "5432");
390 env::set_var("DB_USER", "clementine");
391 env::set_var("DB_PASSWORD", "clementine");
392 env::set_var("DB_NAME", "clementine");
393 env::set_var("CITREA_RPC_URL", "");
394 env::set_var("CITREA_LIGHT_CLIENT_PROVER_URL", "");
395 env::set_var("CITREA_CHAIN_ID", "5655");
396 env::set_var(
397 "BRIDGE_CONTRACT_ADDRESS",
398 "3100000000000000000000000000000000000002",
399 );
400 env::set_var("SERVER_CERT_PATH", "certs/server/server.pem");
401 env::set_var("SERVER_KEY_PATH", "certs/server/server.key");
402 env::set_var("CA_CERT_PATH", "certs/ca/ca.pem");
403 env::set_var("CLIENT_CERT_PATH", "certs/client/client.pem");
404 env::set_var("CLIENT_KEY_PATH", "certs/client/client.key");
405 env::set_var("AGGREGATOR_CERT_PATH", "certs/aggregator/aggregator.pem");
406 env::set_var("CLIENT_VERIFICATION", "true");
407 env::set_var(
408 "SECURITY_COUNCIL",
409 "1:50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
410 );
411
412 env::set_var("TELEMETRY_HOST", "0.0.0.0");
413 env::set_var("TELEMETRY_PORT", "8081");
414 env::set_var("TX_SENDER_FEE_RATE_HARD_CAP", "100");
415 env::set_var("TX_SENDER_MEMPOOL_FEE_RATE_MULTIPLIER", "1");
416 env::set_var("TX_SENDER_MEMPOOL_FEE_RATE_OFFSET_SAT_KVB", "0");
417 env::set_var("TX_SENDER_CPFP_FEE_PAYER_BUMP_WAIT_TIME_SECONDS", "3600");
418 env::set_var("TIME_TO_SEND_WATCHTOWER_CHALLENGE", "216");
419 }
420
421 fn setup_protocol_paramset_env_vars() {
423 env::set_var("NETWORK", "regtest");
424 env::set_var("NUM_ROUND_TXS", "2");
425 env::set_var("NUM_KICKOFFS_PER_ROUND", "10");
426 env::set_var("NUM_SIGNED_KICKOFFS", "2");
427 env::set_var("BRIDGE_AMOUNT", "1000000000");
428 env::set_var("KICKOFF_AMOUNT", "0");
429 env::set_var("OPERATOR_CHALLENGE_AMOUNT", "200000000");
430 env::set_var("COLLATERAL_FUNDING_AMOUNT", "99000000");
431 env::set_var("KICKOFF_BLOCKHASH_COMMIT_LENGTH", "40");
432 env::set_var("WATCHTOWER_CHALLENGE_BYTES", "144");
433 env::set_var("WINTERNITZ_LOG_D", "4");
434 env::set_var("USER_TAKES_AFTER", "200");
435 env::set_var("OPERATOR_CHALLENGE_TIMEOUT_TIMELOCK", "144");
436 env::set_var("OPERATOR_CHALLENGE_NACK_TIMELOCK", "432");
437 env::set_var("DISPROVE_TIMEOUT_TIMELOCK", "720");
438 env::set_var("ASSERT_TIMEOUT_TIMELOCK", "576");
439 env::set_var("OPERATOR_REIMBURSE_TIMELOCK", "12");
440 env::set_var("WATCHTOWER_CHALLENGE_TIMEOUT_TIMELOCK", "288");
441 env::set_var("LATEST_BLOCKHASH_TIMEOUT_TIMELOCK", "360");
442 env::set_var("FINALITY_DEPTH", "1");
443 env::set_var("START_HEIGHT", "8148");
444 env::set_var("GENESIS_HEIGHT", "0");
445 env::set_var(
446 "GENESIS_CHAIN_STATE_HASH",
447 "5f7302ad16c8bd9ef2f3be00c8199a86f9e0ba861484abb4af5f7e457f8c2216",
448 );
449 env::set_var("HEADER_CHAIN_PROOF_BATCH_SIZE", "100");
450 env::set_var("BRIDGE_NONSTANDARD", "true");
451 }
452
453 fn cleanup_config_env_vars() {
455 env::remove_var("HOST");
456 env::remove_var("PORT");
457 env::remove_var("SECRET_KEY");
458 env::remove_var("OPERATOR_WITHDRAWAL_FEE_SATS");
459 env::remove_var("BITCOIN_RPC_URL");
460 env::remove_var("BITCOIN_RPC_USER");
461 env::remove_var("BITCOIN_RPC_PASSWORD");
462 env::remove_var("DB_HOST");
463 env::remove_var("DB_PORT");
464 env::remove_var("DB_USER");
465 env::remove_var("DB_PASSWORD");
466 env::remove_var("DB_NAME");
467 env::remove_var("CITREA_RPC_URL");
468 env::remove_var("CITREA_LIGHT_CLIENT_PROVER_URL");
469 env::remove_var("BRIDGE_CONTRACT_ADDRESS");
470 env::remove_var("SERVER_CERT_PATH");
471 env::remove_var("SERVER_KEY_PATH");
472 env::remove_var("CA_CERT_PATH");
473 env::remove_var("CLIENT_CERT_PATH");
474 env::remove_var("CLIENT_KEY_PATH");
475 env::remove_var("AGGREGATOR_CERT_PATH");
476 env::remove_var("CLIENT_VERIFICATION");
477 env::remove_var("SECURITY_COUNCIL");
478 env::remove_var("TELEMETRY_HOST");
479 env::remove_var("TELEMETRY_PORT");
480 env::remove_var("TIME_TO_SEND_WATCHTOWER_CHALLENGE");
481 }
482
483 fn cleanup_protocol_paramset_env_vars() {
485 env::remove_var("NETWORK");
486 env::remove_var("NUM_ROUND_TXS");
487 env::remove_var("NUM_KICKOFFS_PER_ROUND");
488 env::remove_var("NUM_SIGNED_KICKOFFS");
489 env::remove_var("BRIDGE_AMOUNT");
490 env::remove_var("KICKOFF_AMOUNT");
491 env::remove_var("OPERATOR_CHALLENGE_AMOUNT");
492 env::remove_var("COLLATERAL_FUNDING_AMOUNT");
493 env::remove_var("KICKOFF_BLOCKHASH_COMMIT_LENGTH");
494 env::remove_var("WATCHTOWER_CHALLENGE_BYTES");
495 env::remove_var("WINTERNITZ_LOG_D");
496 env::remove_var("USER_TAKES_AFTER");
497 env::remove_var("OPERATOR_CHALLENGE_TIMEOUT_TIMELOCK");
498 env::remove_var("OPERATOR_CHALLENGE_NACK_TIMELOCK");
499 env::remove_var("DISPROVE_TIMEOUT_TIMELOCK");
500 env::remove_var("ASSERT_TIMEOUT_TIMELOCK");
501 env::remove_var("OPERATOR_REIMBURSE_TIMELOCK");
502 env::remove_var("WATCHTOWER_CHALLENGE_TIMEOUT_TIMELOCK");
503 env::remove_var("FINALITY_DEPTH");
504 env::remove_var("START_HEIGHT");
505 env::remove_var("HEADER_CHAIN_PROOF_BATCH_SIZE");
506 }
507
508 const MINIMAL_CONFIG_CONTENT: &str = include_str!("test/data/bridge_config.toml");
510
511 #[test]
512 #[serial_test::serial]
513 fn test_get_cli_config_file_mode() {
514 with_env_var("READ_CONFIG_FROM_ENV", Some("0"), || {
515 with_temp_config_file(MINIMAL_CONFIG_CONTENT, |config_path| {
516 with_temp_config_file(
518 include_str!("./test/data/protocol_paramset.toml"),
519 |protocol_path| {
520 let args = vec![
521 "clementine-core",
522 "verifier",
523 "--config",
524 config_path.to_str().unwrap(),
525 "--protocol-params",
526 protocol_path.to_str().unwrap(),
527 ];
528
529 let cli_args = parse_cli_args(args).expect("Failed to parse CLI arguments");
530 let result = get_config_from_args(cli_args.clone());
531
532 let config = result.expect("Failed to get CLI config");
533 assert_eq!(config.host, "127.0.0.1");
534 assert_eq!(config.port, 17000);
535 assert!(matches!(cli_args.command, Command::Verifier));
536
537 assert_eq!(config.protocol_paramset.network.to_string(), "regtest");
539 assert_eq!(config.protocol_paramset.num_round_txs, 2);
540 assert_eq!(config.protocol_paramset.winternitz_log_d, 4);
541 },
542 )
543 })
544 })
545 }
546
547 #[test]
548 #[serial_test::serial]
549 fn test_get_cli_config_env_mode() {
550 setup_config_env_vars();
551 setup_protocol_paramset_env_vars();
552
553 with_env_var("READ_CONFIG_FROM_ENV", Some("1"), || {
554 with_env_var("READ_PARAMSET_FROM_ENV", Some("1"), || {
555 let args = vec!["clementine-core", "operator"];
556
557 let cli_args = parse_cli_args(args).expect("Failed to parse CLI arguments");
558 let result = get_config_from_args(cli_args.clone());
559
560 let config = result.expect("Failed to get CLI config");
561 assert_eq!(config.host, "127.0.0.1");
562 assert_eq!(config.port, 17000);
563 assert!(matches!(cli_args.command, Command::Operator));
564
565 assert_eq!(config.protocol_paramset.network.to_string(), "regtest");
567 assert_eq!(config.protocol_paramset.num_round_txs, 2);
568 assert_eq!(config.protocol_paramset.winternitz_log_d, 4);
569 assert_eq!(config.protocol_paramset.start_height, 8148); });
571 });
572
573 cleanup_config_env_vars();
574 cleanup_protocol_paramset_env_vars();
575 }
576
577 #[test]
578 #[serial_test::serial]
579 fn test_mixed_config_sources() {
580 setup_protocol_paramset_env_vars();
582
583 with_env_var("READ_CONFIG_FROM_ENV", Some("0"), || {
584 with_env_var("READ_PARAMSET_FROM_ENV", Some("1"), || {
585 with_temp_config_file(MINIMAL_CONFIG_CONTENT, |config_path| {
586 let args = vec![
587 "clementine-core",
588 "verifier",
589 "--config",
590 config_path.to_str().unwrap(),
591 ];
592
593 let cli_args = parse_cli_args(args).expect("Failed to parse CLI arguments");
594 let result = get_config_from_args(cli_args.clone());
595
596 let config = result.expect("Failed to get CLI config");
597 assert_eq!(config.host, "127.0.0.1");
598 assert_eq!(config.port, 17000);
599 assert!(matches!(cli_args.command, Command::Verifier));
600
601 assert_eq!(config.protocol_paramset.network.to_string(), "regtest");
603 assert_eq!(config.protocol_paramset.start_height, 8148); })
605 })
606 });
607
608 cleanup_protocol_paramset_env_vars();
609 }
610
611 #[test]
612 #[serial_test::serial]
613 fn test_get_cli_config_file_without_path() {
614 with_env_var("READ_CONFIG_FROM_ENV", Some("0"), || {
615 let args = vec!["clementine-core", "verifier"];
616
617 let cli_args = parse_cli_args(args).expect("Failed to parse CLI arguments");
618 let result = get_config_from_args(cli_args.clone());
619 result.expect_err("Expected error when config file path is not provided");
620 })
621 }
622
623 #[test]
624 fn test_parse_cli_args_success() {
625 let args = vec!["clementine-core", "verifier"];
627
628 let result = parse_cli_args(args);
629 let parsed = result.expect("Expected successful CLI parsing");
630
631 assert!(matches!(parsed.command, Command::Verifier));
632 assert_eq!(parsed.verbose, 3);
634 assert!(parsed.config.is_none());
636 assert!(parsed.protocol_params.is_none());
637 }
638
639 #[test]
640 fn test_parse_cli_args_help_and_version_exit_code_zero() {
641 let help_args = vec!["clementine-core", "--help"];
643 let help_result = parse_cli_args(help_args);
644 let (help_code, help_msg) =
645 help_result.expect_err("Expected help invocation to request exit");
646 assert_eq!(help_code, 0);
647 assert!(
648 help_msg.contains("Usage") || help_msg.contains("USAGE"),
649 "help output should contain usage text, got: {help_msg}"
650 );
651
652 let version_args = vec!["clementine-core", "--version"];
654 let version_result = parse_cli_args(version_args);
655 let (version_code, version_msg) =
656 version_result.expect_err("Expected version invocation to request exit");
657 assert_eq!(version_code, 0);
658 assert!(
660 !version_msg.trim().is_empty(),
661 "version output should not be empty"
662 );
663 }
664
665 #[test]
666 fn test_parse_cli_args_invalid_args_exit_code_one() {
667 let args = vec!["clementine-core", "--unknown-flag"];
669
670 let result = parse_cli_args(args);
671 let (code, msg) = result.expect_err("Expected invalid arguments to cause an error");
672
673 assert_eq!(code, 1);
674 assert!(
675 msg.contains("Failed to parse CLI arguments"),
676 "error message should include prefix, got: {msg}"
677 );
678 assert!(
679 msg.contains("unexpected") || msg.contains("Unknown") || msg.contains("unknown"),
680 "underlying clap error should be included in message, got: {msg}"
681 );
682 }
683}