From 094c36db97f969894d126f1c66f73b695cf710a5 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 4 Dec 2025 20:56:48 +1100 Subject: [PATCH 01/13] Use correct `Fork` in `verify_header_signature` (#8528) Fix a bug in `verify_header_signature` which tripped up some Lighthouse nodes at the Fusaka fork. The bug was a latent bug in a function that has been present for a long time, but only used by slashers. With Fulu it entered the critical path of blob/column verification -- call stack: - `FetchBlobsBeaconAdapter::process_engine_blobs` - `BeaconChain::process_engine_blobs` - `BeaconChain::check_engine_blobs_availability_and_import` - `BeaconChain::check_blob_header_signature_and_slashability` - `verify_header_signature` Thanks @eserilev for quickly diagnosing the root cause. Change `verify_header_signature` to use `ChainSpec::fork_at_epoch` to compute the `Fork`, rather than using the head state's fork. At a fork boundary the head state's fork is stale and lacks the data for the new fork. Using `fork_at_epoch` ensures that we use the correct fork data and validate transition block's signature correctly. Co-Authored-By: Michael Sproul --- .../beacon_chain/src/block_verification.rs | 6 +- .../beacon_chain/tests/column_verification.rs | 93 +++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 5078e24a51..dfcf24cf7e 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -2122,11 +2122,13 @@ pub fn verify_header_signature( .get(header.message.proposer_index as usize) .cloned() .ok_or(Err::unknown_validator_error(header.message.proposer_index))?; - let head_fork = chain.canonical_head.cached_head().head_fork(); + let fork = chain + .spec + .fork_at_epoch(header.message.slot.epoch(T::EthSpec::slots_per_epoch())); if header.verify_signature::( &proposer_pubkey, - &head_fork, + &fork, chain.genesis_validators_root, &chain.spec, ) { diff --git a/beacon_node/beacon_chain/tests/column_verification.rs b/beacon_node/beacon_chain/tests/column_verification.rs index 229ae1e199..dc99943464 100644 --- a/beacon_node/beacon_chain/tests/column_verification.rs +++ b/beacon_node/beacon_chain/tests/column_verification.rs @@ -115,3 +115,96 @@ async fn rpc_columns_with_invalid_header_signature() { BlockError::InvalidSignature(InvalidSignature::ProposerSignature) )); } + +// Regression test for verify_header_signature bug: it uses head_fork() which is wrong for fork blocks +#[tokio::test] +async fn verify_header_signature_fork_block_bug() { + // Create a spec with all forks enabled at genesis except Fulu which is at epoch 1 + // This allows us to easily create the scenario where the head is at Electra + // but we're trying to verify a block from Fulu epoch + let mut spec = test_spec::(); + + // Only run this test for FORK_NAME=fulu. + if !spec.is_fulu_scheduled() || spec.is_gloas_scheduled() { + return; + } + + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(0)); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.electra_fork_epoch = Some(Epoch::new(0)); + let fulu_fork_epoch = Epoch::new(1); + spec.fulu_fork_epoch = Some(fulu_fork_epoch); + + let spec = Arc::new(spec); + let harness = get_harness(VALIDATOR_COUNT, spec.clone(), NodeCustodyType::Supernode); + harness.execution_block_generator().set_min_blob_count(1); + + // Add some blocks in epoch 0 (Electra) + harness + .extend_chain( + E::slots_per_epoch() as usize - 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Verify we're still in epoch 0 (Electra) + let pre_fork_state = harness.get_current_state(); + assert_eq!(pre_fork_state.current_epoch(), Epoch::new(0)); + assert!(matches!(pre_fork_state, BeaconState::Electra(_))); + + // Now produce a block at the first slot of epoch 1 (Fulu fork). + // make_block will advance the state which will trigger the Electra->Fulu upgrade. + let fork_slot = fulu_fork_epoch.start_slot(E::slots_per_epoch()); + let ((signed_block, opt_blobs), _state_root) = + harness.make_block(pre_fork_state.clone(), fork_slot).await; + let (_, blobs) = opt_blobs.expect("Blobs should be present"); + assert!(!blobs.is_empty(), "Block should have blobs"); + let block_root = signed_block.canonical_root(); + + // Process the block WITHOUT blobs to make it unavailable. + // The block will be accepted but won't become the head because it's not fully available. + // This keeps the head at the pre-fork state (Electra). + harness.advance_slot(); + let rpc_block = harness + .build_rpc_block_from_blobs(block_root, signed_block.clone(), None) + .expect("Should build RPC block"); + let availability = harness + .chain + .process_block( + block_root, + rpc_block, + NotifyExecutionLayer::Yes, + BlockImportSource::RangeSync, + || Ok(()), + ) + .await + .expect("Block should be processed"); + assert_eq!( + availability, + AvailabilityProcessingStatus::MissingComponents(fork_slot, block_root), + "Block should be pending availability" + ); + + // The head should still be in epoch 0 (Electra) because the fork block isn't available + let current_head_state = harness.get_current_state(); + assert_eq!(current_head_state.current_epoch(), Epoch::new(0)); + assert!(matches!(current_head_state, BeaconState::Electra(_))); + + // Now try to process columns for the fork block. + // The bug: verify_header_signature previously used head_fork() which fetched the fork from + // the head state (still Electra fork), but the block was signed with the Fulu fork version. + // This caused an incorrect signature verification failure. + let data_column_sidecars = + generate_data_column_sidecars_from_block(&signed_block, &harness.chain.spec); + + // Now that the bug is fixed, the block should import. + let status = harness + .chain + .process_rpc_custody_columns(data_column_sidecars) + .await + .unwrap(); + assert_eq!(status, AvailabilityProcessingStatus::Imported(block_root)); +} From 9ddd2d8dbedb234d9d8d62002b9912441dfb0b54 Mon Sep 17 00:00:00 2001 From: 0xMushow <105550256+0xMushow@users.noreply.github.com> Date: Thu, 4 Dec 2025 14:35:12 +0100 Subject: [PATCH 02/13] fix(beacon_node): add pruning of observed_column_sidecars (#8531) None I noticed that `observed_column_sidecars` is missing its prune call in the finalization handler, which results in a memory leak on long-running nodes (very slow (**7MB/day**)) : https://github.com/sigp/lighthouse/blob/13dfa9200f822c41ccd81b95a3f052df54c888e9/beacon_node/beacon_chain/src/canonical_head.rs#L940-L959 Both caches use the same generic type `ObservedDataSidecars:` https://github.com/sigp/lighthouse/blob/22ec4b327186c4a4a87d2c8c745caf3b36cb6dd6/beacon_node/beacon_chain/src/beacon_chain.rs#L413-L416 The type's documentation explicitly requires manual pruning: > "*The cache supports pruning based upon the finalized epoch. It does not automatically prune, you must call Self::prune manually.*" https://github.com/sigp/lighthouse/blob/b4704eab4ac8edf0ea0282ed9a5758b784038dd2/beacon_node/beacon_chain/src/observed_data_sidecars.rs#L66-L74 Currently: - `observed_blob_sidecars` => pruned - `observed_column_sidecars` => **NOT** pruned Without pruning, the underlying HashMap accumulates entries indefinitely, causing continuous memory growth until the node restarts. Co-Authored-By: Antoine James --- beacon_node/beacon_chain/src/canonical_head.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index 7dd4c88c51..92b218f180 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -951,6 +951,13 @@ impl BeaconChain { .start_slot(T::EthSpec::slots_per_epoch()), ); + self.observed_column_sidecars.write().prune( + new_view + .finalized_checkpoint + .epoch + .start_slot(T::EthSpec::slots_per_epoch()), + ); + self.observed_slashable.write().prune( new_view .finalized_checkpoint From bd1966353a873e7e40fe93e6d89d3b19cba60045 Mon Sep 17 00:00:00 2001 From: hopinheimer <48147533+hopinheimer@users.noreply.github.com> Date: Tue, 3 Feb 2026 20:40:16 -0500 Subject: [PATCH 03/13] Use events API to eager send attestations (#7892) Co-Authored-By: hopinheimer Co-Authored-By: hopinheimer <48147533+hopinheimer@users.noreply.github.com> Co-Authored-By: Eitan Seri-Levi Co-Authored-By: Michael Sproul Co-Authored-By: Michael Sproul --- beacon_node/http_api/src/lib.rs | 14 +- book/src/help_vc.md | 6 + common/eth2/src/error.rs | 3 + common/eth2/src/lib.rs | 14 +- lighthouse/tests/validator_client.rs | 18 + .../beacon_node_fallback/Cargo.toml | 2 +- .../src/beacon_head_monitor.rs | 423 ++++++++++++++++++ .../beacon_node_fallback/src/lib.rs | 135 +++++- validator_client/src/cli.rs | 11 + validator_client/src/config.rs | 4 + validator_client/src/lib.rs | 25 +- .../src/attestation_service.rs | 202 +++++++-- 12 files changed, 810 insertions(+), 47 deletions(-) create mode 100644 validator_client/beacon_node_fallback/src/beacon_head_monitor.rs diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 4d7c76eb20..095c52fb29 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -3226,7 +3226,19 @@ pub fn serve( let s = futures::stream::select_all(receivers); - Ok(warp::sse::reply(warp::sse::keep_alive().stream(s))) + let response = warp::sse::reply(warp::sse::keep_alive().stream(s)); + + // Set headers to bypass nginx caching and buffering, which breaks realtime + // delivery. + let response = warp::reply::with_header(response, "X-Accel-Buffering", "no"); + let response = warp::reply::with_header(response, "X-Accel-Expires", "0"); + let response = warp::reply::with_header( + response, + "Cache-Control", + "no-cache, no-store, must-revalidate", + ); + + Ok(response) }) }, ); diff --git a/book/src/help_vc.md b/book/src/help_vc.md index 2a9936d1d2..4647780ea8 100644 --- a/book/src/help_vc.md +++ b/book/src/help_vc.md @@ -185,6 +185,12 @@ Flags: If present, do not attempt to discover new validators in the validators-dir. Validators will need to be manually added to the validator_definitions.yml file. + --disable-beacon-head-monitor + Disable the beacon head monitor which tries to attest as soon as any + of the configured beacon nodes sends a head event. Leaving the service + enabled is recommended, but disabling it can lead to reduced bandwidth + and more predictable usage of the primary beacon node (rather than the + fastest BN). --disable-latency-measurement-service Disables the service that periodically attempts to measure latency to BNs. diff --git a/common/eth2/src/error.rs b/common/eth2/src/error.rs index 1f21220b79..671a617c9e 100644 --- a/common/eth2/src/error.rs +++ b/common/eth2/src/error.rs @@ -17,6 +17,8 @@ pub enum Error { #[cfg(feature = "events")] /// The `reqwest_eventsource` client raised an error. SseClient(Box), + #[cfg(feature = "events")] + SseEventSource(reqwest_eventsource::CannotCloneRequestError), /// The server returned an error message where the body was able to be parsed. ServerMessage(ErrorMessage), /// The server returned an error message with an array of errors. @@ -100,6 +102,7 @@ impl Error { None } } + Error::SseEventSource(_) => None, Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::ServerIndexedMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::StatusCode(status) => Some(*status), diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 8746e3c063..10382b028a 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -40,7 +40,7 @@ use reqwest::{ header::{HeaderMap, HeaderValue}, }; #[cfg(feature = "events")] -use reqwest_eventsource::{Event, EventSource}; +use reqwest_eventsource::{Event, RequestBuilderExt}; use serde::{Serialize, de::DeserializeOwned}; use ssz::Encode; use std::fmt; @@ -76,6 +76,8 @@ const HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT: u32 = 4; const HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT: u32 = 4; const HTTP_GET_VALIDATOR_BLOCK_TIMEOUT_QUOTIENT: u32 = 4; +// Generally the timeout for events should be longer than a slot. +const HTTP_GET_EVENTS_TIMEOUT_MULTIPLIER: u32 = 50; const HTTP_DEFAULT_TIMEOUT_QUOTIENT: u32 = 4; /// A struct to define a variety of different timeouts for different validator tasks to ensure @@ -96,6 +98,7 @@ pub struct Timeouts { pub get_debug_beacon_states: Duration, pub get_deposit_snapshot: Duration, pub get_validator_block: Duration, + pub events: Duration, pub default: Duration, } @@ -116,6 +119,7 @@ impl Timeouts { get_debug_beacon_states: timeout, get_deposit_snapshot: timeout, get_validator_block: timeout, + events: HTTP_GET_EVENTS_TIMEOUT_MULTIPLIER * timeout, default: timeout, } } @@ -138,6 +142,7 @@ impl Timeouts { get_debug_beacon_states: base_timeout / HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT, get_deposit_snapshot: base_timeout / HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT, get_validator_block: base_timeout / HTTP_GET_VALIDATOR_BLOCK_TIMEOUT_QUOTIENT, + events: HTTP_GET_EVENTS_TIMEOUT_MULTIPLIER * base_timeout, default: base_timeout / HTTP_DEFAULT_TIMEOUT_QUOTIENT, } } @@ -2800,7 +2805,12 @@ impl BeaconNodeHttpClient { .join(","); path.query_pairs_mut().append_pair("topics", &topic_string); - let mut es = EventSource::get(path); + let mut es = self + .client + .get(path) + .timeout(self.timeouts.events) + .eventsource() + .map_err(Error::SseEventSource)?; // If we don't await `Event::Open` here, then the consumer // will not get any Message events until they start awaiting the stream. // This is a way to register the stream with the sse server before diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index ee3e910b36..6fd5a6538c 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -758,3 +758,21 @@ fn validator_proposer_nodes() { ); }); } + +// Head monitor is enabled by default. +#[test] +fn head_monitor_default() { + CommandLineTest::new().run().with_config(|config| { + assert!(config.enable_beacon_head_monitor); + }); +} + +#[test] +fn head_monitor_disabled() { + CommandLineTest::new() + .flag("disable-beacon-head-monitor", None) + .run() + .with_config(|config| { + assert!(!config.enable_beacon_head_monitor); + }); +} diff --git a/validator_client/beacon_node_fallback/Cargo.toml b/validator_client/beacon_node_fallback/Cargo.toml index 481aece48b..bc1ac20d44 100644 --- a/validator_client/beacon_node_fallback/Cargo.toml +++ b/validator_client/beacon_node_fallback/Cargo.toml @@ -11,7 +11,7 @@ path = "src/lib.rs" [dependencies] bls = { workspace = true } clap = { workspace = true } -eth2 = { workspace = true } +eth2 = { workspace = true, features = ["events"] } futures = { workspace = true } itertools = { workspace = true } sensitive_url = { workspace = true } diff --git a/validator_client/beacon_node_fallback/src/beacon_head_monitor.rs b/validator_client/beacon_node_fallback/src/beacon_head_monitor.rs new file mode 100644 index 0000000000..bed107d856 --- /dev/null +++ b/validator_client/beacon_node_fallback/src/beacon_head_monitor.rs @@ -0,0 +1,423 @@ +use crate::BeaconNodeFallback; +use eth2::types::{EventKind, EventTopic, Hash256, SseHead}; +use futures::StreamExt; +use slot_clock::SlotClock; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; +use tracing::{debug, info, warn}; +use types::EthSpec; + +type CacheHashMap = HashMap; + +// This is used to send the index derived from `CandidateBeaconNode` to the +// `AttestationService` for further processing +#[derive(Debug)] +pub struct HeadEvent { + pub beacon_node_index: usize, + pub slot: types::Slot, + pub beacon_block_root: Hash256, +} + +/// Cache to maintain the latest head received from each of the beacon nodes +/// in the `BeaconNodeFallback`. +#[derive(Debug)] +pub struct BeaconHeadCache { + cache: RwLock, +} + +impl BeaconHeadCache { + /// Creates a new empty beacon head cache. + pub fn new() -> Self { + Self { + cache: RwLock::new(HashMap::new()), + } + } + + /// Retrieves the cached head for a specific beacon node. + /// Returns `None` if no head has been cached for that node yet. + pub async fn get(&self, beacon_node_index: usize) -> Option { + self.cache.read().await.get(&beacon_node_index).cloned() + } + + /// Stores or updates the head event for a specific beacon node. + /// Replaces any previously cached head for the given node. + pub async fn insert(&self, beacon_node_index: usize, head: SseHead) { + self.cache.write().await.insert(beacon_node_index, head); + } + + /// Checks if the given head is the latest among all cached heads. + /// Returns `true` if the head's slot is >= all cached heads' slots. + pub async fn is_latest(&self, head: &SseHead) -> bool { + let cache = self.cache.read().await; + cache + .values() + .all(|cache_head| head.slot >= cache_head.slot) + } + + /// Clears all cached heads, removing entries for all beacon nodes. + /// Useful when beacon node candidates are refreshed to avoid stale references. + pub async fn purge_cache(&self) { + self.cache.write().await.clear(); + } +} + +impl Default for BeaconHeadCache { + fn default() -> Self { + Self::new() + } +} + +// Runs a non-terminating loop to update the `BeaconHeadCache` with the latest head received +// from the candidate beacon_nodes. This is an attempt to stream events to beacon nodes and +// potential start attestation duties earlier as soon as latest head is receive from any of the +// beacon node in contrast to attest at the 1/3rd mark in the slot. +// +// +// The cache and the candidate BNs list are refresh/purged to avoid dangling reference conditions +// that arise due to `update_candidates_list`. +// +// Starts the service to perpetually stream head events from connected beacon_nodes +pub async fn poll_head_event_from_beacon_nodes( + beacon_nodes: Arc>, +) -> Result<(), String> { + let head_cache = beacon_nodes + .beacon_head_cache + .clone() + .ok_or("Unable to start head monitor without beacon_head_cache")?; + let head_monitor_send = beacon_nodes + .head_monitor_send + .clone() + .ok_or("Unable to start head monitor without head_monitor_send")?; + + info!("Starting head monitoring service"); + let candidates = { + let candidates_guard = beacon_nodes.candidates.read().await; + candidates_guard.clone() + }; + + // Clear the cache in case it contains stale data from a previous run. This function gets + // restarted if it fails (see monitoring in `start_fallback_updater_service`). + head_cache.purge_cache().await; + + // Create Vec of streams, which we will select over. + let mut streams = vec![]; + + for candidate in &candidates { + let head_event_stream = candidate + .beacon_node + .get_events::(&[EventTopic::Head]) + .await; + + let head_event_stream = match head_event_stream { + Ok(stream) => stream, + Err(e) => { + warn!(error = ?e, node_index = candidate.index, "Failed to get head event stream"); + continue; + } + }; + + streams.push(head_event_stream.map(|event| (candidate.index, event))); + } + + if streams.is_empty() { + return Err("No beacon nodes available for head event streaming".to_string()); + } + + // Combine streams into a single stream and poll events from any of them. + let mut combined_stream = futures::stream::select_all(streams); + + while let Some((candidate_index, event_result)) = combined_stream.next().await { + match event_result { + Ok(EventKind::Head(head)) => { + debug!( + candidate_index, + block_root = ?head.block, + slot = %head.slot, + "New head from beacon node" + ); + + // Skip optimistic heads - the beacon node can't produce valid + // attestation data when its execution layer is not verified + if head.execution_optimistic { + debug!( + candidate_index, + block_root = ?head.block, + slot = %head.slot, + "Skipping optimistic head" + ); + continue; + } + + head_cache.insert(candidate_index, head.clone()).await; + + if !head_cache.is_latest(&head).await { + debug!( + candidate_index, + block_root = ?head.block, + slot = %head.slot, + "Skipping stale head" + ); + continue; + } + + if head_monitor_send + .send(HeadEvent { + beacon_node_index: candidate_index, + slot: head.slot, + beacon_block_root: head.block, + }) + .await + .is_err() + { + return Err("Head monitoring service channel closed".into()); + } + } + Ok(event) => { + warn!( + event_kind = event.topic_name(), + candidate_index, "Received unexpected event from BN" + ); + continue; + } + Err(e) => { + return Err(format!( + "Head monitoring stream error, node: {candidate_index}, error: {e:?}" + )); + } + } + } + + Err("Stream ended unexpectedly".into()) +} + +#[cfg(test)] +mod tests { + use super::*; + use bls::FixedBytesExtended; + use types::{Hash256, Slot}; + + fn create_sse_head(slot: u64, block_root: u8) -> SseHead { + SseHead { + slot: types::Slot::new(slot), + block: Hash256::from_low_u64_be(block_root as u64), + state: Hash256::from_low_u64_be(block_root as u64), + epoch_transition: false, + previous_duty_dependent_root: Hash256::from_low_u64_be(block_root as u64), + current_duty_dependent_root: Hash256::from_low_u64_be(block_root as u64), + execution_optimistic: false, + } + } + + #[tokio::test] + async fn test_beacon_head_cache_insertion_and_retrieval() { + let cache = BeaconHeadCache::new(); + let head_1 = create_sse_head(1, 1); + let head_2 = create_sse_head(2, 2); + + cache.insert(0, head_1.clone()).await; + cache.insert(1, head_2.clone()).await; + + assert_eq!(cache.get(0).await, Some(head_1)); + assert_eq!(cache.get(1).await, Some(head_2)); + assert_eq!(cache.get(2).await, None); + } + + #[tokio::test] + async fn test_beacon_head_cache_update() { + let cache = BeaconHeadCache::new(); + let head_old = create_sse_head(1, 1); + let head_new = create_sse_head(2, 2); + + cache.insert(0, head_old).await; + cache.insert(0, head_new.clone()).await; + + assert_eq!(cache.get(0).await, Some(head_new)); + } + + #[tokio::test] + async fn test_is_latest_with_higher_slot() { + let cache = BeaconHeadCache::new(); + let head_1 = create_sse_head(1, 1); + let head_2 = create_sse_head(2, 2); + let head_3 = create_sse_head(3, 3); + + cache.insert(0, head_1).await; + cache.insert(1, head_2).await; + + assert!(cache.is_latest(&head_3).await); + } + + #[tokio::test] + async fn test_is_latest_with_lower_slot() { + let cache = BeaconHeadCache::new(); + let head_1 = create_sse_head(1, 1); + let head_2 = create_sse_head(2, 2); + let head_older = create_sse_head(1, 99); + + cache.insert(0, head_1).await; + cache.insert(1, head_2).await; + + assert!(!cache.is_latest(&head_older).await); + } + + #[tokio::test] + async fn test_is_latest_with_equal_slot() { + let cache = BeaconHeadCache::new(); + let head_1 = create_sse_head(5, 1); + let head_2 = create_sse_head(5, 2); + let head_equal = create_sse_head(5, 3); + + cache.insert(0, head_1).await; + cache.insert(1, head_2).await; + + assert!(cache.is_latest(&head_equal).await); + } + + #[tokio::test] + async fn test_is_latest_empty_cache() { + let cache = BeaconHeadCache::new(); + let head = create_sse_head(1, 1); + + assert!(cache.is_latest(&head).await); + } + + #[tokio::test] + async fn test_purge_cache_clears_all_entries() { + let cache = BeaconHeadCache::new(); + let head_1 = create_sse_head(1, 1); + let head_2 = create_sse_head(2, 2); + + cache.insert(0, head_1).await; + cache.insert(1, head_2).await; + + assert!(cache.get(0).await.is_some()); + assert!(cache.get(1).await.is_some()); + + cache.purge_cache().await; + + assert!(cache.get(0).await.is_none()); + assert!(cache.get(1).await.is_none()); + } + + #[tokio::test] + async fn test_head_event_creation() { + let block_root = Hash256::from_low_u64_be(99); + let event = HeadEvent { + beacon_node_index: 42, + slot: Slot::new(123), + beacon_block_root: block_root, + }; + assert_eq!(event.beacon_node_index, 42); + assert_eq!(event.slot, Slot::new(123)); + assert_eq!(event.beacon_block_root, block_root); + } + + #[tokio::test] + async fn test_cache_caches_multiple_heads_from_different_nodes() { + let cache = BeaconHeadCache::new(); + let head_1 = create_sse_head(10, 1); + let head_2 = create_sse_head(5, 2); + let head_3 = create_sse_head(8, 3); + + cache.insert(0, head_1.clone()).await; + cache.insert(1, head_2.clone()).await; + cache.insert(2, head_3.clone()).await; + + // Verify all are stored + assert_eq!(cache.get(0).await, Some(head_1)); + assert_eq!(cache.get(1).await, Some(head_2)); + assert_eq!(cache.get(2).await, Some(head_3)); + + // The latest should be slot 10 + let head_10 = create_sse_head(10, 99); + assert!(cache.is_latest(&head_10).await); + + // Anything with slot > 10 should be latest + let head_11 = create_sse_head(11, 99); + assert!(cache.is_latest(&head_11).await); + + // Anything with slot < 10 should not be latest + let head_9 = create_sse_head(9, 99); + assert!(!cache.is_latest(&head_9).await); + } + + #[tokio::test] + async fn test_cache_handles_concurrent_operations() { + let cache = Arc::new(BeaconHeadCache::new()); + let mut handles = vec![]; + + // Spawn multiple tasks that insert heads concurrently + for i in 0..10 { + let cache_clone = cache.clone(); + let handle = tokio::spawn(async move { + let head = create_sse_head(i as u64, (i % 256) as u8); + cache_clone.insert(i, head).await; + }); + handles.push(handle); + } + + // Wait for all tasks to complete + for handle in handles { + handle.await.unwrap(); + } + + // Verify all heads are cached + for i in 0..10 { + assert!(cache.get(i).await.is_some()); + } + } + + #[tokio::test] + async fn test_is_latest_after_cache_updates() { + let cache = BeaconHeadCache::new(); + + // Start with head at slot 5 + let head_5 = create_sse_head(5, 1); + cache.insert(0, head_5.clone()).await; + assert!(cache.is_latest(&head_5).await); + + // Add a higher slot + let head_10 = create_sse_head(10, 2); + cache.insert(1, head_10.clone()).await; + + // head_5 should no longer be latest + assert!(!cache.is_latest(&head_5).await); + // head_10 should be latest + assert!(cache.is_latest(&head_10).await); + + // Add an even higher slot + let head_15 = create_sse_head(15, 3); + cache.insert(2, head_15.clone()).await; + + // head_10 should no longer be latest + assert!(!cache.is_latest(&head_10).await); + // head_15 should be latest + assert!(cache.is_latest(&head_15).await); + } + + #[tokio::test] + async fn test_cache_default_is_empty() { + let cache = BeaconHeadCache::default(); + assert!(cache.get(0).await.is_none()); + assert!(cache.get(999).await.is_none()); + } + + #[tokio::test] + async fn test_is_latest_with_multiple_same_slot_heads() { + let cache = BeaconHeadCache::new(); + let head_slot_5_node1 = create_sse_head(5, 1); + let head_slot_5_node2 = create_sse_head(5, 2); + let head_slot_5_node3 = create_sse_head(5, 3); + + cache.insert(0, head_slot_5_node1).await; + cache.insert(1, head_slot_5_node2).await; + + // All heads with slot 5 should be considered latest + assert!(cache.is_latest(&head_slot_5_node3).await); + + // But heads with slot 4 should not be latest + let head_slot_4 = create_sse_head(4, 4); + assert!(!cache.is_latest(&head_slot_4).await); + } +} diff --git a/validator_client/beacon_node_fallback/src/lib.rs b/validator_client/beacon_node_fallback/src/lib.rs index 3c20e57200..b36ec70aa3 100644 --- a/validator_client/beacon_node_fallback/src/lib.rs +++ b/validator_client/beacon_node_fallback/src/lib.rs @@ -2,7 +2,10 @@ //! "fallback" behaviour; it will try a request on all of the nodes until one or none of them //! succeed. +pub mod beacon_head_monitor; pub mod beacon_node_health; + +use beacon_head_monitor::{BeaconHeadCache, HeadEvent, poll_head_event_from_beacon_nodes}; use beacon_node_health::{ BeaconNodeHealth, BeaconNodeSyncDistanceTiers, ExecutionEngineHealth, IsOptimistic, SyncDistanceTier, check_node_health, @@ -22,7 +25,10 @@ use std::time::{Duration, Instant}; use std::vec::Vec; use strum::VariantNames; use task_executor::TaskExecutor; -use tokio::{sync::RwLock, time::sleep}; +use tokio::{ + sync::{RwLock, mpsc}, + time::sleep, +}; use tracing::{debug, error, warn}; use types::{ChainSpec, Config as ConfigSpec, EthSpec, Slot}; use validator_metrics::{ENDPOINT_ERRORS, ENDPOINT_REQUESTS, inc_counter_vec}; @@ -68,6 +74,31 @@ pub fn start_fallback_updater_service( return Err("Cannot start fallback updater without slot clock"); } + let beacon_nodes_ref = beacon_nodes.clone(); + + // the existence of head_monitor_send is overloaded with the predicate of + // requirement of starting the head monitoring service or not. + if beacon_nodes_ref.head_monitor_send.is_some() { + let head_monitor_future = async move { + loop { + if let Err(error) = + poll_head_event_from_beacon_nodes::(beacon_nodes_ref.clone()).await + { + warn!(error, "Head service failed retrying starting next slot"); + + let sleep_time = beacon_nodes_ref + .slot_clock + .as_ref() + .and_then(|slot_clock| slot_clock.duration_to_next_slot()) + .unwrap_or_else(|| beacon_nodes_ref.spec.get_slot_duration()); + sleep(sleep_time).await + } + } + }; + + executor.spawn(head_monitor_future, "head_monitoring"); + } + let future = async move { loop { beacon_nodes.update_all_candidates::().await; @@ -96,12 +127,15 @@ pub fn start_fallback_updater_service( pub enum Error { /// We attempted to contact the node but it failed. RequestFailed(T), + /// The beacon node with the requested index was not available. + CandidateIndexUnknown(usize), } impl Error { pub fn request_failure(&self) -> Option<&T> { match self { Error::RequestFailed(e) => Some(e), + Error::CandidateIndexUnknown(_) => None, } } } @@ -380,6 +414,8 @@ pub struct BeaconNodeFallback { pub candidates: Arc>>, distance_tiers: BeaconNodeSyncDistanceTiers, slot_clock: Option, + beacon_head_cache: Option>, + head_monitor_send: Option>>, broadcast_topics: Vec, spec: Arc, } @@ -396,6 +432,8 @@ impl BeaconNodeFallback { candidates: Arc::new(RwLock::new(candidates)), distance_tiers, slot_clock: None, + beacon_head_cache: None, + head_monitor_send: None, broadcast_topics, spec, } @@ -410,6 +448,15 @@ impl BeaconNodeFallback { self.slot_clock = Some(slot_clock); } + /// This the head monitor channel that streams events from all the beacon nodes that the + /// validator client is connected in the `BeaconNodeFallback`. This also initializes the + /// beacon_head_cache under the assumption the beacon_head_cache will always be needed when + /// head_monitor_send is set. + pub fn set_head_send(&mut self, head_monitor_send: Arc>) { + self.head_monitor_send = Some(head_monitor_send); + self.beacon_head_cache = Some(Arc::new(BeaconHeadCache::new())); + } + /// The count of candidates, regardless of their state. pub async fn num_total(&self) -> usize { self.candidates.read().await.len() @@ -493,6 +540,10 @@ impl BeaconNodeFallback { let mut candidates = self.candidates.write().await; *candidates = new_candidates; + if let Some(cache) = &self.beacon_head_cache { + cache.purge_cache().await; + } + Ok(new_list) } @@ -646,6 +697,32 @@ impl BeaconNodeFallback { Err(Errors(errors)) } + /// Try `func` on a specific beacon node by index. + /// + /// Returns immediately if the preferred node succeeds, otherwise return an error. + pub async fn run_on_candidate_index( + &self, + candidate_index: usize, + func: F, + ) -> Result> + where + F: Fn(BeaconNodeHttpClient) -> R + Clone, + R: Future>, + Err: Debug, + { + // Find the requested beacon node or return an error. + let candidates = self.candidates.read().await; + let Some(candidate) = candidates.iter().find(|c| c.index == candidate_index) else { + return Err(Error::CandidateIndexUnknown(candidate_index)); + }; + let candidate_node = candidate.beacon_node.clone(); + drop(candidates); + + Self::run_on_candidate(candidate_node, &func) + .await + .map_err(|(_, err)| err) + } + /// Run the future `func` on `candidate` while reporting metrics. async fn run_on_candidate( candidate: BeaconNodeHttpClient, @@ -1073,4 +1150,60 @@ mod tests { mock1.expect(3).assert(); mock2.expect(3).assert(); } + + #[tokio::test] + async fn run_on_candidate_index_success() { + let spec = Arc::new(MainnetEthSpec::default_spec()); + let (mut mock_beacon_node_1, beacon_node_1) = new_mock_beacon_node(0, &spec).await; + let (mut mock_beacon_node_2, beacon_node_2) = new_mock_beacon_node(1, &spec).await; + let (mut mock_beacon_node_3, beacon_node_3) = new_mock_beacon_node(2, &spec).await; + + let beacon_node_fallback = create_beacon_node_fallback( + vec![beacon_node_1, beacon_node_2, beacon_node_3], + vec![], + spec.clone(), + ); + + let mock1 = mock_beacon_node_1.mock_offline_node(); + let _mock2 = mock_beacon_node_2.mock_online_node(); + let mock3 = mock_beacon_node_3.mock_online_node(); + + // Request with preferred_index=1 (beacon_node_2) + let result = beacon_node_fallback + .run_on_candidate_index(1, |client| async move { client.get_node_version().await }) + .await; + + // Should succeed since beacon_node_2 is online + assert!(result.is_ok()); + + // mock1 should not be called since preferred node succeeds + mock1.expect(0).assert(); + mock3.expect(0).assert(); + } + + #[tokio::test] + async fn run_on_candidate_index_error() { + let spec = Arc::new(MainnetEthSpec::default_spec()); + let (mut mock_beacon_node_1, beacon_node_1) = new_mock_beacon_node(0, &spec).await; + let (mut mock_beacon_node_2, beacon_node_2) = new_mock_beacon_node(1, &spec).await; + let (mut mock_beacon_node_3, beacon_node_3) = new_mock_beacon_node(2, &spec).await; + + let beacon_node_fallback = create_beacon_node_fallback( + vec![beacon_node_1, beacon_node_2, beacon_node_3], + vec![], + spec.clone(), + ); + + let _mock1 = mock_beacon_node_1.mock_online_node(); + let _mock2 = mock_beacon_node_2.mock_offline_node(); + let _mock3 = mock_beacon_node_3.mock_offline_node(); + + // Request with preferred_index=1 (beacon_node_2), but it's offline + let result = beacon_node_fallback + .run_on_candidate_index(1, |client| async move { client.get_node_version().await }) + .await; + + // Should fail. + assert!(result.is_err()); + } } diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 3e1c46097f..0eb0e9e5dd 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -476,6 +476,17 @@ pub struct ValidatorClient { )] pub beacon_nodes_sync_tolerances: Vec, + #[clap( + long, + help = "Disable the beacon head monitor which tries to attest as soon as any of the \ + configured beacon nodes sends a head event. Leaving the service enabled is \ + recommended, but disabling it can lead to reduced bandwidth and more predictable \ + usage of the primary beacon node (rather than the fastest BN).", + display_order = 0, + help_heading = FLAG_HEADER + )] + pub disable_beacon_head_monitor: bool, + #[clap( long, help = "Disable Lighthouse's slashing protection for all web3signer keys. This can \ diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index 1a286a74dc..d68a78b705 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -82,6 +82,8 @@ pub struct Config { pub broadcast_topics: Vec, /// 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. @@ -132,6 +134,7 @@ impl Default for Config { 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(), @@ -377,6 +380,7 @@ impl Config { 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; diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 2b863715d2..c0d561b175 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -9,10 +9,12 @@ use metrics::set_gauge; use monitoring_api::{MonitoringHttpClient, ProcessType}; use sensitive_url::SensitiveUrl; use slashing_protection::{SLASHING_PROTECTION_FILENAME, SlashingDatabase}; +use tokio::sync::Mutex; use account_utils::validator_definitions::ValidatorDefinitions; use beacon_node_fallback::{ - BeaconNodeFallback, CandidateBeaconNode, start_fallback_updater_service, + BeaconNodeFallback, CandidateBeaconNode, beacon_head_monitor::HeadEvent, + start_fallback_updater_service, }; use clap::ArgMatches; use doppelganger_service::DoppelgangerService; @@ -70,6 +72,8 @@ pub const AGGREGATION_PRE_COMPUTE_EPOCHS: u64 = 2; /// Number of slots in advance to compute sync selection proofs when in `distributed` mode. pub const AGGREGATION_PRE_COMPUTE_SLOTS_DISTRIBUTED: u64 = 1; +const MAX_HEAD_EVENT_QUEUE_LEN: usize = 1_024; + type ValidatorStore = LighthouseValidatorStore; #[derive(Clone)] @@ -395,6 +399,17 @@ impl ProductionValidatorClient { beacon_nodes.set_slot_clock(slot_clock.clone()); proposer_nodes.set_slot_clock(slot_clock.clone()); + // Only the beacon_nodes are used for attestation duties and thus biconditionally + // proposer_nodes do not need head_send ref. + let head_monitor_rx = if config.enable_beacon_head_monitor { + let (head_monitor_tx, head_receiver) = + mpsc::channel::(MAX_HEAD_EVENT_QUEUE_LEN); + beacon_nodes.set_head_send(Arc::new(head_monitor_tx)); + Some(Mutex::new(head_receiver)) + } else { + None + }; + let beacon_nodes = Arc::new(beacon_nodes); start_fallback_updater_service::<_, E>(context.executor.clone(), beacon_nodes.clone())?; @@ -505,15 +520,17 @@ impl ProductionValidatorClient { let block_service = block_service_builder.build()?; - let attestation_service = AttestationServiceBuilder::new() + let attestation_builder = AttestationServiceBuilder::new() .duties_service(duties_service.clone()) .slot_clock(slot_clock.clone()) .validator_store(validator_store.clone()) .beacon_nodes(beacon_nodes.clone()) .executor(context.executor.clone()) + .head_monitor_rx(head_monitor_rx) .chain_spec(context.eth2_config.spec.clone()) - .disable(config.disable_attesting) - .build()?; + .disable(config.disable_attesting); + + let attestation_service = attestation_builder.build()?; let preparation_service = PreparationServiceBuilder::new() .slot_clock(slot_clock.clone()) diff --git a/validator_client/validator_services/src/attestation_service.rs b/validator_client/validator_services/src/attestation_service.rs index 326ec6d01e..a9d5283312 100644 --- a/validator_client/validator_services/src/attestation_service.rs +++ b/validator_client/validator_services/src/attestation_service.rs @@ -1,5 +1,5 @@ use crate::duties_service::{DutiesService, DutyAndProof}; -use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; +use beacon_node_fallback::{ApiTopic, BeaconNodeFallback, beacon_head_monitor::HeadEvent}; use futures::future::join_all; use logging::crit; use slot_clock::SlotClock; @@ -7,10 +7,12 @@ use std::collections::HashMap; use std::ops::Deref; use std::sync::Arc; use task_executor::TaskExecutor; +use tokio::sync::Mutex; +use tokio::sync::mpsc; use tokio::time::{Duration, Instant, sleep, sleep_until}; -use tracing::{Instrument, debug, error, info, info_span, instrument, trace, warn}; +use tracing::{Instrument, debug, error, info, info_span, instrument, warn}; use tree_hash::TreeHash; -use types::{Attestation, AttestationData, ChainSpec, CommitteeIndex, EthSpec, Slot}; +use types::{Attestation, AttestationData, ChainSpec, CommitteeIndex, EthSpec, Hash256, Slot}; use validator_store::{Error as ValidatorStoreError, ValidatorStore}; /// Builds an `AttestationService`. @@ -22,6 +24,7 @@ pub struct AttestationServiceBuilder beacon_nodes: Option>>, executor: Option, chain_spec: Option>, + head_monitor_rx: Option>>, disable: bool, } @@ -34,6 +37,7 @@ impl AttestationServiceBuil beacon_nodes: None, executor: None, chain_spec: None, + head_monitor_rx: None, disable: false, } } @@ -73,6 +77,13 @@ impl AttestationServiceBuil self } + pub fn head_monitor_rx( + mut self, + head_monitor_rx: Option>>, + ) -> Self { + self.head_monitor_rx = head_monitor_rx; + self + } pub fn build(self) -> Result, String> { Ok(AttestationService { inner: Arc::new(Inner { @@ -94,7 +105,9 @@ impl AttestationServiceBuil chain_spec: self .chain_spec .ok_or("Cannot build AttestationService without chain_spec")?, + head_monitor_rx: self.head_monitor_rx, disable: self.disable, + latest_attested_slot: Mutex::new(Slot::default()), }), }) } @@ -108,10 +121,13 @@ pub struct Inner { beacon_nodes: Arc>, executor: TaskExecutor, chain_spec: Arc, + head_monitor_rx: Option>>, disable: bool, + latest_attested_slot: Mutex, } -/// Attempts to produce attestations for all known validators 1/3rd of the way through each slot. +/// Attempts to produce attestations for all known validators 1/3rd of the way through each slot +/// or when a head event is received from the BNs. /// /// If any validators are on the same committee, a single attestation will be downloaded and /// returned to the beacon node. This attestation will have a signature from each of the @@ -161,19 +177,42 @@ impl AttestationService None, + event = self.poll_for_head_events() => + event.map(|event| (event.beacon_node_index, event.beacon_block_root)), + } + } else { + sleep(duration + unaggregated_attestation_due).await; + None + }; + + let Some(current_slot) = self.slot_clock.now() else { + error!("Failed to read slot clock after trigger"); + continue; + }; + + let mut last_slot = self.latest_attested_slot.lock().await; + + if current_slot <= *last_slot { + debug!(%current_slot, "Attestation already initiated for the slot"); + continue; + } + + match self.spawn_attestation_tasks(beacon_node_data).await { + Ok(_) => { + *last_slot = current_slot; + } + Err(e) => { + crit!(error = e, "Failed to spawn attestation tasks") + } } } }; @@ -182,15 +221,38 @@ impl AttestationService Option { + let Some(receiver) = &self.head_monitor_rx else { + return None; + }; + let mut receiver = receiver.lock().await; + loop { + match receiver.recv().await { + Some(head_event) => { + // Only return head events for the current slot - this ensures the + // block for this slot has been produced before triggering attestation + let current_slot = self.slot_clock.now()?; + if head_event.slot == current_slot { + return Some(head_event); + } + // Head event is for a previous slot, keep waiting + } + None => { + warn!("Head monitor channel closed unexpectedly"); + return None; + } + } + } + } + /// Spawn only one new task for attestation post-Electra /// For each required aggregates, spawn a new task that downloads, signs and uploads the /// aggregates to the beacon node. - fn spawn_attestation_tasks(&self) -> Result<(), String> { + async fn spawn_attestation_tasks( + &self, + beacon_node_data: Option<(usize, Hash256)>, + ) -> Result<(), String> { let slot = self.slot_clock.now().ok_or("Failed to read slot clock")?; - let duration_to_next_slot = self - .slot_clock - .duration_to_next_slot() - .ok_or("Unable to determine duration to next slot")?; // Create and publish an `Attestation` for all validators only once // as the committee_index is not included in AttestationData post-Electra @@ -201,29 +263,89 @@ impl AttestationService attestation_data_from_head_event = Some(data), + Err(error) => { + warn!(?error, "Failed to attest based on head event"); + } + } + } + + // If the beacon node that sent us the head failed to attest, wait until the attestation + // deadline then try all BNs. + let attestation_data = if let Some(attestation_data) = attestation_data_from_head_event { + attestation_data + } else { + let duration_to_deadline = self + .slot_clock + .duration_to_slot(slot + 1) + .and_then(|duration_to_next_slot| { + duration_to_next_slot + .checked_add(self.chain_spec.get_unaggregated_attestation_due()) + }) + .map(|next_slot_deadline| { + next_slot_deadline.saturating_sub(self.chain_spec.get_slot_duration()) + }) + .unwrap_or(Duration::from_secs(0)); + sleep(duration_to_deadline).await; + + attestation_service + .beacon_nodes + .first_success(|beacon_node| async move { + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::ATTESTATION_SERVICE_TIMES, + &[validator_metrics::ATTESTATIONS_HTTP_GET], + ); + let data = beacon_node + .get_validator_attestation_data(slot, 0) + .await + .map_err(|e| format!("Failed to produce attestation data: {:?}", e))? + .data; + Ok::(data) + }) + .await + .map_err(|e| e.to_string())? + }; + + // Sign and publish attestations. + let publication_handle = self .inner .executor .spawn_handle( async move { - let attestation_data = attestation_service - .beacon_nodes - .first_success(|beacon_node| async move { - let _timer = validator_metrics::start_timer_vec( - &validator_metrics::ATTESTATION_SERVICE_TIMES, - &[validator_metrics::ATTESTATIONS_HTTP_GET], - ); - beacon_node - .get_validator_attestation_data(slot, 0) - .await - .map_err(|e| format!("Failed to produce attestation data: {:?}", e)) - .map(|result| result.data) - }) - .await - .map_err(|e| e.to_string())?; - attestation_service .sign_and_publish_attestations( slot, @@ -241,12 +363,16 @@ impl AttestationService(attestation_data) }, - "unaggregated attestation production", + "unaggregated attestation publication", ) .ok_or("Failed to spawn attestation data task")?; // If a validator needs to publish an aggregate attestation, they must do so at 2/3 // through the slot. This delay triggers at this time + let duration_to_next_slot = self + .slot_clock + .duration_to_slot(slot + 1) + .ok_or("Unable to determine duration to next slot")?; let aggregate_production_instant = Instant::now() + duration_to_next_slot .checked_add(self.chain_spec.get_aggregate_attestation_due()) @@ -270,7 +396,7 @@ impl AttestationService data, Ok(Some(Err(err))) => { error!(?err, "Attestation production failed"); From 819dae3d94d013def40ceb7a779eedefe2b0e9a4 Mon Sep 17 00:00:00 2001 From: hopinheimer <48147533+hopinheimer@users.noreply.github.com> Date: Tue, 3 Feb 2026 20:49:56 -0500 Subject: [PATCH 04/13] fix bootnode entry (#8748) --- .../built_in_network_configs/mainnet/bootstrap_nodes.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/bootstrap_nodes.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/bootstrap_nodes.yaml index 70aeaac9c5..5a75d22965 100644 --- a/common/eth2_network_config/built_in_network_configs/mainnet/bootstrap_nodes.yaml +++ b/common/eth2_network_config/built_in_network_configs/mainnet/bootstrap_nodes.yaml @@ -31,4 +31,4 @@ # Lodestar team's bootnodes - enr:-IS4QPi-onjNsT5xAIAenhCGTDl4z-4UOR25Uq-3TmG4V3kwB9ljLTb_Kp1wdjHNj-H8VVLRBSSWVZo3GUe3z6k0E-IBgmlkgnY0gmlwhKB3_qGJc2VjcDI1NmsxoQMvAfgB4cJXvvXeM6WbCG86CstbSxbQBSGx31FAwVtOTYN1ZHCCIyg # 160.119.254.161 | hostafrica-southafrica -- enr:-KG4QCb8NC3gEM3I0okStV5BPX7Bg6ZXTYCzzbYyEXUPGcZtHmvQtiJH4C4F2jG7azTcb9pN3JlgpfxAnRVFzJ3-LykBgmlkgnY0gmlwhFPlR9KDaXA2kP6AAAAAAAAAAlBW__4my5iJc2VjcDI1NmsxoQLdUv9Eo9sxCt0tc_CheLOWnX59yHJtkBSOL7kpxdJ6GYN1ZHCCIyiEdWRwNoIjKA # 83.229.71.210 | kamatera-telaviv-israel \ No newline at end of file +- enr:-KG4QPUf8-g_jU-KrwzG42AGt0wWM1BTnQxgZXlvCEIfTQ5hSmptkmgmMbRkpOqv6kzb33SlhPHJp7x4rLWWiVq5lSECgmlkgnY0gmlwhFPlR9KDaXA2kCoGxcAJAAAVAAAAAAAAABCJc2VjcDI1NmsxoQLdUv9Eo9sxCt0tc_CheLOWnX59yHJtkBSOL7kpxdJ6GYN1ZHCCIyiEdWRwNoIjKA # 83.229.71.210 | kamatera-telaviv-israel From edba56b9a654cea555cb0db2e8d0712525686973 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 5 Feb 2026 15:40:20 +1100 Subject: [PATCH 05/13] Release v8.1.0 (#8749) Closes #8681 Co-Authored-By: Jimmy Chen --- Cargo.lock | 14 +++++++------- Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 913382fe66..8748be726c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "account_manager" -version = "8.0.1" +version = "8.1.0" dependencies = [ "account_utils", "bls", @@ -1276,7 +1276,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "8.0.1" +version = "8.1.0" dependencies = [ "account_utils", "beacon_chain", @@ -1513,7 +1513,7 @@ dependencies = [ [[package]] name = "boot_node" -version = "8.0.1" +version = "8.1.0" dependencies = [ "beacon_node", "bytes", @@ -4897,7 +4897,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "8.0.1" +version = "8.1.0" dependencies = [ "account_utils", "beacon_chain", @@ -5383,7 +5383,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "8.0.1" +version = "8.1.0" dependencies = [ "account_manager", "account_utils", @@ -5515,7 +5515,7 @@ dependencies = [ [[package]] name = "lighthouse_version" -version = "8.0.1" +version = "8.1.0" dependencies = [ "regex", ] @@ -9622,7 +9622,7 @@ dependencies = [ [[package]] name = "validator_client" -version = "8.0.1" +version = "8.1.0" dependencies = [ "account_utils", "beacon_node_fallback", diff --git a/Cargo.toml b/Cargo.toml index 78c63875d3..aac26e060b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ resolver = "2" [workspace.package] edition = "2024" -version = "8.0.1" +version = "8.1.0" [workspace.dependencies] account_utils = { path = "common/account_utils" } From bb133d510d8050861d058318b335a2f539876bfd Mon Sep 17 00:00:00 2001 From: Mac L Date: Fri, 6 Feb 2026 18:16:53 +0400 Subject: [PATCH 06/13] Update `time` to fix `cargo audit` failure (#8764) Update `time` to fix [ RUSTSEC-2026-0009 ](https://rustsec.org/advisories/RUSTSEC-2026-0009.html) Co-Authored-By: Mac L --- Cargo.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 913382fe66..c0b407df71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6323,9 +6323,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -8899,30 +8899,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", From e4bc6500978eeda106f31e0e8017124ff0ac0bd3 Mon Sep 17 00:00:00 2001 From: Mac L Date: Fri, 6 Feb 2026 19:40:09 +0400 Subject: [PATCH 07/13] Use `if-addrs` instead of `local_ip_address` (#8659) Swaps out the `local_ip_address` dependency for `if-addrs`. The reason for this is that is that `local_ip_address` is a relatively heavy dependency (depends on `neli`) compared to `if-addrs` and we only use it to check the presence of an IPv6 interface. This is an experiment to see if we can use the more lightweight `if-addrs` instead. Co-Authored-By: Mac L --- Cargo.lock | 98 +++----------------- beacon_node/lighthouse_network/Cargo.toml | 2 +- beacon_node/lighthouse_network/src/config.rs | 10 +- 3 files changed, 18 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c0b407df71..7651e5b99d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2552,37 +2552,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn 2.0.111", -] - [[package]] name = "derive_more" version = "0.99.20" @@ -3810,18 +3779,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "getset" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" -dependencies = [ - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "ghash" version = "0.5.1" @@ -4552,6 +4509,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "if-addrs" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf39cc0423ee66021dc5eccface85580e4a001e0c5288bae8bea7ecb69225e90" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "if-watch" version = "3.2.1" @@ -4562,7 +4529,7 @@ dependencies = [ "core-foundation 0.9.4", "fnv", "futures", - "if-addrs", + "if-addrs 0.10.2", "ipnet", "log", "netlink-packet-core", @@ -5453,11 +5420,11 @@ dependencies = [ "fnv", "futures", "hex", + "if-addrs 0.14.0", "itertools 0.10.5", "libp2p", "libp2p-mplex", "lighthouse_version", - "local-ip-address", "logging", "lru 0.12.5", "lru_cache", @@ -5559,18 +5526,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "local-ip-address" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a60bf300a990b2d1ebdde4228e873e8e4da40d834adbf5265f3da1457ede652" -dependencies = [ - "libc", - "neli", - "thiserror 2.0.17", - "windows-sys 0.61.2", -] - [[package]] name = "lock_api" version = "0.4.14" @@ -6048,35 +6003,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "neli" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e23bebbf3e157c402c4d5ee113233e5e0610cc27453b2f07eefce649c7365dcc" -dependencies = [ - "bitflags 2.10.0", - "byteorder", - "derive_builder", - "getset", - "libc", - "log", - "neli-proc-macros", - "parking_lot", -] - -[[package]] -name = "neli-proc-macros" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d8d08c6e98f20a62417478ebf7be8e1425ec9acecc6f63e22da633f6b71609" -dependencies = [ - "either", - "proc-macro2", - "quote", - "serde", - "syn 2.0.111", -] - [[package]] name = "netlink-packet-core" version = "0.7.0" diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index eb0cc2cc99..659886f0f1 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -22,11 +22,11 @@ fixed_bytes = { workspace = true } fnv = { workspace = true } futures = { workspace = true } hex = { workspace = true } +if-addrs = "0.14" itertools = { workspace = true } libp2p = { workspace = true } libp2p-mplex = { git = "https://github.com/libp2p/rust-libp2p.git" } lighthouse_version = { workspace = true } -local-ip-address = "0.6" logging = { workspace = true } lru = { workspace = true } lru_cache = { workspace = true } diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index 9940cb9f7f..cb94bfff22 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -5,8 +5,8 @@ use crate::{Enr, PeerIdSerialized}; use directory::{ DEFAULT_BEACON_NODE_DIR, DEFAULT_HARDCODED_NETWORK, DEFAULT_NETWORK_DIR, DEFAULT_ROOT_DIR, }; +use if_addrs::get_if_addrs; use libp2p::{Multiaddr, gossipsub}; -use local_ip_address::local_ipv6; use network_utils::listen_addr::{ListenAddr, ListenAddress}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -262,13 +262,13 @@ impl Config { /// A helper function to check if the local host has a globally routeable IPv6 address. If so, /// returns true. pub fn is_ipv6_supported() -> bool { - // If IPv6 is supported - let Ok(std::net::IpAddr::V6(local_ip)) = local_ipv6() else { + let Ok(addrs) = get_if_addrs() else { return false; }; - // If its globally routable, return true - is_global_ipv6(&local_ip) + addrs.iter().any( + |iface| matches!(iface.addr, if_addrs::IfAddr::V6(ref v6) if is_global_ipv6(&v6.ip)), + ) } pub fn listen_addrs(&self) -> &ListenAddress { From 2ad02510cdc432f653dfdc9b25732f235f71ede6 Mon Sep 17 00:00:00 2001 From: Mac L Date: Sat, 7 Feb 2026 05:18:04 +0400 Subject: [PATCH 08/13] Remove `syn` version `1` (#8678) #8547 This updates a few of our crates to remove the older `syn 1` crate. This updates: - `criterion` -> `0.8` - `itertools` -> `0.14` And also certain `sigp` crates: - `xdelta3` -> [`fe39066`](https://github.com/sigp/xdelta3-rs/commit/fe3906605c87b6c0515bd7c8fc671f47875e3ccc) - `superstruct` -> `0.10.1` - `ethereum_ssz` -> `0.10.1` - `tree_hash` -> `0.12.1` - `metastruct` -> `0.1.4` - `context_deserialize` -> `0.2.1` - `compare_fields` -> `0.1.1` Co-Authored-By: Mac L --- Cargo.lock | 310 +++++++----------- Cargo.toml | 6 +- beacon_node/beacon_chain/benches/benches.rs | 3 +- .../swap_or_not_shuffle/benches/benches.rs | 3 +- consensus/types/benches/benches.rs | 3 +- deny.toml | 1 + 6 files changed, 136 insertions(+), 190 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7651e5b99d..0d04537129 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,6 +111,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -1031,7 +1040,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.2", + "rustix", "slab", "windows-sys 0.61.2", ] @@ -1232,7 +1241,7 @@ dependencies = [ "genesis", "hex", "int_to_bytes", - "itertools 0.10.5", + "itertools 0.14.0", "kzg", "lighthouse_version", "logging", @@ -1315,7 +1324,7 @@ dependencies = [ "clap", "eth2", "futures", - "itertools 0.10.5", + "itertools 0.14.0", "sensitive_url", "serde", "slot_clock", @@ -1334,7 +1343,7 @@ version = "0.1.0" dependencies = [ "fnv", "futures", - "itertools 0.10.5", + "itertools 0.14.0", "lighthouse_network", "logging", "metrics", @@ -1371,15 +1380,32 @@ dependencies = [ "itertools 0.12.1", "lazy_static", "lazycell", - "log", - "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash 1.1.0", "shlex", "syn 2.0.111", - "which", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.1", + "shlex", + "syn 2.0.111", ] [[package]] @@ -1807,7 +1833,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", "terminal_size", ] @@ -1914,9 +1940,9 @@ dependencies = [ [[package]] name = "compare_fields" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05162add7c8618791829528194a271dca93f69194d35b19db1ca7fbfb8275278" +checksum = "f6f45d0b4d61b582303179fb7a1a142bc9d647b7583db3b0d5f25a21d286fab9" dependencies = [ "compare_fields_derive", "itertools 0.14.0", @@ -1924,12 +1950,12 @@ dependencies = [ [[package]] name = "compare_fields_derive" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ee468b2e568b668e2a686112935e7bbe9a81bf4fa6b9f6fc3410ea45fb7ce" +checksum = "92ff1dbbda10d495b2c92749c002b2025e0be98f42d1741ecc9ff820d2f04dce" dependencies = [ "quote", - "syn 1.0.109", + "syn 2.0.111", ] [[package]] @@ -2026,9 +2052,9 @@ dependencies = [ [[package]] name = "context_deserialize" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5f9ea0a0ae2de4943f5ca71590b6dbd0b952475f0a0cafb30a470cec78c8b9" +checksum = "4c523eea4af094b5970c321f4604abc42c5549d3cbae332e98325403fbbdbf70" dependencies = [ "context_deserialize_derive", "serde", @@ -2036,12 +2062,12 @@ dependencies = [ [[package]] name = "context_deserialize_derive" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c57b2db1e4e3ed804dcc49894a144b68fe6c754b8f545eb1dda7ad3c7dbe7e6" +checksum = "3b7bf98c48ffa511b14bb3c76202c24a8742cea1efa9570391c5d41373419a09" dependencies = [ "quote", - "syn 1.0.109", + "syn 2.0.111", ] [[package]] @@ -2129,25 +2155,24 @@ dependencies = [ [[package]] name = "criterion" -version = "0.5.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ + "alloca", "anes", "cast", "ciborium", "clap", "criterion-plot", - "is-terminal", - "itertools 0.10.5", + "itertools 0.13.0", "num-traits", - "once_cell", "oorandom", + "page_size", "plotters", "rayon", "regex", "serde", - "serde_derive", "serde_json", "tinytemplate", "walkdir", @@ -2155,12 +2180,12 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.5.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", - "itertools 0.10.5", + "itertools 0.13.0", ] [[package]] @@ -2279,26 +2304,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "darling" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" -dependencies = [ - "darling_core 0.13.4", - "darling_macro 0.13.4", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - [[package]] name = "darling" version = "0.21.3" @@ -2310,31 +2315,13 @@ dependencies = [ ] [[package]] -name = "darling_core" -version = "0.13.4" +name = "darling" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn 1.0.109", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.111", + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -2348,29 +2335,20 @@ dependencies = [ "proc-macro2", "quote", "serde", - "strsim 0.11.1", + "strsim", "syn 2.0.111", ] [[package]] -name = "darling_macro" -version = "0.13.4" +name = "darling_core" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "darling_core 0.13.4", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", + "ident_case", + "proc-macro2", "quote", + "strsim", "syn 2.0.111", ] @@ -2385,6 +2363,17 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.111", +] + [[package]] name = "darwin-libproc" version = "0.1.2" @@ -3232,15 +3221,15 @@ dependencies = [ [[package]] name = "ethereum_ssz" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8cd8c4f47dfb947dbfe3cdf2945ae1da808dbedc592668658e827a12659ba1" +checksum = "2128a84f7a3850d54ee343334e3392cca61f9f6aa9441eec481b9394b43c238b" dependencies = [ "alloy-primitives", "arbitrary", "context_deserialize", "ethereum_serde_utils", - "itertools 0.13.0", + "itertools 0.14.0", "serde", "serde_derive", "smallvec", @@ -3249,11 +3238,11 @@ dependencies = [ [[package]] name = "ethereum_ssz_derive" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78d247bc40823c365a62e572441a8f8b12df03f171713f06bc76180fcd56ab71" +checksum = "cd596f91cff004fc8d02be44c21c0f9b93140a04b66027ae052f5f8e05b48eba" dependencies = [ - "darling 0.20.11", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.111", @@ -4094,15 +4083,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "http" version = "0.2.12" @@ -4688,17 +4668,6 @@ dependencies = [ "serde", ] -[[package]] -name = "is-terminal" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -5421,7 +5390,7 @@ dependencies = [ "futures", "hex", "if-addrs 0.14.0", - "itertools 0.10.5", + "itertools 0.14.0", "libp2p", "libp2p-mplex", "lighthouse_version", @@ -5487,12 +5456,6 @@ dependencies = [ "regex", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -5650,13 +5613,13 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "match-lookup" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" +checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.111", ] [[package]] @@ -5685,7 +5648,7 @@ name = "mdbx-sys" version = "0.11.6-4" source = "git+https://github.com/sigp/libmdbx-rs?rev=e6ff4b9377c1619bcf0bfdf52bee5a980a432a1a#e6ff4b9377c1619bcf0bfdf52bee5a980a432a1a" dependencies = [ - "bindgen", + "bindgen 0.69.5", "cc", "cmake", "libc", @@ -5725,25 +5688,25 @@ dependencies = [ [[package]] name = "metastruct" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d74f54f231f9a18d77393ecc5cc7ab96709b2a61ee326c2b2b291009b0cc5a07" +checksum = "969a1be9bd80794bdf93b23ab552c2ec6f3e83b33164824553fd996cdad513b8" dependencies = [ "metastruct_macro", ] [[package]] name = "metastruct_macro" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7225f3a4dfbec47a0c6a730a874185fda840d365d7bbd6ba199dd81796d5" +checksum = "de9164f767d73a507c19205868c84da411dc7795f4bdabf497d3dd93cfef9930" dependencies = [ - "darling 0.13.4", - "itertools 0.10.5", + "darling 0.23.0", + "itertools 0.14.0", "proc-macro2", "quote", "smallvec", - "syn 1.0.109", + "syn 2.0.111", ] [[package]] @@ -6090,7 +6053,7 @@ dependencies = [ "genesis", "hex", "igd-next", - "itertools 0.10.5", + "itertools 0.14.0", "k256", "kzg", "libp2p", @@ -6526,7 +6489,7 @@ dependencies = [ "ethereum_ssz", "ethereum_ssz_derive", "fixed_bytes", - "itertools 0.10.5", + "itertools 0.14.0", "maplit", "metrics", "parking_lot", @@ -6541,6 +6504,16 @@ dependencies = [ "types", ] +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "pairing" version = "0.23.0" @@ -6741,7 +6714,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.2", + "rustix", "windows-sys 0.61.2", ] @@ -6901,7 +6874,7 @@ checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" dependencies = [ "bitflags 2.10.0", "procfs-core", - "rustix 1.1.2", + "rustix", ] [[package]] @@ -7619,19 +7592,6 @@ dependencies = [ "nom", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - [[package]] name = "rustix" version = "1.1.2" @@ -7641,7 +7601,7 @@ dependencies = [ "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys", "windows-sys 0.61.2", ] @@ -8426,7 +8386,7 @@ dependencies = [ "fixed_bytes", "int_to_bytes", "integer-sqrt", - "itertools 0.10.5", + "itertools 0.14.0", "merkle_proof", "metrics", "milhouse", @@ -8474,7 +8434,7 @@ dependencies = [ "ethereum_ssz", "ethereum_ssz_derive", "fixed_bytes", - "itertools 0.10.5", + "itertools 0.14.0", "leveldb", "logging", "lru 0.12.5", @@ -8499,12 +8459,6 @@ dependencies = [ "zstd", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -8540,12 +8494,12 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "superstruct" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b986e4a629907f20a2c2a639a75bc22a8b5d99b444e0d83c395f4cb309022bf" +checksum = "bae4a9ccd7882533c1f210e400763ec6ee64c390fc12248c238276281863719e" dependencies = [ - "darling 0.20.11", - "itertools 0.13.0", + "darling 0.23.0", + "itertools 0.14.0", "proc-macro2", "quote", "smallvec", @@ -8706,7 +8660,7 @@ dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix 1.1.2", + "rustix", "windows-sys 0.61.2", ] @@ -8716,7 +8670,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "rustix 1.1.2", + "rustix", "windows-sys 0.60.2", ] @@ -9285,9 +9239,9 @@ dependencies = [ [[package]] name = "tree_hash" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2db21caa355767db4fd6129876e5ae278a8699f4a6959b1e3e7aff610b532d52" +checksum = "f7fd51aa83d2eb83b04570808430808b5d24fdbf479a4d5ac5dee4a2e2dd2be4" dependencies = [ "alloy-primitives", "ethereum_hashing", @@ -9298,11 +9252,11 @@ dependencies = [ [[package]] name = "tree_hash_derive" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711cc655fcbb48384a87dc2bf641b991a15c5ad9afc3caa0b1ab1df3b436f70f" +checksum = "8840ad4d852e325d3afa7fde8a50b2412f89dce47d7eb291c0cc7f87cd040f38" dependencies = [ - "darling 0.21.3", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.111", @@ -9361,7 +9315,7 @@ dependencies = [ "fixed_bytes", "hex", "int_to_bytes", - "itertools 0.10.5", + "itertools 0.14.0", "kzg", "maplit", "merkle_proof", @@ -9619,7 +9573,7 @@ dependencies = [ "graffiti_file", "health_metrics", "initialized_validators", - "itertools 0.10.5", + "itertools 0.14.0", "lighthouse_validator_store", "lighthouse_version", "logging", @@ -10012,18 +9966,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - [[package]] name = "widestring" version = "0.4.3" @@ -10478,15 +10420,15 @@ dependencies = [ [[package]] name = "xdelta3" version = "0.1.5" -source = "git+https://github.com/sigp/xdelta3-rs?rev=4db64086bb02e9febb584ba93b9d16bb2ae3825a#4db64086bb02e9febb584ba93b9d16bb2ae3825a" +source = "git+https://github.com/sigp/xdelta3-rs?rev=fe3906605c87b6c0515bd7c8fc671f47875e3ccc#fe3906605c87b6c0515bd7c8fc671f47875e3ccc" dependencies = [ - "bindgen", + "bindgen 0.72.1", "cc", "futures-io", "futures-util", "libc", "log", - "rand 0.8.5", + "rand 0.9.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 78c63875d3..4e28b124ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,7 +126,7 @@ clap_utils = { path = "common/clap_utils" } compare_fields = "0.1" console-subscriber = "0.4" context_deserialize = "0.2" -criterion = "0.5" +criterion = "0.8" delay_map = "0.4" deposit_contract = { path = "common/deposit_contract" } directory = { path = "common/directory" } @@ -164,7 +164,7 @@ http_api = { path = "beacon_node/http_api" } hyper = "1" initialized_validators = { path = "validator_client/initialized_validators" } int_to_bytes = { path = "consensus/int_to_bytes" } -itertools = "0.10" +itertools = "0.14" kzg = { path = "crypto/kzg" } libp2p = { git = "https://github.com/libp2p/rust-libp2p.git", default-features = false, features = [ "identify", @@ -286,7 +286,7 @@ validator_test_rig = { path = "testing/validator_test_rig" } warp = { version = "0.3.7", default-features = false, features = ["tls"] } warp_utils = { path = "common/warp_utils" } workspace_members = { path = "common/workspace_members" } -xdelta3 = { git = "https://github.com/sigp/xdelta3-rs", rev = "4db64086bb02e9febb584ba93b9d16bb2ae3825a" } +xdelta3 = { git = "https://github.com/sigp/xdelta3-rs", rev = "fe3906605c87b6c0515bd7c8fc671f47875e3ccc" } zeroize = { version = "1", features = ["zeroize_derive", "serde"] } zip = { version = "6.0", default-features = false, features = ["deflate"] } zstd = "0.13" diff --git a/beacon_node/beacon_chain/benches/benches.rs b/beacon_node/beacon_chain/benches/benches.rs index 0d4040155d..e71a19d8c1 100644 --- a/beacon_node/beacon_chain/benches/benches.rs +++ b/beacon_node/beacon_chain/benches/benches.rs @@ -1,8 +1,9 @@ +use std::hint::black_box; use std::sync::Arc; use beacon_chain::kzg_utils::{blobs_to_data_column_sidecars, reconstruct_data_columns}; use beacon_chain::test_utils::get_kzg; -use criterion::{Criterion, black_box, criterion_group, criterion_main}; +use criterion::{Criterion, criterion_group, criterion_main}; use bls::Signature; use kzg::{KzgCommitment, KzgProof}; diff --git a/consensus/swap_or_not_shuffle/benches/benches.rs b/consensus/swap_or_not_shuffle/benches/benches.rs index f33556be38..5a9ba38f06 100644 --- a/consensus/swap_or_not_shuffle/benches/benches.rs +++ b/consensus/swap_or_not_shuffle/benches/benches.rs @@ -1,4 +1,5 @@ -use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main}; +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use std::hint::black_box; use swap_or_not_shuffle::{compute_shuffled_index, shuffle_list as fast_shuffle}; const SHUFFLE_ROUND_COUNT: u8 = 90; diff --git a/consensus/types/benches/benches.rs b/consensus/types/benches/benches.rs index 397c33163e..85d7de980b 100644 --- a/consensus/types/benches/benches.rs +++ b/consensus/types/benches/benches.rs @@ -1,8 +1,9 @@ -use criterion::{BatchSize, BenchmarkId, Criterion, black_box, criterion_group, criterion_main}; +use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_main}; use fixed_bytes::FixedBytesExtended; use milhouse::List; use rayon::prelude::*; use ssz::Encode; +use std::hint::black_box; use std::sync::Arc; use types::{ BeaconState, Epoch, Eth1Data, EthSpec, Hash256, MainnetEthSpec, Validator, diff --git a/deny.toml b/deny.toml index 54ede06429..04f2ed30ad 100644 --- a/deny.toml +++ b/deny.toml @@ -16,6 +16,7 @@ deny = [ { crate = "sha2", deny-multiple-versions = true, reason = "takes a long time to compile" }, { crate = "pbkdf2", deny-multiple-versions = true, reason = "takes a long time to compile" }, { crate = "scrypt", deny-multiple-versions = true, reason = "takes a long time to compile" }, + { crate = "syn", deny-multiple-versions = true, reason = "takes a long time to compile" }, ] [sources] From c4437d2927cdcdccbae725e4eef77286d53b2501 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 9 Feb 2026 12:31:50 +1100 Subject: [PATCH 09/13] Fix regression test for #8528 (#8771) I accidentally broke `unstable` while merging some missed commits from `release-v8.0`. The merge was clean but semantically broken, and I didn't notice because I pushed without running CI :grimacing: - Fix the regression test added for #8528, for compatibility with the recent `RpcBlock` changes. I'm passing `is_available = false` which seems correct for this test. Co-Authored-By: Michael Sproul --- beacon_node/beacon_chain/tests/column_verification.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/tests/column_verification.rs b/beacon_node/beacon_chain/tests/column_verification.rs index f048a08bb8..ca9893941a 100644 --- a/beacon_node/beacon_chain/tests/column_verification.rs +++ b/beacon_node/beacon_chain/tests/column_verification.rs @@ -170,7 +170,7 @@ async fn verify_header_signature_fork_block_bug() { // This keeps the head at the pre-fork state (Electra). harness.advance_slot(); let rpc_block = harness - .build_rpc_block_from_blobs(block_root, signed_block.clone(), None) + .build_rpc_block_from_blobs(signed_block.clone(), None, false) .expect("Should build RPC block"); let availability = harness .chain From b8d098685fdeee753ca0a572c2959c8fbd8428b9 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Mon, 9 Feb 2026 10:53:44 +0530 Subject: [PATCH 10/13] Record metrics for only valid gossip blocks (#8723) N/A Fixes the issue where we were setting block observed timings for blocks that were potentially gossip invalid. Thanks @gitToki for the find Co-Authored-By: Pawan Dhananjay Co-Authored-By: Michael Sproul --- .../gossip_methods.rs | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 6193725323..a4125f3df0 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1214,28 +1214,25 @@ impl NetworkBeaconProcessor { .verify_block_for_gossip(block.clone()) .await; - if verification_result.is_ok() { + let block_root = if let Ok(verified_block) = &verification_result { metrics::set_gauge( &metrics::BEACON_BLOCK_DELAY_GOSSIP, block_delay.as_millis() as i64, ); - } - - let block_root = if let Ok(verified_block) = &verification_result { + // Write the time the block was observed into delay cache only for gossip + // valid blocks. + self.chain.block_times_cache.write().set_time_observed( + verified_block.block_root, + block.slot(), + seen_duration, + Some(peer_id.to_string()), + Some(peer_client.to_string()), + ); verified_block.block_root } else { block.canonical_root() }; - // Write the time the block was observed into delay cache. - self.chain.block_times_cache.write().set_time_observed( - block_root, - block.slot(), - seen_duration, - Some(peer_id.to_string()), - Some(peer_client.to_string()), - ); - let verified_block = match verification_result { Ok(verified_block) => { if block_delay >= self.chain.spec.get_unaggregated_attestation_due() { From f8cfaa4251bc3c5de956e45dd7d5204ed693eae1 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 9 Feb 2026 16:26:34 +1100 Subject: [PATCH 11/13] Gloas consensus logic for attestations (#8760) Co-Authored-By: Michael Sproul --- .../common/get_attestation_participation.rs | 44 ++++- .../src/per_block_processing/errors.rs | 4 + .../process_operations.rs | 152 +++++++++++++++++- .../verify_attestation.rs | 7 +- consensus/types/src/state/beacon_state.rs | 3 + testing/ef_tests/check_all_files_accessed.py | 1 - testing/ef_tests/src/cases/operations.rs | 18 ++- testing/ef_tests/src/handler.rs | 4 +- 8 files changed, 219 insertions(+), 14 deletions(-) diff --git a/consensus/state_processing/src/common/get_attestation_participation.rs b/consensus/state_processing/src/common/get_attestation_participation.rs index 71bf6329f1..2262b59ac1 100644 --- a/consensus/state_processing/src/common/get_attestation_participation.rs +++ b/consensus/state_processing/src/common/get_attestation_participation.rs @@ -1,8 +1,8 @@ use integer_sqrt::IntegerSquareRoot; +use safe_arith::SafeArith; use smallvec::SmallVec; -use types::{AttestationData, BeaconState, ChainSpec, EthSpec}; use types::{ - BeaconStateError as Error, + AttestationData, BeaconState, BeaconStateError as Error, ChainSpec, EthSpec, consts::altair::{ NUM_FLAG_INDICES, TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, @@ -16,6 +16,8 @@ use types::{ /// /// This function will return an error if the source of the attestation doesn't match the /// state's relevant justified checkpoint. +/// +/// This function has been abstracted to work for all forks from Altair to Gloas. pub fn get_attestation_participation_flag_indices( state: &BeaconState, data: &AttestationData, @@ -27,13 +29,43 @@ pub fn get_attestation_participation_flag_indices( } else { state.previous_justified_checkpoint() }; - - // Matching roots. let is_matching_source = data.source == justified_checkpoint; + + // Matching target. let is_matching_target = is_matching_source && data.target.root == *state.get_block_root_at_epoch(data.target.epoch)?; - let is_matching_head = - is_matching_target && data.beacon_block_root == *state.get_block_root(data.slot)?; + + // [New in Gloas:EIP7732] + let payload_matches = if state.fork_name_unchecked().gloas_enabled() { + if state.is_attestation_same_slot(data)? { + // For same-slot attestations, data.index must be 0 + if data.index != 0 { + return Err(Error::BadOverloadedDataIndex(data.index)); + } + true + } else { + // For non same-slot attestations, check execution payload availability + let slot_index = data + .slot + .as_usize() + .safe_rem(E::slots_per_historical_root())?; + let payload_index = state + .execution_payload_availability()? + .get(slot_index) + .map(|avail| if avail { 1 } else { 0 }) + .map_err(|_| Error::InvalidExecutionPayloadAvailabilityIndex(slot_index))?; + data.index == payload_index + } + } else { + // Essentially `payload_matches` is always true pre-Gloas (it is not considered for matching + // head). + true + }; + + // Matching head. + let is_matching_head = is_matching_target + && data.beacon_block_root == *state.get_block_root(data.slot)? + && payload_matches; if !is_matching_source { return Err(Error::IncorrectAttestationSource); diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index d0cf7b46d9..5c1db9d732 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -99,6 +99,8 @@ pub enum BlockProcessingError { IncorrectExpectedWithdrawalsVariant, MissingLastWithdrawal, PendingAttestationInElectra, + /// Builder payment index out of bounds (Gloas) + BuilderPaymentIndexOutOfBounds(usize), } impl From for BlockProcessingError { @@ -372,6 +374,8 @@ pub enum AttestationInvalid { BadSignature, /// The indexed attestation created from this attestation was found to be invalid. BadIndexedAttestation(IndexedAttestationInvalid), + /// The overloaded "data.index" field is invalid (post-Gloas). + BadOverloadedDataIndex, } impl From> diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index 8afeeb685b..9db439b543 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -212,6 +212,148 @@ pub mod altair_deneb { } } +pub mod gloas { + use super::*; + use crate::common::update_progressive_balances_cache::update_progressive_balances_on_attestation; + + pub fn process_attestations<'a, E: EthSpec, I>( + state: &mut BeaconState, + attestations: I, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> + where + I: Iterator>, + { + attestations.enumerate().try_for_each(|(i, attestation)| { + process_attestation(state, attestation, i, ctxt, verify_signatures, spec) + }) + } + + pub fn process_attestation( + state: &mut BeaconState, + attestation: AttestationRef, + att_index: usize, + ctxt: &mut ConsensusContext, + verify_signatures: VerifySignatures, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> { + let proposer_index = ctxt.get_proposer_index(state, spec)?; + let previous_epoch = ctxt.previous_epoch; + let current_epoch = ctxt.current_epoch; + + let indexed_att = verify_attestation_for_block_inclusion( + state, + attestation, + ctxt, + verify_signatures, + spec, + ) + .map_err(|e| e.into_with_index(att_index))?; + + // Matching roots, participation flag indices + let data = attestation.data(); + let inclusion_delay = state.slot().safe_sub(data.slot)?.as_u64(); + let participation_flag_indices = + get_attestation_participation_flag_indices(state, data, inclusion_delay, spec)?; + + // [New in EIP-7732] + let current_epoch_target = data.target.epoch == state.current_epoch(); + let slot_mod = data + .slot + .as_usize() + .safe_rem(E::slots_per_epoch() as usize)?; + let payment_index = if current_epoch_target { + (E::slots_per_epoch() as usize).safe_add(slot_mod)? + } else { + slot_mod + }; + // Cached here to avoid repeat lookups. The withdrawal amount is immutable throughout + // this whole function. + let payment_withdrawal_amount = state + .builder_pending_payments()? + .get(payment_index) + .ok_or(BlockProcessingError::BuilderPaymentIndexOutOfBounds( + payment_index, + ))? + .withdrawal + .amount; + + // Update epoch participation flags. + let mut proposer_reward_numerator = 0; + for index in indexed_att.attesting_indices_iter() { + let index = *index as usize; + + let validator_effective_balance = state.epoch_cache().get_effective_balance(index)?; + let validator_slashed = state.slashings_cache().is_slashed(index); + + // [New in Gloas:EIP7732] + // For same-slot attestations, check if we're setting any new flags + // If we are, this validator hasn't contributed to this slot's quorum yet + let mut will_set_new_flag = false; + + for (flag_index, &weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() { + let epoch_participation = state.get_epoch_participation_mut( + data.target.epoch, + previous_epoch, + current_epoch, + )?; + + if participation_flag_indices.contains(&flag_index) { + let validator_participation = epoch_participation + .get_mut(index) + .ok_or(BeaconStateError::ParticipationOutOfBounds(index))?; + + if !validator_participation.has_flag(flag_index)? { + validator_participation.add_flag(flag_index)?; + proposer_reward_numerator + .safe_add_assign(state.get_base_reward(index)?.safe_mul(weight)?)?; + will_set_new_flag = true; + + update_progressive_balances_on_attestation( + state, + data.target.epoch, + flag_index, + validator_effective_balance, + validator_slashed, + )?; + } + } + } + + // [New in Gloas:EIP7732] + // Add weight for same-slot attestations when any new flag is set. + // This ensures each validator contributes exactly once per slot. + if will_set_new_flag + && state.is_attestation_same_slot(data)? + && payment_withdrawal_amount > 0 + { + let builder_payments = state.builder_pending_payments_mut()?; + let payment = builder_payments.get_mut(payment_index).ok_or( + BlockProcessingError::BuilderPaymentIndexOutOfBounds(payment_index), + )?; + payment + .weight + .safe_add_assign(validator_effective_balance)?; + } + } + + let proposer_reward_denominator = WEIGHT_DENOMINATOR + .safe_sub(PROPOSER_WEIGHT)? + .safe_mul(WEIGHT_DENOMINATOR)? + .safe_div(PROPOSER_WEIGHT)?; + let proposer_reward = proposer_reward_numerator.safe_div(proposer_reward_denominator)?; + increase_balance(state, proposer_index as usize, proposer_reward)?; + + // [New in Gloas:EIP7732] + // Update builder payment weight + // No-op, this is done inline above. + + Ok(()) + } +} + /// Validates each `ProposerSlashing` and updates the state, short-circuiting on an invalid object. /// /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns @@ -285,7 +427,15 @@ pub fn process_attestations>( ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - if state.fork_name_unchecked().altair_enabled() { + if state.fork_name_unchecked().gloas_enabled() { + gloas::process_attestations( + state, + block_body.attestations(), + verify_signatures, + ctxt, + spec, + )?; + } else if state.fork_name_unchecked().altair_enabled() { altair_deneb::process_attestations( state, block_body.attestations(), diff --git a/consensus/state_processing/src/per_block_processing/verify_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_attestation.rs index 0d1fd17768..00105f323c 100644 --- a/consensus/state_processing/src/per_block_processing/verify_attestation.rs +++ b/consensus/state_processing/src/per_block_processing/verify_attestation.rs @@ -74,7 +74,12 @@ pub fn verify_attestation_for_state<'ctxt, E: EthSpec>( ); } AttestationRef::Electra(_) => { - verify!(data.index == 0, Invalid::BadCommitteeIndex); + let fork_at_attestation_slot = spec.fork_name_at_slot::(data.slot); + if fork_at_attestation_slot.gloas_enabled() { + verify!(data.index < 2, Invalid::BadOverloadedDataIndex); + } else { + verify!(data.index == 0, Invalid::BadCommitteeIndex); + } } } diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index f661988edb..2720745b01 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -176,6 +176,8 @@ pub enum BeaconStateError { NonExecutionAddressWithdrawalCredential, NoCommitteeFound(CommitteeIndex), InvalidCommitteeIndex(CommitteeIndex), + /// `Attestation.data.index` field is invalid in overloaded data index scenario. + BadOverloadedDataIndex(u64), InvalidSelectionProof { aggregator_index: u64, }, @@ -198,6 +200,7 @@ pub enum BeaconStateError { i: usize, }, InvalidIndicesCount, + InvalidExecutionPayloadAvailabilityIndex(usize), } /// Control whether an epoch-indexed field can be indexed at the next epoch or not. diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index d270817267..ed220fbe8c 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -48,7 +48,6 @@ excluded_paths = [ "tests/.*/eip7732", "tests/.*/eip7805", # TODO(gloas): remove these ignores as more Gloas operations are implemented - "tests/.*/gloas/operations/attestation/.*", "tests/.*/gloas/operations/attester_slashing/.*", "tests/.*/gloas/operations/block_header/.*", "tests/.*/gloas/operations/bls_to_execution_change/.*", diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index e778300879..9133378ac5 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -16,8 +16,9 @@ use state_processing::{ errors::BlockProcessingError, process_block_header, process_execution_payload, process_operations::{ - altair_deneb, base, process_attester_slashings, process_bls_to_execution_changes, - process_deposits, process_exits, process_proposer_slashings, + altair_deneb, base, gloas, process_attester_slashings, + process_bls_to_execution_changes, process_deposits, process_exits, + process_proposer_slashings, }, process_sync_aggregate, withdrawals, }, @@ -98,9 +99,18 @@ impl Operation for Attestation { _: &Operations, ) -> Result<(), BlockProcessingError> { initialize_epoch_cache(state, spec)?; + initialize_progressive_balances_cache(state, spec)?; let mut ctxt = ConsensusContext::new(state.slot()); - if state.fork_name_unchecked().altair_enabled() { - initialize_progressive_balances_cache(state, spec)?; + if state.fork_name_unchecked().gloas_enabled() { + gloas::process_attestation( + state, + self.to_ref(), + 0, + &mut ctxt, + VerifySignatures::True, + spec, + ) + } else if state.fork_name_unchecked().altair_enabled() { altair_deneb::process_attestation( state, self.to_ref(), diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 5af2df33b4..39ddff46e7 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -1137,7 +1137,9 @@ impl> Handler for OperationsHandler fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { // TODO(gloas): So far only withdrawals tests are enabled for Gloas. Self::Case::is_enabled_for_fork(fork_name) - && (!fork_name.gloas_enabled() || self.handler_name() == "withdrawals") + && (!fork_name.gloas_enabled() + || self.handler_name() == "withdrawals" + || self.handler_name() == "attestation") } } From 0c9f97f015108086f8bf3fcf04f89258d36a5310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Oliveira?= Date: Mon, 9 Feb 2026 23:31:49 +0000 Subject: [PATCH 12/13] Remove libp2p multiaddress (#8683) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: João Oliveira Co-Authored-By: ackintosh --- .../lighthouse_network/src/discovery/mod.rs | 87 +++++++++++-------- .../lighthouse_network/src/service/mod.rs | 1 + beacon_node/src/cli.rs | 2 +- beacon_node/src/config.rs | 12 +-- book/src/help_bn.md | 3 +- 5 files changed, 59 insertions(+), 46 deletions(-) diff --git a/beacon_node/lighthouse_network/src/discovery/mod.rs b/beacon_node/lighthouse_network/src/discovery/mod.rs index 939eca3b94..38a6a84b44 100644 --- a/beacon_node/lighthouse_network/src/discovery/mod.rs +++ b/beacon_node/lighthouse_network/src/discovery/mod.rs @@ -264,47 +264,62 @@ impl Discovery { info!("Contacting Multiaddr boot-nodes for their ENR"); } - // get futures for requesting the Enrs associated to these multiaddr and wait for their + // get futures for requesting the ENRs associated to these multiaddr and wait for their // completion - let mut fut_coll = config + let discv5_eligible_addrs = config .boot_nodes_multiaddr .iter() - .map(|addr| addr.to_string()) - // request the ENR for this multiaddr and keep the original for logging - .map(|addr| { - futures::future::join( - discv5.request_enr(addr.clone()), - futures::future::ready(addr), - ) - }) - .collect::>(); + // Filter out multiaddrs without UDP or P2P protocols required for discv5 ENR requests + .filter(|addr| { + addr.iter().any(|proto| matches!(proto, Protocol::Udp(_))) + && addr.iter().any(|proto| matches!(proto, Protocol::P2p(_))) + }); - while let Some((result, original_addr)) = fut_coll.next().await { - match result { - Ok(enr) => { - debug!( - node_id = %enr.node_id(), - peer_id = %enr.peer_id(), - ip4 = ?enr.ip4(), - udp4 = ?enr.udp4(), - tcp4 = ?enr.tcp4(), - quic4 = ?enr.quic4(), - "Adding node to routing table" - ); - let _ = discv5.add_enr(enr).map_err(|e| { - error!( - addr = original_addr.to_string(), - error = e.to_string(), - "Could not add peer to the local routing table" - ) - }); - } - Err(e) => { - error!( - multiaddr = original_addr.to_string(), - error = e.to_string(), - "Error getting mapping to ENR" + if config.disable_discovery { + if discv5_eligible_addrs.count() > 0 { + warn!( + "Boot node multiaddrs requiring discv5 ENR lookup will be ignored because discovery is disabled" + ); + } + } else { + let mut fut_coll = discv5_eligible_addrs + .map(|addr| addr.to_string()) + // request the ENR for this multiaddr and keep the original for logging + .map(|addr| { + futures::future::join( + discv5.request_enr(addr.clone()), + futures::future::ready(addr), ) + }) + .collect::>(); + + while let Some((result, original_addr)) = fut_coll.next().await { + match result { + Ok(enr) => { + debug!( + node_id = %enr.node_id(), + peer_id = %enr.peer_id(), + ip4 = ?enr.ip4(), + udp4 = ?enr.udp4(), + tcp4 = ?enr.tcp4(), + quic4 = ?enr.quic4(), + "Adding node to routing table" + ); + let _ = discv5.add_enr(enr).map_err(|e| { + error!( + addr = original_addr.to_string(), + error = e.to_string(), + "Could not add peer to the local routing table" + ) + }); + } + Err(e) => { + error!( + multiaddr = original_addr.to_string(), + error = e.to_string(), + "Error getting mapping to ENR" + ) + } } } } diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 74b1fb4b98..3d709ed9b5 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -573,6 +573,7 @@ impl Network { }; // attempt to connect to user-input libp2p nodes + // DEPRECATED: can be removed in v8.2.0./v9.0.0 for multiaddr in &config.libp2p_nodes { dial(multiaddr.clone()); } diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index e4c7c6ff1f..9553fe60ba 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -364,7 +364,7 @@ pub fn cli_app() -> Command { .long("libp2p-addresses") .value_name("MULTIADDR") .help("One or more comma-delimited multiaddrs to manually connect to a libp2p peer \ - without an ENR.") + without an ENR. DEPRECATED. The --libp2p-addresses flag is deprecated and replaced by --boot-nodes") .action(ArgAction::Set) .display_order(0) ) diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 2e5a045502..752cf10550 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -15,7 +15,7 @@ use directory::{DEFAULT_BEACON_NODE_DIR, DEFAULT_NETWORK_DIR, DEFAULT_ROOT_DIR}; use environment::RuntimeContext; use execution_layer::DEFAULT_JWT_FILE; use http_api::TlsConfig; -use lighthouse_network::{Enr, Multiaddr, NetworkConfig, PeerIdSerialized, multiaddr::Protocol}; +use lighthouse_network::{Enr, Multiaddr, NetworkConfig, PeerIdSerialized}; use network_utils::listen_addr::ListenAddress; use sensitive_url::SensitiveUrl; use std::collections::HashSet; @@ -28,7 +28,7 @@ use std::num::NonZeroU16; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::time::Duration; -use tracing::{error, info, warn}; +use tracing::{info, warn}; use types::graffiti::GraffitiString; use types::{Checkpoint, Epoch, EthSpec, Hash256}; @@ -1193,12 +1193,6 @@ pub fn set_network_config( let multi: Multiaddr = addr .parse() .map_err(|_| format!("Not valid as ENR nor Multiaddr: {}", addr))?; - if !multi.iter().any(|proto| matches!(proto, Protocol::Udp(_))) { - error!(multiaddr = multi.to_string(), "Missing UDP in Multiaddr"); - } - if !multi.iter().any(|proto| matches!(proto, Protocol::P2p(_))) { - error!(multiaddr = multi.to_string(), "Missing P2P in Multiaddr"); - } multiaddrs.push(multi); } } @@ -1207,7 +1201,9 @@ pub fn set_network_config( config.boot_nodes_multiaddr = multiaddrs; } + // DEPRECATED: can be removed in v8.2.0./v9.0.0 if let Some(libp2p_addresses_str) = cli_args.get_one::("libp2p-addresses") { + warn!("The --libp2p-addresses flag is deprecated and replaced by --boot-nodes"); config.libp2p_nodes = libp2p_addresses_str .split(',') .map(|multiaddr| { diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 5f3c43a7e4..d3aa27c8a7 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -225,7 +225,8 @@ Options: be careful to avoid filling up their disks. --libp2p-addresses One or more comma-delimited multiaddrs to manually connect to a libp2p - peer without an ENR. + peer without an ENR. DEPRECATED. The --libp2p-addresses flag is + deprecated and replaced by --boot-nodes --listen-address [
...] The address lighthouse will listen for UDP and TCP connections. To listen over IPv4 and IPv6 set this flag twice with the different From 286b67f0484a1986e28462952f5edc13e39beef4 Mon Sep 17 00:00:00 2001 From: Mac L Date: Tue, 10 Feb 2026 06:10:48 +0400 Subject: [PATCH 13/13] Remove dependency on OpenSSL (#8768) https://github.com/sigp/lighthouse/issues/8756 Only the Web3Signer actually needs OpenSSL in order to parse PKCS12 certificates. This updates the function to instead manually parse the cert (using the `p12-keystore` crate) and converts it to a `PEM` certificate (using the `pem` crate) which can be directly converted to a `reqwest::tls::Identity` as this can be done directly in `rustls`. Co-Authored-By: Mac L --- Cargo.lock | 268 ++++++++++-------- Cargo.toml | 1 - deny.toml | 1 + testing/web3signer_tests/src/lib.rs | 6 +- testing/web3signer_tests/tls/generate.sh | 8 - .../tls/lighthouse/key_legacy.p12 | Bin 4221 -> 0 bytes .../initialized_validators/Cargo.toml | 2 + .../initialized_validators/src/lib.rs | 25 +- 8 files changed, 173 insertions(+), 138 deletions(-) delete mode 100644 testing/web3signer_tests/tls/lighthouse/key_legacy.p12 diff --git a/Cargo.lock b/Cargo.lock index e12c180f27..69204ccaec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1481,6 +1481,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -1694,6 +1703,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.49" @@ -1923,6 +1941,18 @@ dependencies = [ "cc", ] +[[package]] +name = "cms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b77c319abfd5219629c45c34c89ba945ed3c5e49fcde9d16b6c3885f118a730" +dependencies = [ + "const-oid", + "der", + "spki", + "x509-cert", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -2492,6 +2522,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", + "der_derive", + "flagset", + "pem-rfc7468", "zeroize", ] @@ -2509,6 +2542,17 @@ dependencies = [ "rusticata-macros", ] +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "deranged" version = "0.5.5" @@ -2577,6 +2621,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher", +] + [[package]] name = "digest" version = "0.9.0" @@ -3486,6 +3539,12 @@ dependencies = [ "safe_arith", ] +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + [[package]] name = "flate2" version = "1.1.5" @@ -3516,21 +3575,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "fork_choice" version = "0.1.0" @@ -4307,22 +4351,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.8.1", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.19" @@ -4598,7 +4626,9 @@ dependencies = [ "filesystem", "lockfile", "metrics", + "p12-keystore", "parking_lot", + "pem", "rand 0.9.2", "reqwest", "serde", @@ -4619,6 +4649,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ + "block-padding", "generic-array", ] @@ -5949,23 +5980,6 @@ dependencies = [ "unsigned-varint", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "security-framework-sys", - "tempfile", -] - [[package]] name = "netlink-packet-core" version = "0.7.0" @@ -6350,60 +6364,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "openssl-src" -version = "300.5.4+3.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" -dependencies = [ - "cc", -] - -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "openssl-src", - "pkg-config", - "vcpkg", -] - [[package]] name = "opentelemetry" version = "0.30.0" @@ -6504,6 +6470,29 @@ dependencies = [ "types", ] +[[package]] +name = "p12-keystore" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d55319bae67f92141ce4da80c5392acd3d1323bd8312c1ffdfb018927d07d7" +dependencies = [ + "base64 0.22.1", + "cbc", + "cms", + "der", + "des", + "hex", + "hmac", + "pkcs12", + "pkcs5", + "rand 0.9.2", + "rc2", + "sha1", + "sha2", + "thiserror 2.0.17", + "x509-parser", +] + [[package]] name = "page_size" version = "0.6.0" @@ -6606,6 +6595,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -6654,6 +6652,36 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs12" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b3df3d3cc1015f12d70235e35b6b79befc5fa7a9b95b951eab1dd07c9efc2" +dependencies = [ + "cms", + "const-oid", + "der", + "digest 0.10.7", + "spki", + "x509-cert", + "zeroize", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2", + "scrypt", + "sha2", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -7252,6 +7280,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rc2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c64daa8e9438b84aaae55010a93f396f8e60e3911590fcba770d04643fc1dd" +dependencies = [ + "cipher", +] + [[package]] name = "rcgen" version = "0.13.2" @@ -7359,11 +7396,9 @@ dependencies = [ "http-body-util", "hyper 1.8.1", "hyper-rustls", - "hyper-tls", "hyper-util", "js-sys", "log", - "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -7374,7 +7409,6 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", "tokio-rustls 0.26.4", "tokio-util", "tower 0.5.2", @@ -7643,7 +7677,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.1", + "security-framework", ] [[package]] @@ -7846,19 +7880,6 @@ dependencies = [ "cc", ] -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - [[package]] name = "security-framework" version = "3.5.1" @@ -8909,16 +8930,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.25.0" @@ -10400,6 +10411,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "spki", +] + [[package]] name = "x509-parser" version = "0.17.0" diff --git a/Cargo.toml b/Cargo.toml index fd7ca381e1..100a916c50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -224,7 +224,6 @@ reqwest = { version = "0.12", default-features = false, features = [ "json", "stream", "rustls-tls", - "native-tls-vendored", ] } ring = "0.17" rpds = "0.11" diff --git a/deny.toml b/deny.toml index 04f2ed30ad..e6c30f6a48 100644 --- a/deny.toml +++ b/deny.toml @@ -10,6 +10,7 @@ deny = [ { crate = "protobuf", reason = "use quick-protobuf instead" }, { crate = "derivative", reason = "use educe or derive_more instead" }, { crate = "ark-ff", reason = "present in Cargo.lock but not needed by Lighthouse" }, + { crate = "openssl", reason = "non-Rust dependency, use rustls instead" }, { crate = "strum", deny-multiple-versions = true, reason = "takes a long time to compile" }, { crate = "reqwest", deny-multiple-versions = true, reason = "takes a long time to compile" }, { crate = "aes", deny-multiple-versions = true, reason = "takes a long time to compile" }, diff --git a/testing/web3signer_tests/src/lib.rs b/testing/web3signer_tests/src/lib.rs index 0483f61538..4b9432b67b 100644 --- a/testing/web3signer_tests/src/lib.rs +++ b/testing/web3signer_tests/src/lib.rs @@ -137,11 +137,7 @@ mod tests { } fn client_identity_path() -> PathBuf { - if cfg!(target_os = "macos") { - tls_dir().join("lighthouse").join("key_legacy.p12") - } else { - tls_dir().join("lighthouse").join("key.p12") - } + tls_dir().join("lighthouse").join("key.p12") } fn client_identity_password() -> String { diff --git a/testing/web3signer_tests/tls/generate.sh b/testing/web3signer_tests/tls/generate.sh index 3b14dbddba..31900d5d90 100755 --- a/testing/web3signer_tests/tls/generate.sh +++ b/testing/web3signer_tests/tls/generate.sh @@ -1,12 +1,5 @@ #!/bin/bash -# The lighthouse/key_legacy.p12 file is generated specifically for macOS because the default `openssl pkcs12` encoding -# algorithm in OpenSSL v3 is not compatible with the PKCS algorithm used by the Apple Security Framework. The client -# side (using the reqwest crate) relies on the Apple Security Framework to parse PKCS files. -# We don't need to generate web3signer/key_legacy.p12 because the compatibility issue doesn't occur on the web3signer -# side. It seems that web3signer (Java) uses its own implementation to parse PKCS files. -# See https://github.com/sigp/lighthouse/issues/6442#issuecomment-2469252651 - # We specify `-days 825` when generating the certificate files because Apple requires TLS server certificates to have a # validity period of 825 days or fewer. # See https://github.com/sigp/lighthouse/issues/6442#issuecomment-2474979183 @@ -16,5 +9,4 @@ openssl pkcs12 -export -out web3signer/key.p12 -inkey web3signer/key.key -in web cp web3signer/cert.pem lighthouse/web3signer.pem && openssl req -x509 -sha256 -nodes -days 825 -newkey rsa:4096 -keyout lighthouse/key.key -out lighthouse/cert.pem -config lighthouse/config && openssl pkcs12 -export -out lighthouse/key.p12 -inkey lighthouse/key.key -in lighthouse/cert.pem -password pass:$(cat lighthouse/password.txt) && -openssl pkcs12 -export -legacy -out lighthouse/key_legacy.p12 -inkey lighthouse/key.key -in lighthouse/cert.pem -password pass:$(cat lighthouse/password.txt) && openssl x509 -noout -fingerprint -sha256 -inform pem -in lighthouse/cert.pem | cut -b 20-| sed "s/^/lighthouse /" > web3signer/known_clients.txt diff --git a/testing/web3signer_tests/tls/lighthouse/key_legacy.p12 b/testing/web3signer_tests/tls/lighthouse/key_legacy.p12 deleted file mode 100644 index c3394fae9af893142c035e087fa752c5225eefde..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4221 zcmV-@5Q6V8f)IHE0Ru3C5I+V9Duzgg_YDCD0ic2qFa&}SEHHu)C@_KsUj_*(hDe6@ z4FLxRpn?WaFoFh50s#Opf(Atf2`Yw2hW8Bt2LUh~1_~;MNQU0s;sCfPw}XdgI(fT@^x}(`3PBdm*Ho@y&3CpRAjR6oM;$OHD}Ua=k?g4zRjN z&8&g~)8unA{ALdXZg=g;)g_slEs!#9S%wqAuQ9vt%lJl@;8lFZ&6UT;pa$K>LMD^cWp>u;r*zC(?(!;$|*(0aLS$qhYu zTUgS;*sYz^t`G*GC+Skgl#0|qKv?N;A-OurZSTalB{C-jKhOb8Qg*%==NrW?1Ka?H1<^kC zue}11j^=3pMz#^>B3~l50cex-pzF2+8OR2@G&0Edt)CF?UCE4=0f9|Q6R|uFcjbvM zR_LAYTuTbh`C7H_|DomHrEwW4E4D+eWVkla@DoY2_pUjGAy@4)Wi%b9fsYk0!Q#Ag zY<>;7{uPXBt`%R8`x5>p|K;X@8^Rc`>tj)HetCd0L19U_dJK%G!*TP$Rc_!1sIN>T zD5(N@bD{y~f!>+3S!MN?vZQ@{tddtUB;b1g{(;S!qSd!|$DYcR2M?9{XtT$tV)Xx> ziB3zY%M`h=peo>SQ5)#hy0v#Q>P#sspt2IwAF4o8o0G$oUs=EXZ=%4@2z8kl*j3Ec zVc^gkVWRzuvmsz?%lMR%3EYOkZL5XnR`$D?Q$Iw%O%XAb2Pg_R1;HCIjHZj)1x&*_ zRCC%P)vVbEGfWIw_!6XWXc;afqfFAy#1U49mjnYUj5N^HDS+2r(i==k-YVmjQ z2|m4l^rSSC4Dl=99viQsIxr+UH3))Ww$a>d|C6V$6eU4gHE;j(_ z>p?L&Nm!zGY2vN-?C^B5QJ^1ptFYZ^C*w1t@!WXSjpMOgpN`JhxJ8yBpKV}mSPbyf z*BL79>gSq2=V z#>e_HOKlAOHY4@PUzNxM_T+@VMne>uwus>crfR^LJG1&|88u@r(^Y+;rBf`C!Raid z%PAU;+_Dr^a;*tycT)F-%8$-m4)+8*U>Xe1;k%#E1!jAwmFmBA zvRML7E9?a5oYvr$`|<%EbZcw1cmDKrH>Z_%pGCyVTqTl%V+Mi)FJ6mwVYTq#DFy69 z<*zy*L3T@(W)O#RGwFEY<%jLD&Mt605;axuN*{9n?9dVRy@?15J3JZv&;$bY=%mU%PDmr9;p+I{m9RV z*ZkNIEGA^t-xv%`w+RS?LiL3ZdNODCDyWG>qPtyuF@H7p`*#PPU^2$yLbMe~L=Qsh zgJu`z;rx8@cK_p?vYqz0a7|#ggYFRef;#0BM2J_+A7xU7JmgbKPt}FNP-XT5QMja& zpiqA@fBcb)mmr;xJ@nnOis5axzEay6(F3MqNK#cOYOkB&AlX0E?%f{&LNQU zF1^c{btRsP&MF&>=zph=`XWp8fEfg=+m`t)B+ABMTxUG<7s2BDQ0V!*x#yP1iaSIs zc+rect$YCR%0>qT!|-m&=~mCQB_gjzMgj0& zX4g-Jf>)mb!19Ol=-zK!>{>FjwM1IM>H7{iqu{o58m zMnod1G!^M~+Aw79%$N+1^2P1h4X~Z( ze|&U{tSs#*?3_BwL0bb2>@ARsbz%ODij;z(!*qcp`*8`4ZyJ(F2WSfRGIb@VT`?a6 zt1|Zkg8&LUb)mevsp*NNi$YAfs8xWZq9_uka7i%Z0f7gOa(l^y=*=!Lt1{T~h53 zInjFdRjd}OV}cIA?%5G>Z$F1IVERB+10gvkDvMz3S*>fVKj3Do!JN66CoZAWIaFrl9v}ZyQWt+49R25d1-JHHj+;%k@X&6=g zTB>|F$VQu6hmVK%+mig=b&E#6Ip6t!=Qj8Mxl5ok*_TFLB1Pbl$P^{h^W(^f)eO5oMQaO6L^k&a_}Q2MW}@RY*~$&hvYfAUU$Rx zf;_Z(^dFLOck_sPz+&m~_uBZAG3ors#kvI%8+(Q4I?FjC4F@dg~rgUcf6S~9F z9(_y0Og2|&c_SpwcWg2)grz{gIdV^VhRhse@FG(jU(aKnI?96f$Y zS)5{lTAslO6pn-wN!s4QH$K;Wp{L^)Y^jLu9llms#mBDeJOsM$3B_qVp^@e9dZF&a>wvUpi@of-t^o`(Ncyx#TF{xU zFsmR96e>r;b8X~z8E+)@b&W051yrqQ#C8q2R^xR{7K~2lsfQ zxjKbD?wTJcg?KRmSyP1){X#Q;(jZ;kE8`|4WSXpngK)*xZ~iYd|cMgj<<8RfF6(dK~JmgZT)C5D?}TJBVTDoJWctVL|@vJUM$` z1mSp`juk*69)bLSOfw`4PYJ@iul70hXgGPFguz&@bC^E zjXF?JciTq4MSk<D;-@i=an zH@6H;+Upk=n~$`;(^Q}i7zQ&ws|B*;p(h3iE-+a1>AJ97y<# zQ?B)Li|pKA>!!{JNR-BBQ4>`;q?i7iSnE~W>2KC8-qQaY7+DyIW6w*L9BGU!0B$%< z@DE?9zJ$n%fkRjqW{AreqUz*#E0}FLUCex6{EufX^SR?71_b3&_<`{M^`(E2xQ+f( z;yJNon9073Qx zd!}}`DT*|3Voz3YC8icW=ltaa*o&v#vi>Yy#1>g}`nPftsq&d&bVeuAjT!h1spKl( z;Y`R))OaPA9_h!3M~_NHq^lwYwiSzC!2*XjyxM7(I}jy>xeP=Uof~7+hvPs3lV}M7 z3s10xq2OjRG?LfqM9)!Fe3&DDuzgg_YDCF6)_eB6u|?x z6Cx|aZ@(C)=e31t!hKSr3NSG+AutIB1uG5%0vZJX1Qdy85~ufG97a}O7z;%xFch3+ Tn{osQX`OM>(pem_path: P) -> Result>( pkcs12_path: P, password: &str, @@ -406,7 +407,29 @@ pub fn load_pkcs12_identity>( .map_err(Error::InvalidWeb3SignerClientIdentityCertificateFile)? .read_to_end(&mut buf) .map_err(Error::InvalidWeb3SignerClientIdentityCertificateFile)?; - Identity::from_pkcs12_der(&buf, password) + + let keystore = p12_keystore::KeyStore::from_pkcs12(&buf, password).map_err(|e| { + Error::InvalidWeb3SignerClientIdentityCertificateFile(io::Error::new( + io::ErrorKind::InvalidData, + format!("PKCS12 parse error: {e:?}"), + )) + })?; + + let (_alias, key_chain) = keystore + .private_key_chain() + .ok_or(Error::MissingWeb3SignerClientIdentityCertificateFile)?; + + let key_pem = pem::encode(&pem::Pem::new("PRIVATE KEY", key_chain.key())); + let certs_pem: String = key_chain + .chain() + .iter() + .map(|cert| pem::encode(&pem::Pem::new("CERTIFICATE", cert.as_der()))) + .collect::>() + .join("\n"); + + let combined_pem = format!("{key_pem}\n{certs_pem}"); + + Identity::from_pem(combined_pem.as_bytes()) .map_err(Error::InvalidWeb3SignerClientIdentityCertificate) }