Files
lighthouse/validator_client/src/config.rs
Eitan Seri-Levi 477c25db9f Fix race condition between validator duties service and proposer preferences (#9309)
The proposer preferences service was attempting to publish preferences at the start of each epoch. This caused it to race with the validator duties service, it wouldn't calculate validator duties in time for the proposer preference service.

This PR first updates the validator duties service to calculate proposer duties for the current epoch and the next epoch. After Fulu we have the ability to look ahead one epoch for proposer duties, but we never updated the vc to leverage this feature.

This PR also updates the proposer preferences service to fire at every slot. We have an `(Epoch, DependentRoot)` map that prevents us from publishing the same preferences twice.

The changes here should prevent the race condition between the two services and make the proposer preferences service more robust in general.


  


Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com>

Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu>

Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com>
2026-06-20 19:41:30 +00:00

424 lines
18 KiB
Rust

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, GraffitiPolicy};
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<SensitiveUrl>,
/// An optional beacon node used for block proposals only.
pub proposer_nodes: Vec<SensitiveUrl>,
/// 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>,
/// Graffiti file to load per validator graffitis.
pub graffiti_file: Option<GraffitiFile>,
/// GraffitiPolicy to append client version info
pub graffiti_policy: Option<GraffitiPolicy>,
/// 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<monitoring_api::Config>,
/// 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<u64>,
/// 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<Vec<PathBuf>>,
/// Enables broadcasting of various requests (by topic) to all beacon nodes.
pub broadcast_topics: Vec<ApiTopic>,
/// Enables a service which attempts to measure latency between the VC and BNs.
pub enable_latency_measurement_service: bool,
/// Enables the beacon head monitor that reacts to head updates from connected beacon nodes.
pub enable_beacon_head_monitor: 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,
/// Fetch proposer duties using the v1 endpoint instead of v2.
pub disable_proposer_duties_v2: 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,
graffiti_policy: 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,
enable_beacon_head_monitor: true,
validator_registration_batch_size: 500,
distributed: false,
initialized_validators: <_>::default(),
disable_attesting: false,
disable_proposer_duties_v2: 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<Config, String> {
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::<String>("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::<Result<_, _>>()
.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::<Result<_, _>>()
.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());
}
}
config.graffiti_policy = if validator_client_config.graffiti_append.unwrap_or(true) {
Some(GraffitiPolicy::AppendClientVersions)
} else {
Some(GraffitiPolicy::PreserveUserGraffiti)
};
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::<IpAddr>()
.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::<IpAddr>()
.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.enable_beacon_head_monitor = !validator_client_config.disable_beacon_head_monitor;
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;
config.disable_proposer_duties_v2 = validator_client_config.disable_proposer_duties_v2;
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
// Ensures the default config does not panic.
fn default_config() {
Config::default();
}
}