Files
lighthouse/beacon_node/src/lib.rs
Jimmy Chen af1d9b9991 Fix custody context initialization race condition that caused panic (#8391)
Take 2 of #8390.

Fixes the race condition properly instead of propagating the error. I think this is a better alternative, and doesn't seem to look that bad.


  * Lift node id loading or generation from `NetworkService ` startup to the `ClientBuilder`, so that it can be used to compute custody columns for the beacon chain without waiting for Network bootstrap.

I've considered and implemented a few alternatives:
1. passing `node_id` to beacon chain builder and compute columns when creating `CustodyContext`. This approach isn't good for separation of concerns and isn't great for testability
2. passing `ordered_custody_groups` to beacon chain. `CustodyContext` only uses this to compute ordered custody columns, so we might as well lift this logic out, so we don't have to do error handling in `CustodyContext` construction. Less tests to update;.


Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com>
2025-11-17 05:23:12 +00:00

222 lines
8.0 KiB
Rust

mod cli;
mod config;
pub use beacon_chain;
use beacon_chain::{builder::Witness, slot_clock::SystemTimeSlotClock};
use clap::ArgMatches;
pub use cli::cli_app;
pub use client::{Client, ClientBuilder, ClientConfig, ClientGenesis};
pub use config::{get_config, get_data_dir, set_network_config};
use environment::RuntimeContext;
pub use eth2_config::Eth2Config;
use lighthouse_network::load_private_key;
use network_utils::enr_ext::peer_id_to_node_id;
use slasher::{DatabaseBackendOverride, Slasher};
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use store::database::interface::BeaconNodeBackend;
use tracing::{info, warn};
use types::{ChainSpec, Epoch, EthSpec, ForkName};
/// A type-alias to the tighten the definition of a production-intended `Client`.
pub type ProductionClient<E> =
Client<Witness<SystemTimeSlotClock, E, BeaconNodeBackend<E>, BeaconNodeBackend<E>>>;
/// The beacon node `Client` that will be used in production.
///
/// Generic over some `EthSpec`.
///
/// ## Notes:
///
/// Despite being titled `Production...`, this code is not ready for production. The name
/// demonstrates an intention, not a promise.
pub struct ProductionBeaconNode<E: EthSpec>(ProductionClient<E>);
impl<E: EthSpec> ProductionBeaconNode<E> {
/// Starts a new beacon node `Client` in the given `environment`.
///
/// Identical to `start_from_client_config`, however the `client_config` is generated from the
/// given `matches` and potentially configuration files on the local filesystem or other
/// configurations hosted remotely.
pub async fn new_from_cli(
context: RuntimeContext<E>,
matches: ArgMatches,
) -> Result<Self, String> {
let client_config = get_config::<E>(&matches, &context)?;
Self::new(context, client_config).await
}
/// Starts a new beacon node `Client` in the given `environment`.
///
/// Client behaviour is defined by the given `client_config`.
pub async fn new(
context: RuntimeContext<E>,
mut client_config: ClientConfig,
) -> Result<Self, String> {
let spec = context.eth2_config().spec.clone();
let client_genesis = client_config.genesis.clone();
let store_config = client_config.store.clone();
let _datadir = client_config.create_data_dir()?;
let db_path = client_config.create_db_path()?;
let freezer_db_path = client_config.create_freezer_db_path()?;
let blobs_db_path = client_config.create_blobs_db_path()?;
let executor = context.executor.clone();
if let Some(legacy_dir) = client_config.get_existing_legacy_data_dir() {
warn!(
msg = "this occurs when using relative paths for a datadir location",
location = ?legacy_dir,
"Legacy datadir location"
)
}
if let Err(misaligned_forks) = validator_fork_epochs(&spec) {
warn!(
info = "This may cause issues as fork boundaries do not align with the \
start of sync committee period.",
?misaligned_forks,
"Fork boundaries are not well aligned / multiples of 256"
);
}
let builder = ClientBuilder::new(context.eth_spec_instance.clone())
.runtime_context(context)
.chain_spec(spec.clone())
.beacon_processor(client_config.beacon_processor.clone())
.http_api_config(client_config.http_api.clone())
.disk_store(&db_path, &freezer_db_path, &blobs_db_path, store_config)?;
let builder = if let Some(mut slasher_config) = client_config.slasher.clone() {
match slasher_config.override_backend() {
DatabaseBackendOverride::Success(old_backend) => {
info!(
reason = "database exists",
configured_backend = %old_backend,
override_backend = %slasher_config.backend,
"Slasher backend overridden"
);
}
DatabaseBackendOverride::Failure(path) => {
warn!(
advice = "delete old MDBX database or enable MDBX backend",
path = %path.display(),
"Slasher backend override failed"
);
}
_ => {}
}
let slasher = Arc::new(
Slasher::open(slasher_config, spec)
.map_err(|e| format!("Slasher open error: {:?}", e))?,
);
builder.slasher(slasher)
} else {
builder
};
let builder = if let Some(monitoring_config) = &mut client_config.monitoring_api {
monitoring_config.db_path = Some(db_path);
monitoring_config.freezer_db_path = Some(freezer_db_path);
builder.monitoring_client(monitoring_config)?
} else {
builder
};
// Generate or load the node id.
let local_keypair = load_private_key(&client_config.network);
let node_id = peer_id_to_node_id(&local_keypair.public().to_peer_id())?.raw();
let builder = builder
.beacon_chain_builder(client_genesis, client_config.clone(), node_id)
.await?;
info!("Block production enabled");
let builder = builder.system_time_slot_clock()?;
// Inject the executor into the discv5 network config.
let discv5_executor = Discv5Executor(executor);
client_config.network.discv5_config.executor = Some(Box::new(discv5_executor));
builder
.build_beacon_chain()?
.network(Arc::new(client_config.network), local_keypair)
.await?
.notifier()?
.http_metrics_config(client_config.http_metrics.clone())
.build()
.map(Self)
}
pub fn into_inner(self) -> ProductionClient<E> {
self.0
}
}
fn validator_fork_epochs(spec: &ChainSpec) -> Result<(), Vec<(ForkName, Epoch)>> {
// @dapplion: "We try to schedule forks such that the fork epoch is a multiple of 256, to keep
// historical vectors in the same fork. Indirectly that makes light client periods align with
// fork boundaries."
let sync_committee_period = spec.epochs_per_sync_committee_period; // 256
let is_fork_boundary_misaligned = |epoch: Epoch| epoch % sync_committee_period != 0;
let forks_with_misaligned_epochs = ForkName::list_all_fork_epochs(spec)
.iter()
.filter_map(|(fork, fork_epoch_opt)| {
fork_epoch_opt
.and_then(|epoch| is_fork_boundary_misaligned(epoch).then_some((*fork, epoch)))
})
.collect::<Vec<_>>();
if forks_with_misaligned_epochs.is_empty() {
Ok(())
} else {
Err(forks_with_misaligned_epochs)
}
}
impl<E: EthSpec> Deref for ProductionBeaconNode<E> {
type Target = ProductionClient<E>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<E: EthSpec> DerefMut for ProductionBeaconNode<E> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
// Implements the Discv5 Executor trait over our global executor
#[derive(Clone)]
struct Discv5Executor(task_executor::TaskExecutor);
impl lighthouse_network::discv5::Executor for Discv5Executor {
fn spawn(&self, future: std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>) {
self.0.spawn(future, "discv5")
}
}
#[cfg(test)]
mod test {
use super::*;
use types::MainnetEthSpec;
#[test]
fn test_validator_fork_epoch_alignments() {
let mut spec = MainnetEthSpec::default_spec();
spec.altair_fork_epoch = Some(Epoch::new(0));
spec.bellatrix_fork_epoch = Some(Epoch::new(256));
spec.deneb_fork_epoch = Some(Epoch::new(257));
spec.electra_fork_epoch = None;
spec.fulu_fork_epoch = None;
spec.gloas_fork_epoch = None;
let result = validator_fork_epochs(&spec);
assert_eq!(
result,
Err(vec![(ForkName::Deneb, spec.deneb_fork_epoch.unwrap())])
);
}
}