use crate::cli::ValidatorClient; use beacon_node_fallback::ApiTopic; use beacon_node_fallback::beacon_node_health::BeaconNodeSyncDistanceTiers; use clap::ArgMatches; use clap_utils::{flags::DISABLE_MALLOC_TUNING_FLAG, parse_required}; use directory::{ DEFAULT_HARDCODED_NETWORK, DEFAULT_ROOT_DIR, DEFAULT_SECRET_DIR, DEFAULT_VALIDATOR_DIR, get_network_dir, }; use eth2::types::Graffiti; use graffiti_file::GraffitiFile; use initialized_validators::Config as InitializedValidatorsConfig; use lighthouse_validator_store::Config as ValidatorStoreConfig; use sensitive_url::SensitiveUrl; use serde::{Deserialize, Serialize}; use std::fs; use std::net::IpAddr; use std::path::PathBuf; use std::time::Duration; use tracing::{info, warn}; use types::GRAFFITI_BYTES_LEN; use validator_http_api::{self, PK_FILENAME}; use validator_http_metrics; pub const DEFAULT_BEACON_NODE: &str = "http://localhost:5052/"; /// Stores the core configuration for this validator instance. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Config { /// Configuration parameters for the validator store. #[serde(flatten)] pub validator_store: ValidatorStoreConfig, /// The data directory, which stores all validator databases pub validator_dir: PathBuf, /// The directory containing the passwords to unlock validator keystores. pub secrets_dir: PathBuf, /// The http endpoints of the beacon node APIs. /// /// Should be similar to `["http://localhost:8080"]` pub beacon_nodes: Vec, /// An optional beacon node used for block proposals only. pub proposer_nodes: Vec, /// If true, the validator client will still poll for duties and produce blocks even if the /// beacon node is not synced at startup. pub allow_unsynced_beacon_node: bool, /// If true, don't scan the validators dir for new keystores. pub disable_auto_discover: bool, /// If true, re-register existing validators in definitions.yml for slashing protection. pub init_slashing_protection: bool, /// If true, use longer timeouts for requests made to the beacon node. pub use_long_timeouts: bool, /// Multiplier to use for long timeouts. pub long_timeouts_multiplier: u32, /// Graffiti to be inserted everytime we create a block. pub graffiti: Option, /// Graffiti file to load per validator graffitis. pub graffiti_file: Option, /// Configuration for the HTTP REST API. pub http_api: validator_http_api::Config, /// Configuration for the HTTP REST API. pub http_metrics: validator_http_metrics::Config, /// Configuration for the Beacon Node fallback. pub beacon_node_fallback: beacon_node_fallback::Config, /// Configuration for sending metrics to a remote explorer endpoint. pub monitoring_api: Option, /// If true, enable functionality that monitors the network for attestations or proposals from /// any of the validators managed by this client before starting up. pub enable_doppelganger_protection: bool, /// If true, then we publish validator specific metrics (e.g next attestation duty slot) /// for all our managed validators. /// Note: We publish validator specific metrics for low validator counts without this flag /// (<= 64 validators) pub enable_high_validator_count_metrics: bool, /// Enable use of the blinded block endpoints during proposals. pub builder_registration_timestamp_override: Option, /// A list of custom certificates that the validator client will additionally use when /// connecting to a beacon node over SSL/TLS. pub beacon_nodes_tls_certs: Option>, /// Enables broadcasting of various requests (by topic) to all beacon nodes. pub broadcast_topics: Vec, /// Enables a service which attempts to measure latency between the VC and BNs. pub enable_latency_measurement_service: bool, /// Defines the number of validators per `validator/register_validator` request sent to the BN. pub validator_registration_batch_size: usize, /// Whether we are running with distributed network support. pub distributed: bool, /// Configuration for the initialized validators #[serde(flatten)] pub initialized_validators: InitializedValidatorsConfig, pub disable_attesting: bool, } impl Default for Config { /// Build a new configuration from defaults. fn default() -> Self { // WARNING: these directory defaults should be always overwritten with parameters from cli // for specific networks. let base_dir = dirs::home_dir() .unwrap_or_else(|| PathBuf::from(".")) .join(DEFAULT_ROOT_DIR) .join(DEFAULT_HARDCODED_NETWORK); let validator_dir = base_dir.join(DEFAULT_VALIDATOR_DIR); let secrets_dir = base_dir.join(DEFAULT_SECRET_DIR); let beacon_nodes = vec![ SensitiveUrl::parse(DEFAULT_BEACON_NODE) .expect("beacon_nodes must always be a valid url."), ]; Self { validator_store: ValidatorStoreConfig::default(), validator_dir, secrets_dir, beacon_nodes, proposer_nodes: Vec::new(), allow_unsynced_beacon_node: false, disable_auto_discover: false, init_slashing_protection: false, use_long_timeouts: false, long_timeouts_multiplier: 1, graffiti: None, graffiti_file: None, http_api: <_>::default(), http_metrics: <_>::default(), beacon_node_fallback: <_>::default(), monitoring_api: None, enable_doppelganger_protection: false, enable_high_validator_count_metrics: false, beacon_nodes_tls_certs: None, builder_registration_timestamp_override: None, broadcast_topics: vec![ApiTopic::Subscriptions], enable_latency_measurement_service: true, validator_registration_batch_size: 500, distributed: false, initialized_validators: <_>::default(), disable_attesting: false, } } } impl Config { /// Returns a `Default` implementation of `Self` with some parameters modified by the supplied /// `cli_args`. pub fn from_cli( cli_args: &ArgMatches, validator_client_config: &ValidatorClient, ) -> Result { let mut config = Config::default(); let default_root_dir = dirs::home_dir() .map(|home| home.join(DEFAULT_ROOT_DIR)) .unwrap_or_else(|| PathBuf::from(".")); let (mut validator_dir, mut secrets_dir) = (None, None); if cli_args.get_one::("datadir").is_some() { let base_dir: PathBuf = parse_required(cli_args, "datadir")?; validator_dir = Some(base_dir.join(DEFAULT_VALIDATOR_DIR)); secrets_dir = Some(base_dir.join(DEFAULT_SECRET_DIR)); } if let Some(validator_dir_path) = validator_client_config.validators_dir.as_ref() { validator_dir = Some(validator_dir_path.clone()); } if let Some(secrets_dir_path) = validator_client_config.secrets_dir.as_ref() { secrets_dir = Some(secrets_dir_path.clone()); } config.validator_dir = validator_dir.unwrap_or_else(|| { default_root_dir .join(get_network_dir(cli_args)) .join(DEFAULT_VALIDATOR_DIR) }); config.secrets_dir = secrets_dir.unwrap_or_else(|| { default_root_dir .join(get_network_dir(cli_args)) .join(DEFAULT_SECRET_DIR) }); if !config.validator_dir.exists() { fs::create_dir_all(&config.validator_dir) .map_err(|e| format!("Failed to create {:?}: {:?}", config.validator_dir, e))?; } if let Some(beacon_nodes) = validator_client_config.beacon_nodes.as_ref() { config.beacon_nodes = beacon_nodes .iter() .map(|s| SensitiveUrl::parse(s)) .collect::>() .map_err(|e| format!("Unable to parse beacon node URL: {:?}", e))?; } if let Some(proposer_nodes) = validator_client_config.proposer_nodes.as_ref() { config.proposer_nodes = proposer_nodes .iter() .map(|s| SensitiveUrl::parse(s)) .collect::>() .map_err(|e| format!("Unable to parse proposer node URL: {:?}", e))?; } config.disable_auto_discover = validator_client_config.disable_auto_discover; config.init_slashing_protection = validator_client_config.init_slashing_protection; config.use_long_timeouts = validator_client_config.use_long_timeouts; config.long_timeouts_multiplier = validator_client_config.long_timeouts_multiplier; if let Some(graffiti_file_path) = validator_client_config.graffiti_file.as_ref() { let mut graffiti_file = GraffitiFile::new(graffiti_file_path.into()); graffiti_file .read_graffiti_file() .map_err(|e| format!("Error reading graffiti file: {:?}", e))?; config.graffiti_file = Some(graffiti_file); info!( path = graffiti_file_path.to_str(), "Successfully loaded graffiti file" ); } if let Some(input_graffiti) = validator_client_config.graffiti.as_ref() { let graffiti_bytes = input_graffiti.as_bytes(); if graffiti_bytes.len() > GRAFFITI_BYTES_LEN { return Err(format!( "Your graffiti is too long! {} bytes maximum!", GRAFFITI_BYTES_LEN )); } else { let mut graffiti = [0; 32]; // Copy the provided bytes over. // // Panic-free because `graffiti_bytes.len()` <= `GRAFFITI_BYTES_LEN`. graffiti[..graffiti_bytes.len()].copy_from_slice(graffiti_bytes); config.graffiti = Some(graffiti.into()); } } if let Some(input_fee_recipient) = validator_client_config.suggested_fee_recipient { config.validator_store.fee_recipient = Some(input_fee_recipient); } if let Some(tls_certs) = validator_client_config.beacon_nodes_tls_certs.as_ref() { config.beacon_nodes_tls_certs = Some(tls_certs.iter().map(PathBuf::from).collect()); } config.distributed = validator_client_config.distributed; if let Some(mut broadcast_topics) = validator_client_config.broadcast.clone() { broadcast_topics.retain(|topic| *topic != ApiTopic::None); config.broadcast_topics = broadcast_topics; } /* * Beacon node fallback */ config.beacon_node_fallback.sync_tolerances = BeaconNodeSyncDistanceTiers::from_vec( &validator_client_config.beacon_nodes_sync_tolerances, )?; /* * Web3 signer */ if validator_client_config.web3_signer_keep_alive_timeout == 0 { config.initialized_validators.web3_signer_keep_alive_timeout = None } else { config.initialized_validators.web3_signer_keep_alive_timeout = Some( Duration::from_millis(validator_client_config.web3_signer_keep_alive_timeout), ); } if let Some(n) = validator_client_config.web3_signer_max_idle_connections { config .initialized_validators .web3_signer_max_idle_connections = Some(n); } /* * Http API server */ config.http_api.enabled = validator_client_config.http; if let Some(address) = &validator_client_config.http_address { if validator_client_config.unencrypted_http_transport { config.http_api.listen_addr = address .parse::() .map_err(|_| "http-address is not a valid IP address.")?; } else { return Err( "While using `--http-address`, you must also use `--unencrypted-http-transport`." .to_string(), ); } } config.http_api.listen_port = validator_client_config.http_port; if let Some(allow_origin) = validator_client_config.http_allow_origin.as_ref() { // Pre-validate the config value to give feedback to the user on node startup, instead of // as late as when the first API response is produced. hyper::header::HeaderValue::from_str(allow_origin) .map_err(|_| "Invalid allow-origin value")?; config.http_api.allow_origin = Some(allow_origin.to_string()); } config.http_api.allow_keystore_export = validator_client_config.http_allow_keystore_export; config.http_api.store_passwords_in_secrets_dir = validator_client_config.http_store_passwords_in_secrets_dir; if let Some(http_token_path) = &validator_client_config.http_token_path { config.http_api.http_token_path = PathBuf::from(http_token_path); } else { // For backward compatibility, default to the path under the validator dir if not provided. config.http_api.http_token_path = config.validator_dir.join(PK_FILENAME); } /* * Prometheus metrics HTTP server */ config.http_metrics.enabled = validator_client_config.metrics; config.enable_high_validator_count_metrics = validator_client_config.enable_high_validator_count_metrics; if let Some(metrics_address) = &validator_client_config.metrics_address { config.http_metrics.listen_addr = metrics_address .parse::() .map_err(|_| "metrics-address is not a valid IP address.")?; } config.http_metrics.listen_port = validator_client_config.metrics_port; if let Some(allow_origin) = validator_client_config.metrics_allow_origin.as_ref() { // Pre-validate the config value to give feedback to the user on node startup, instead of // as late as when the first API response is produced. hyper::header::HeaderValue::from_str(allow_origin) .map_err(|_| "Invalid allow-origin value")?; config.http_metrics.allow_origin = Some(allow_origin.to_string()); } if cli_args.get_flag(DISABLE_MALLOC_TUNING_FLAG) { config.http_metrics.allocator_metrics_enabled = false; } /* * Explorer metrics */ if let Some(monitoring_endpoint) = validator_client_config.monitoring_endpoint.as_ref() { let update_period_secs = Some(validator_client_config.monitoring_endpoint_period); config.monitoring_api = Some(monitoring_api::Config { db_path: None, freezer_db_path: None, update_period_secs, monitoring_endpoint: monitoring_endpoint.to_string(), }); } config.enable_doppelganger_protection = validator_client_config.enable_doppelganger_protection; config.validator_store.builder_proposals = validator_client_config.builder_proposals; config.validator_store.prefer_builder_proposals = validator_client_config.prefer_builder_proposals; config.validator_store.gas_limit = Some(validator_client_config.gas_limit); config.builder_registration_timestamp_override = validator_client_config.builder_registration_timestamp_override; config.validator_store.builder_boost_factor = validator_client_config.builder_boost_factor; config.enable_latency_measurement_service = !validator_client_config.disable_latency_measurement_service; config.validator_registration_batch_size = validator_client_config.validator_registration_batch_size; if config.validator_registration_batch_size == 0 { return Err("validator-registration-batch-size cannot be 0".to_string()); } config.validator_store.enable_web3signer_slashing_protection = if validator_client_config.disable_slashing_protection_web3signer { warn!( info = "ensure slashing protection on web3signer is enabled or you WILL \ get slashed", "Slashing protection for remote keys disabled" ); false } else { true }; config.disable_attesting = validator_client_config.disable_attesting; Ok(config) } } #[cfg(test)] mod tests { use super::*; #[test] // Ensures the default config does not panic. fn default_config() { Config::default(); } }