use crate::config::protocol::ProtocolParamset;
use crate::config::BridgeConfig;
use crate::errors::BridgeError;
use crate::errors::ErrorExt;
use crate::utils;
use crate::utils::delayed_panic;
use clap::Parser;
use clap::ValueEnum;
use eyre::Context;
use std::env;
use std::ffi::OsString;
use std::path::PathBuf;
use std::process;
use std::str::FromStr;
use tracing::level_filters::LevelFilter;
use tracing::Level;
#[derive(Debug, Clone, Copy, ValueEnum, Eq, PartialEq)]
pub enum Actors {
Verifier,
Operator,
Aggregator,
}
#[derive(Parser, Debug, Clone)]
#[command(version, about, long_about = None)]
pub struct Args {
pub actor: Actors,
#[arg(short, long)]
pub config: Option<PathBuf>,
#[arg(short, long)]
pub protocol_params: Option<PathBuf>,
#[arg(short, long, default_value_t = 3)]
pub verbose: u8,
}
fn parse_from<I, T>(itr: I) -> Result<Args, BridgeError>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
match Args::try_parse_from(itr) {
Ok(c) => Ok(c),
Err(e)
if matches!(
e.kind(),
clap::error::ErrorKind::DisplayHelp
| clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
| clap::error::ErrorKind::DisplayVersion
) =>
{
Err(BridgeError::CLIDisplayAndExit(e.render()))
}
Err(e) => Err(BridgeError::ConfigError(e.to_string())),
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ConfigSource {
File(PathBuf),
Env,
}
pub fn get_config_source(
read_from_env_name: &'static str,
provided_arg: Option<PathBuf>,
) -> Result<ConfigSource, BridgeError> {
Ok(match std::env::var(read_from_env_name) {
Err(_) => ConfigSource::File(provided_arg.ok_or(BridgeError::ConfigError(
"No file path or environment variable provided for config file.".to_string(),
))?),
Ok(str) if str == "0" || str == "off" => ConfigSource::File(provided_arg.ok_or(
BridgeError::ConfigError("No file path provided for config file.".to_string()),
)?),
Ok(str) => {
if str != "1" && str != "on" {
tracing::warn!("Unknown value for {read_from_env_name}: {str}. Expected 1/0/off/on. Defaulting to environment variables.");
}
if provided_arg.is_some() {
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.");
}
ConfigSource::Env
}
})
}
pub fn get_cli_config() -> (BridgeConfig, Args) {
let args = env::args();
match get_cli_config_from_args(args) {
Ok(config) => config,
Err(e) => {
let e = e.into_eyre();
match e.root_cause().downcast_ref::<BridgeError>() {
Some(BridgeError::CLIDisplayAndExit(msg)) => {
println!("{}", msg);
process::exit(0);
}
_ => delayed_panic!("Failed to get CLI config: {e:?}"),
}
}
}
}
fn get_cli_config_from_args<I, T>(itr: I) -> Result<(BridgeConfig, Args), BridgeError>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let args = parse_from(itr).wrap_err("Failed to parse CLI arguments.")?;
let level_filter = match args.verbose {
0 => None,
other => Some(LevelFilter::from_level(
Level::from_str(&other.to_string()).unwrap_or(Level::INFO),
)),
};
utils::initialize_logger(level_filter).wrap_err("Failed to initialize logger.")?;
let config_source = get_config_source("READ_CONFIG_FROM_ENV", args.config.clone());
let mut config =
match config_source.wrap_err("Failed to determine source for configuration.")? {
ConfigSource::File(config_file) => {
BridgeConfig::try_parse_file(config_file)
.wrap_err("Failed to read configuration from file.")?
}
ConfigSource::Env => BridgeConfig::from_env()
.wrap_err("Failed to read configuration from environment variables.")?,
};
let protocol_params_source =
get_config_source("READ_PARAMSET_FROM_ENV", args.protocol_params.clone())
.wrap_err("Failed to determine source for protocol parameters.")?;
let paramset: &'static ProtocolParamset = Box::leak(Box::new(match protocol_params_source {
ConfigSource::File(path) => ProtocolParamset::from_toml_file(path.as_path())
.wrap_err("Failed to read protocol parameters from file.")?,
ConfigSource::Env => ProtocolParamset::from_env()
.wrap_err("Failed to read protocol parameters from environment.")?,
}));
config.protocol_paramset = paramset;
Ok((config, args))
}
#[cfg(test)]
mod tests {
use super::{get_cli_config_from_args, get_config_source, parse_from, ConfigSource};
use crate::cli::Actors;
use crate::errors::BridgeError;
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
#[test]
fn help_message() {
match parse_from(vec!["clementine-core", "--help"]) {
Ok(_) => panic!("expected configuration error"),
Err(BridgeError::CLIDisplayAndExit(_)) => {}
e => panic!("unexpected error {e:#?}"),
}
}
#[test]
fn version() {
match parse_from(vec!["clementine-core", "--version"]) {
Ok(_) => panic!("expected configuration error"),
Err(BridgeError::CLIDisplayAndExit(_)) => {}
e => panic!("unexpected error {e:#?}"),
}
}
fn with_env_var<F, T>(name: &str, value: Option<&str>, test: F) -> T
where
F: FnOnce() -> T,
{
let prev_value = env::var(name).ok();
match value {
Some(val) => env::set_var(name, val),
None => env::remove_var(name),
}
let result = test();
match prev_value {
Some(val) => env::set_var(name, val),
None => env::remove_var(name),
}
result
}
#[test]
#[serial_test::serial]
fn test_get_config_source_env_not_set() {
with_env_var("TEST_READ_FROM_ENV", None, || {
let path = PathBuf::from("/path/to/config");
let result = get_config_source("TEST_READ_FROM_ENV", Some(path.clone()));
assert_eq!(result.unwrap(), ConfigSource::File(path));
let result = get_config_source("TEST_READ_FROM_ENV", None);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), BridgeError::ConfigError(_)));
})
}
#[test]
#[serial_test::serial]
fn test_get_config_source_env_set_to_off() {
with_env_var("TEST_READ_FROM_ENV", Some("0"), || {
let path = PathBuf::from("/path/to/config");
let result = get_config_source("TEST_READ_FROM_ENV", Some(path.clone()));
assert_eq!(result.unwrap(), ConfigSource::File(path));
let result = get_config_source("TEST_READ_FROM_ENV", None);
assert!(result.is_err());
});
with_env_var("TEST_READ_FROM_ENV", Some("off"), || {
let path = PathBuf::from("/path/to/config");
let result = get_config_source("TEST_READ_FROM_ENV", Some(path.clone()));
assert_eq!(result.unwrap(), ConfigSource::File(path));
})
}
#[test]
#[serial_test::serial]
fn test_get_config_source_env_set_to_on() {
with_env_var("TEST_READ_FROM_ENV", Some("1"), || {
let result = get_config_source("TEST_READ_FROM_ENV", None);
assert_eq!(result.unwrap(), ConfigSource::Env);
let path = PathBuf::from("/path/to/config");
let result = get_config_source("TEST_READ_FROM_ENV", Some(path));
assert_eq!(result.unwrap(), ConfigSource::Env);
});
with_env_var("TEST_READ_FROM_ENV", Some("on"), || {
let result = get_config_source("TEST_READ_FROM_ENV", None);
assert_eq!(result.unwrap(), ConfigSource::Env);
})
}
#[test]
#[serial_test::serial]
fn test_get_config_source_env_unknown_value() {
with_env_var("TEST_READ_FROM_ENV", Some("invalid"), || {
let result = get_config_source("TEST_READ_FROM_ENV", None);
assert_eq!(result.unwrap(), ConfigSource::Env);
})
}
fn with_temp_config_file<F, T>(content: &str, test: F) -> T
where
F: FnOnce(PathBuf) -> T,
{
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("bridge_config.toml");
let mut file = File::create(&file_path).unwrap();
file.write_all(content.as_bytes()).unwrap();
let result = test(file_path);
temp_dir.close().unwrap();
result
}
fn setup_config_env_vars() {
env::set_var("HOST", "127.0.0.1");
env::set_var("PORT", "17000");
env::set_var(
"SECRET_KEY",
"1111111111111111111111111111111111111111111111111111111111111111",
);
env::set_var("OPERATOR_WITHDRAWAL_FEE_SATS", "100000");
env::set_var("BITCOIN_RPC_URL", "http://127.0.0.1:18443/wallet/admin");
env::set_var("BITCOIN_RPC_USER", "admin");
env::set_var("BITCOIN_RPC_PASSWORD", "admin");
env::set_var("DB_HOST", "127.0.0.1");
env::set_var("DB_PORT", "5432");
env::set_var("DB_USER", "clementine");
env::set_var("DB_PASSWORD", "clementine");
env::set_var("DB_NAME", "clementine");
env::set_var("CITREA_RPC_URL", "");
env::set_var("CITREA_LIGHT_CLIENT_PROVER_URL", "");
env::set_var("CITREA_CHAIN_ID", "5655");
env::set_var(
"BRIDGE_CONTRACT_ADDRESS",
"3100000000000000000000000000000000000002",
);
env::set_var("SERVER_CERT_PATH", "certs/server/server.pem");
env::set_var("SERVER_KEY_PATH", "certs/server/server.key");
env::set_var("CA_CERT_PATH", "certs/ca/ca.pem");
env::set_var("CLIENT_CERT_PATH", "certs/client/client.pem");
env::set_var("CLIENT_KEY_PATH", "certs/client/client.key");
env::set_var("AGGREGATOR_CERT_PATH", "certs/aggregator/aggregator.pem");
env::set_var("CLIENT_VERIFICATION", "true");
env::set_var(
"SECURITY_COUNCIL",
"1:50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
);
env::set_var(
"BRIDGE_CIRCUIT_METHOD_ID_CONSTANT",
"5f1c8bf89505f4f0f29081628a5391819eea9b6b4c28a8d11a8c44415963b27c",
);
}
fn setup_protocol_paramset_env_vars() {
env::set_var("NETWORK", "regtest");
env::set_var("NUM_ROUND_TXS", "2");
env::set_var("NUM_KICKOFFS_PER_ROUND", "10");
env::set_var("NUM_SIGNED_KICKOFFS", "2");
env::set_var("BRIDGE_AMOUNT", "1000000000");
env::set_var("KICKOFF_AMOUNT", "0");
env::set_var("OPERATOR_CHALLENGE_AMOUNT", "200000000");
env::set_var("COLLATERAL_FUNDING_AMOUNT", "99000000");
env::set_var("KICKOFF_BLOCKHASH_COMMIT_LENGTH", "40");
env::set_var("WATCHTOWER_CHALLENGE_BYTES", "144");
env::set_var("WINTERNITZ_LOG_D", "4");
env::set_var("USER_TAKES_AFTER", "200");
env::set_var("OPERATOR_CHALLENGE_TIMEOUT_TIMELOCK", "144");
env::set_var("OPERATOR_CHALLENGE_NACK_TIMELOCK", "432");
env::set_var("DISPROVE_TIMEOUT_TIMELOCK", "720");
env::set_var("ASSERT_TIMEOUT_TIMELOCK", "576");
env::set_var("OPERATOR_REIMBURSE_TIMELOCK", "12");
env::set_var("WATCHTOWER_CHALLENGE_TIMEOUT_TIMELOCK", "288");
env::set_var("TIME_TO_SEND_WATCHTOWER_CHALLENGE", "216");
env::set_var("LATEST_BLOCKHASH_TIMEOUT_TIMELOCK", "360");
env::set_var("FINALITY_DEPTH", "1");
env::set_var("START_HEIGHT", "8148");
env::set_var("GENESIS_HEIGHT", "0");
env::set_var(
"GENESIS_CHAIN_STATE_HASH",
"5f7302ad16c8bd9ef2f3be00c8199a86f9e0ba861484abb4af5f7e457f8c2216",
);
env::set_var("HEADER_CHAIN_PROOF_BATCH_SIZE", "100");
env::set_var(
"BRIDGE_CIRCUIT_METHOD_ID_CONSTANT",
"5f1c8bf89505f4f0f29081628a5391819eea9b6b4c28a8d11a8c44415963b27c",
);
env::set_var("BRIDGE_NONSTANDARD", "true");
}
fn cleanup_config_env_vars() {
env::remove_var("HOST");
env::remove_var("PORT");
env::remove_var("SECRET_KEY");
env::remove_var("OPERATOR_WITHDRAWAL_FEE_SATS");
env::remove_var("BITCOIN_RPC_URL");
env::remove_var("BITCOIN_RPC_USER");
env::remove_var("BITCOIN_RPC_PASSWORD");
env::remove_var("DB_HOST");
env::remove_var("DB_PORT");
env::remove_var("DB_USER");
env::remove_var("DB_PASSWORD");
env::remove_var("DB_NAME");
env::remove_var("CITREA_RPC_URL");
env::remove_var("CITREA_LIGHT_CLIENT_PROVER_URL");
env::remove_var("BRIDGE_CONTRACT_ADDRESS");
env::remove_var("SERVER_CERT_PATH");
env::remove_var("SERVER_KEY_PATH");
env::remove_var("CA_CERT_PATH");
env::remove_var("CLIENT_CERT_PATH");
env::remove_var("CLIENT_KEY_PATH");
env::remove_var("AGGREGATOR_CERT_PATH");
env::remove_var("CLIENT_VERIFICATION");
env::remove_var("SECURITY_COUNCIL");
}
fn cleanup_protocol_paramset_env_vars() {
env::remove_var("NETWORK");
env::remove_var("NUM_ROUND_TXS");
env::remove_var("NUM_KICKOFFS_PER_ROUND");
env::remove_var("NUM_SIGNED_KICKOFFS");
env::remove_var("BRIDGE_AMOUNT");
env::remove_var("KICKOFF_AMOUNT");
env::remove_var("OPERATOR_CHALLENGE_AMOUNT");
env::remove_var("COLLATERAL_FUNDING_AMOUNT");
env::remove_var("KICKOFF_BLOCKHASH_COMMIT_LENGTH");
env::remove_var("WATCHTOWER_CHALLENGE_BYTES");
env::remove_var("WINTERNITZ_LOG_D");
env::remove_var("USER_TAKES_AFTER");
env::remove_var("OPERATOR_CHALLENGE_TIMEOUT_TIMELOCK");
env::remove_var("OPERATOR_CHALLENGE_NACK_TIMELOCK");
env::remove_var("DISPROVE_TIMEOUT_TIMELOCK");
env::remove_var("ASSERT_TIMEOUT_TIMELOCK");
env::remove_var("OPERATOR_REIMBURSE_TIMELOCK");
env::remove_var("WATCHTOWER_CHALLENGE_TIMEOUT_TIMELOCK");
env::remove_var("TIME_TO_SEND_WATCHTOWER_CHALLENGE");
env::remove_var("FINALITY_DEPTH");
env::remove_var("START_HEIGHT");
env::remove_var("HEADER_CHAIN_PROOF_BATCH_SIZE");
}
const MINIMAL_CONFIG_CONTENT: &str = include_str!("test/data/bridge_config.toml");
#[test]
#[serial_test::serial]
fn test_get_cli_config_file_mode() {
with_env_var("READ_CONFIG_FROM_ENV", Some("0"), || {
with_temp_config_file(MINIMAL_CONFIG_CONTENT, |config_path| {
with_temp_config_file(
include_str!("./test/data/protocol_paramset.toml"),
|protocol_path| {
let args = vec![
"clementine-core",
"verifier",
"--config",
config_path.to_str().unwrap(),
"--protocol-params",
protocol_path.to_str().unwrap(),
];
let result = get_cli_config_from_args(args);
let (config, cli_args) = result.expect("Failed to get CLI config");
assert_eq!(config.host, "127.0.0.1");
assert_eq!(config.port, 17000);
assert_eq!(cli_args.actor, Actors::Verifier);
assert_eq!(config.protocol_paramset.network.to_string(), "regtest");
assert_eq!(config.protocol_paramset.num_round_txs, 2);
assert_eq!(config.protocol_paramset.winternitz_log_d, 4);
},
)
})
})
}
#[test]
#[serial_test::serial]
fn test_get_cli_config_env_mode() {
setup_config_env_vars();
setup_protocol_paramset_env_vars();
with_env_var("READ_CONFIG_FROM_ENV", Some("1"), || {
with_env_var("READ_PARAMSET_FROM_ENV", Some("1"), || {
let args = vec!["clementine-core", "operator"];
let result = get_cli_config_from_args(args);
let (config, cli_args) = result.expect("Failed to get CLI config");
assert_eq!(config.host, "127.0.0.1");
assert_eq!(config.port, 17000);
assert_eq!(cli_args.actor, Actors::Operator);
assert_eq!(config.protocol_paramset.network.to_string(), "regtest");
assert_eq!(config.protocol_paramset.num_round_txs, 2);
assert_eq!(config.protocol_paramset.winternitz_log_d, 4);
assert_eq!(config.protocol_paramset.start_height, 8148); });
});
cleanup_config_env_vars();
cleanup_protocol_paramset_env_vars();
}
#[test]
#[serial_test::serial]
fn test_mixed_config_sources() {
setup_protocol_paramset_env_vars();
with_env_var("READ_CONFIG_FROM_ENV", Some("0"), || {
with_env_var("READ_PARAMSET_FROM_ENV", Some("1"), || {
with_temp_config_file(MINIMAL_CONFIG_CONTENT, |config_path| {
let args = vec![
"clementine-core",
"verifier",
"--config",
config_path.to_str().unwrap(),
];
let result = get_cli_config_from_args(args);
let (config, cli_args) = result.expect("Failed to get CLI config");
assert_eq!(config.host, "127.0.0.1");
assert_eq!(config.port, 17000);
assert_eq!(cli_args.actor, Actors::Verifier);
assert_eq!(config.protocol_paramset.network.to_string(), "regtest");
assert_eq!(config.protocol_paramset.start_height, 8148); })
})
});
cleanup_protocol_paramset_env_vars();
}
#[test]
#[serial_test::serial]
fn test_get_cli_config_file_without_path() {
with_env_var("READ_CONFIG_FROM_ENV", Some("0"), || {
let args = vec!["clementine-core", "verifier"];
let result = get_cli_config_from_args(args);
result.expect_err("Expected error when config file path is not provided");
})
}
}