From 905656a788303e2556b7ae0961ffa369b23f65e4 Mon Sep 17 00:00:00 2001 From: conache Date: Sun, 2 Aug 2026 12:03:00 +0300 Subject: [PATCH 01/44] Add IL committee duties endpoint types and client to eth2 --- common/eth2/src/lib.rs | 27 +++++++++++++++++++++++++++ common/eth2/src/types.rs | 9 +++++++++ 2 files changed, 36 insertions(+) diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index b216362b389..51a4324afeb 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -81,6 +81,7 @@ const HTTP_SYNC_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_SYNC_AGGREGATOR_TIMEOUT_QUOTIENT: u32 = 24; // For DVT involving middleware only // TODO(EIP-7732): Determine what this quotient should be const HTTP_PTC_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; +const HTTP_INCLUSION_LIST_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; 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; @@ -103,6 +104,7 @@ pub struct Timeouts { pub sync_duties: Duration, pub sync_aggregators: Duration, pub ptc_duties: Duration, + pub inclusion_list_duties: Duration, pub get_beacon_blocks_ssz: Duration, pub get_debug_beacon_states: Duration, pub get_deposit_snapshot: Duration, @@ -125,6 +127,7 @@ impl Timeouts { sync_duties: timeout, sync_aggregators: timeout, ptc_duties: timeout, + inclusion_list_duties: timeout, get_beacon_blocks_ssz: timeout, get_debug_beacon_states: timeout, get_deposit_snapshot: timeout, @@ -149,6 +152,7 @@ impl Timeouts { sync_duties: base_timeout / HTTP_SYNC_DUTIES_TIMEOUT_QUOTIENT, sync_aggregators: base_timeout / HTTP_SYNC_AGGREGATOR_TIMEOUT_QUOTIENT, ptc_duties: base_timeout / HTTP_PTC_DUTIES_TIMEOUT_QUOTIENT, + inclusion_list_duties: base_timeout / HTTP_INCLUSION_LIST_DUTIES_TIMEOUT_QUOTIENT, get_beacon_blocks_ssz: base_timeout / HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT, get_debug_beacon_states: base_timeout / HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT, get_deposit_snapshot: base_timeout / HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT, @@ -3651,6 +3655,29 @@ impl BeaconNodeHttpClient { ) .await } + + /// `POST validator/duties/inclusion_list/{epoch}` + pub async fn post_validator_duties_inclusion_list( + &self, + epoch: Epoch, + indices: &[u64], + ) -> Result>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("duties") + .push("inclusion_list") + .push(&epoch.to_string()); + + self.post_with_timeout_and_response( + path, + &ValidatorIndexDataRef(indices), + self.timeouts.inclusion_list_duties, + ) + .await + } } #[cfg(test)] diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index f941857d28f..2ef0ca4152c 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -862,6 +862,15 @@ pub struct PtcDuty { pub slot: Slot, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct InclusionListDuty { + pub pubkey: PublicKeyBytes, + #[serde(with = "serde_utils::quoted_u64")] + pub validator_index: u64, + pub slot: Slot, + pub inclusion_list_committee_root: Hash256, +} + #[derive(Clone, Deserialize)] pub struct ValidatorBlocksQuery { pub randao_reveal: SignatureBytes, From aaccd5f567649f45bca3581c53b12fa64deb257f Mon Sep 17 00:00:00 2001 From: conache Date: Sun, 2 Aug 2026 13:26:29 +0300 Subject: [PATCH 02/44] Scaffold task for tracking IL comittee responsibilities for locally-managed validators --- .../validator_services/src/duties_service.rs | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index 5fe413a2166..35bc2b9bb62 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -11,10 +11,7 @@ use crate::sync::SyncDutiesMap; use crate::sync::poll_sync_committee_duties; use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; use bls::PublicKeyBytes; -use eth2::types::{ - AttesterData, BeaconCommitteeSelection, BeaconCommitteeSubscription, DutiesResponse, - ProposerData, PtcDuty, StateId, ValidatorId, -}; +use eth2::types::{AttesterData, BeaconCommitteeSelection, BeaconCommitteeSubscription, DutiesResponse, InclusionListDuty, ProposerData, PtcDuty, StateId, ValidatorId}; use futures::{ StreamExt, stream::{self, FuturesUnordered, TryStreamExt}, @@ -286,6 +283,7 @@ type DependentRoot = Hash256; type AttesterMap = HashMap>; type ProposerMap = HashMap)>; type PtcMap = HashMap)>; +type IlMap = HashMap)>; pub struct DutiesServiceBuilder { /// Provides the canonical list of locally-managed validators. @@ -395,6 +393,7 @@ impl DutiesServiceBuilder { proposers: Default::default(), sync_duties: SyncDutiesMap::new(self.sync_selection_proof_config), ptc_duties: Default::default(), + il_duties: Default::default(), validator_store: self .validator_store .ok_or("Cannot build DutiesService without validator_store")?, @@ -428,6 +427,8 @@ pub struct DutiesService { pub sync_duties: SyncDutiesMap, /// Maps an epoch to PTC duties for locally-managed validators. pub ptc_duties: RwLock, + // Maps an epoch to the IL committee duties for locally-managed validators. + pub il_duties: RwLock, /// Provides the canonical list of locally-managed validators. pub validator_store: Arc, /// Maps unknown validator pubkeys to the next slot time when a poll should be conducted again. @@ -764,6 +765,49 @@ pub fn start_update_service "duties_service_ptc", ); } + + // Spawn the task which keeps track of the inclusion list committee duties. + // Only track IL committee duties if the heze fork is scheduled + if core_duties_service.spec.is_heze_scheduled() { + let duties_service = core_duties_service.clone(); + core_duties_service.executor.spawn( + async move { + loop { + let Some(current_slot) = duties_service.slot_clock.now() else { + // Sleep for one slot if we are unable to read from the system clock + sleep(duties_service.slot_clock.slot_duration()).await; + continue; + }; + + let current_epoch = current_slot.epoch(S::E::slots_per_epoch()); + let Some(heze_fork_epoch) = duties_service.spec.heze_fork_epoch else { + // Heze fork epoch not configured + break; + }; + + if current_epoch + 1 < heze_fork_epoch { + // Wait until the next slot and check again if the Heze fork epoch is close + if let Some(duration) = duties_service.slot_clock.duration_to_next_slot() { + sleep(duration).await; + } else { + sleep(duties_service.slot_clock.slot_duration()).await; + } + continue; + } + + // TODO: poll IL committee duties + + if let Some(duration) = duties_service.slot_clock.duration_to_next_slot() { + sleep(duration).await; + } else { + // Sleep for one slot if we are unable to read from the system clock + sleep(duties_service.slot_clock.slot_duration()).await; + } + } + }, + "duties_service_il_committee" + ) + } } /// Iterate through all the voting pubkeys in the `ValidatorStore` and attempt to learn any unknown From 35287207cfc32678c3106165814dc5227bc4888f Mon Sep 17 00:00:00 2001 From: conache Date: Sun, 2 Aug 2026 18:50:27 +0300 Subject: [PATCH 03/44] Add il duties polling for current and next epoch --- validator_client/validator_metrics/src/lib.rs | 2 + .../validator_services/src/duties_service.rs | 211 +++++++++++++++++- 2 files changed, 204 insertions(+), 9 deletions(-) diff --git a/validator_client/validator_metrics/src/lib.rs b/validator_client/validator_metrics/src/lib.rs index 46a86381f91..ac6bfd4b103 100644 --- a/validator_client/validator_metrics/src/lib.rs +++ b/validator_client/validator_metrics/src/lib.rs @@ -26,8 +26,10 @@ pub const UPDATE_PTC_CURRENT_EPOCH: &str = "update_ptc_current_epoch"; pub const UPDATE_PTC_NEXT_EPOCH: &str = "update_ptc_next_epoch"; pub const UPDATE_PTC_FETCH: &str = "update_ptc_fetch"; pub const UPDATE_PTC_STORE: &str = "update_ptc_store"; +pub const UPDATE_IL_DUTIES_STORE: &str = "update_il_duties_store"; pub const ATTESTER_DUTIES_HTTP_POST: &str = "attester_duties_http_post"; pub const PTC_DUTIES_HTTP_POST: &str = "ptc_duties_http_post"; +pub const IL_DUTIES_HTTP_POST: &str = "il_duties_http_post"; pub const PROPOSER_DUTIES_HTTP_GET: &str = "proposer_duties_http_get"; pub const VALIDATOR_DUTIES_SYNC_HTTP_POST: &str = "validator_duties_sync_http_post"; pub const VALIDATOR_ID_HTTP_GET: &str = "validator_id_http_get"; diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index 35bc2b9bb62..bc1bf94d45d 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -11,7 +11,10 @@ use crate::sync::SyncDutiesMap; use crate::sync::poll_sync_committee_duties; use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; use bls::PublicKeyBytes; -use eth2::types::{AttesterData, BeaconCommitteeSelection, BeaconCommitteeSubscription, DutiesResponse, InclusionListDuty, ProposerData, PtcDuty, StateId, ValidatorId}; +use eth2::types::{ + AttesterData, BeaconCommitteeSelection, BeaconCommitteeSubscription, DutiesResponse, + InclusionListDuty, ProposerData, PtcDuty, StateId, ValidatorId, +}; use futures::{ StreamExt, stream::{self, FuturesUnordered, TryStreamExt}, @@ -44,6 +47,7 @@ const VALIDATOR_METRICS_MIN_COUNT: usize = 64; /// reduces the amount of data that needs to be transferred. const INITIAL_DUTIES_QUERY_SIZE: usize = 1; const INITIAL_PTC_DUTIES_QUERY_SIZE: usize = 1; +const INITIAL_IL_DUTIES_QUERY_SIZE: usize = 1; /// Offsets from the attestation duty slot at which a subscription should be sent. const ATTESTATION_SUBSCRIPTION_OFFSETS: [u64; 8] = [3, 4, 5, 6, 7, 8, 16, 32]; @@ -82,6 +86,7 @@ pub enum Error { UnableToReadSlotClock, FailedToDownloadAttesters(#[allow(dead_code)] String), FailedToDownloadPtc(#[allow(dead_code)] String), + FailedToDownloadILs(#[allow(dead_code)] String), FailedToProduceSelectionProof(#[allow(dead_code)] ValidatorStoreError), InvalidModulo(#[allow(dead_code)] ArithError), Arith(#[allow(dead_code)] ArithError), @@ -769,15 +774,15 @@ pub fn start_update_service // Spawn the task which keeps track of the inclusion list committee duties. // Only track IL committee duties if the heze fork is scheduled if core_duties_service.spec.is_heze_scheduled() { - let duties_service = core_duties_service.clone(); + let duties_service = core_duties_service.clone(); core_duties_service.executor.spawn( async move { loop { - let Some(current_slot) = duties_service.slot_clock.now() else { - // Sleep for one slot if we are unable to read from the system clock - sleep(duties_service.slot_clock.slot_duration()).await; - continue; - }; + let Some(current_slot) = duties_service.slot_clock.now() else { + // Sleep for one slot if we are unable to read from the system clock + sleep(duties_service.slot_clock.slot_duration()).await; + continue; + }; let current_epoch = current_slot.epoch(S::E::slots_per_epoch()); let Some(heze_fork_epoch) = duties_service.spec.heze_fork_epoch else { @@ -795,7 +800,12 @@ pub fn start_update_service continue; } - // TODO: poll IL committee duties + if let Err(e) = poll_beacon_il_committee_duties(&duties_service).await { + error!( + error = ?e, + "Failed to poll il committee duties" + ) + } if let Some(duration) = duties_service.slot_clock.duration_to_next_slot() { sleep(duration).await; @@ -805,7 +815,7 @@ pub fn start_update_service } } }, - "duties_service_il_committee" + "duties_service_il_committee", ) } } @@ -1448,6 +1458,26 @@ async fn post_validator_duties_ptc( .map_err(|e| Error::FailedToDownloadPtc(e.to_string())) } +async fn post_validator_il_duties( + duties_service: &Arc>, + epoch: Epoch, + validator_indices: &[u64], +) -> Result>, Error> { + duties_service + .beacon_nodes + .first_success(|beacon_node| async move { + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::DUTIES_SERVICE_TIMES, + &[validator_metrics::PTC_DUTIES_HTTP_POST], + ); + beacon_node + .post_validator_duties_inclusion_list(epoch, validator_indices) + .await + }) + .await + .map_err(|e| Error::FailedToDownloadILs(e.to_string())) +} + /// Compute the attestation selection proofs for the `duties` and add them to the `attesters` map. /// /// Duties are computed in batches each slot. If a re-org is detected then the process will @@ -2028,6 +2058,169 @@ async fn poll_beacon_ptc_attesters_for_epoch< Ok(()) } +async fn poll_beacon_il_committee_duties( + duties_service: &Arc>, +) -> Result<(), Error> { + // figure out current time + let current_slot = duties_service + .slot_clock + .now() + .ok_or(Error::UnableToReadSlotClock)?; + let current_epoch = current_slot.epoch(S::E::slots_per_epoch()); + + // Collect *all* pubkeys, even those undergoing doppelganger protection. + let local_pubkeys: HashSet = duties_service + .validator_store + .voting_pubkeys(DoppelgangerStatus::ignored); + let local_indices = local_pubkeys + .iter() + .filter_map(|pubkey| duties_service.validator_store.validator_index(pubkey)) + .collect::>(); + + // Poll for current epoch + if let Err(e) = poll_beacon_il_duties_for_epoch( + duties_service, + current_epoch, + &local_indices, + &local_pubkeys, + ) + .await + { + error!( + %current_epoch, + request_epoch = %current_epoch, + err = ?e, + "Failed to download IL committee duties" + ); + } + + // Poll for next epoch + let next_epoch = current_epoch + 1; + if let Err(e) = + poll_beacon_il_duties_for_epoch(duties_service, next_epoch, &local_indices, &local_pubkeys) + .await + { + error!( + %next_epoch, + request_epoch = %next_epoch, + err = ?e, + "Failed to download IL committee duties" + ); + } + + // Prune old IL committee duties + duties_service + .il_duties + .write() + .retain(|&epoch, _| epoch + HISTORICAL_DUTIES_EPOCHS >= current_epoch); + + Ok(()) +} + +/// For the given `local_indices` and `local_pubkeys`, download the IL committee duties +/// for the given `epoch` and store them in `duties_service.il_duties` using bandwidth optimization. +async fn poll_beacon_il_duties_for_epoch( + duties_service: &Arc>, + epoch: Epoch, + local_indices: &[u64], + local_pubkeys: &HashSet, +) -> Result<(), Error> { + if local_indices.is_empty() { + debug!( + %epoch, + "No validators, not downloading PTC duties" + ); + return Ok(()); + } + + let initial_indices_to_request = + &local_indices[0..min(INITIAL_IL_DUTIES_QUERY_SIZE, local_indices.len())]; + + let response = + post_validator_il_duties(duties_service, epoch, initial_indices_to_request).await?; + let dependent_root = response.dependent_root; + + // Check if we need to update duties for this epoch and collect validators to update. + // We update if we have no epoch data OR if the dependent_root changed. + let validators_to_update = { + // Avoid holding the read-lock for any longer than required. + let ptc_duties = duties_service.ptc_duties.read(); + let needs_update = ptc_duties.get(&epoch).is_none_or(|(prior_root, _duties)| { + // Update if dependent_root changed + *prior_root != dependent_root + }); + + if needs_update { + local_pubkeys.iter().collect::>() + } else { + Vec::new() + } + }; + + if validators_to_update.is_empty() { + // No validators have conflicting (epoch, dependent_root) values for this epoch. + return Ok(()); + } + + // Make a request for all indices that require updating which we have not already made a request for. + let indices_to_request = validators_to_update + .iter() + .filter_map(|pubkey| duties_service.validator_store.validator_index(pubkey)) + .filter(|validator_index| !initial_indices_to_request.contains(validator_index)) + .collect::>(); + + // Filter the initial duties by their relevance so that we don't hit warnings about + // overwriting duties. + let new_initial_duties = response + .data + .into_iter() + .filter(|duty| validators_to_update.contains(&&duty.pubkey)); + + let mut new_duties = if !indices_to_request.is_empty() { + post_validator_il_duties(duties_service, epoch, indices_to_request.as_slice()) + .await? + .data + } else { + vec![] + }; + new_duties.extend(new_initial_duties); + + let _store_timer = validator_metrics::start_timer_vec( + &validator_metrics::DUTIES_SERVICE_TIMES, + &[validator_metrics::UPDATE_IL_DUTIES_STORE], + ); + + debug!( + %dependent_root, + num_new_duties = new_duties.len(), + "Downloaded IL duties" + ); + + // Update duties - we only reach here if dependent_root changed or epoch is missing + let mut il_duties = duties_service.il_duties.write(); + + match il_duties.entry(epoch) { + hash_map::Entry::Occupied(mut entry) => { + // Dependent root must have changed, so we do complete replacement. + // We cannot support partial updates for the same dependent_root. + let (existing_root, _existing_duties) = entry.get(); + debug!( + old_root = %existing_root, + new_root = %dependent_root, + "IL dependent root changed, replacing all duties" + ); + + *entry.get_mut() = (dependent_root, new_duties); + } + hash_map::Entry::Vacant(entry) => { + // No existing duties for this epoch + entry.insert((dependent_root, new_duties)); + } + } + + Ok(()) +} + /// Notify the block service if it should produce a block. async fn notify_block_production_service( current_slot: Slot, From 0c2101003da299b4ff56fdd3fd22fccde2ac02f9 Mon Sep 17 00:00:00 2001 From: conache Date: Tue, 4 Aug 2026 18:20:36 +0300 Subject: [PATCH 04/44] Add missing metrics --- validator_client/validator_metrics/src/lib.rs | 5 ++++- .../validator_services/src/duties_service.rs | 20 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/validator_client/validator_metrics/src/lib.rs b/validator_client/validator_metrics/src/lib.rs index ac6bfd4b103..60a4d4dd9c0 100644 --- a/validator_client/validator_metrics/src/lib.rs +++ b/validator_client/validator_metrics/src/lib.rs @@ -26,9 +26,12 @@ pub const UPDATE_PTC_CURRENT_EPOCH: &str = "update_ptc_current_epoch"; pub const UPDATE_PTC_NEXT_EPOCH: &str = "update_ptc_next_epoch"; pub const UPDATE_PTC_FETCH: &str = "update_ptc_fetch"; pub const UPDATE_PTC_STORE: &str = "update_ptc_store"; -pub const UPDATE_IL_DUTIES_STORE: &str = "update_il_duties_store"; pub const ATTESTER_DUTIES_HTTP_POST: &str = "attester_duties_http_post"; pub const PTC_DUTIES_HTTP_POST: &str = "ptc_duties_http_post"; +pub const UPDATE_IL_DUTIES_CURRENT_EPOCH: &str = "update_il_duties_current_epoch"; +pub const UPDATE_IL_DUTIES_NEXT_EPOCH: &str = "update_il_duties_next_epoch"; +pub const UPDATE_IL_DUTIES_FETCH: &str = "update_il_duties_fetch"; +pub const UPDATE_IL_DUTIES_STORE: &str = "update_il_duties_store"; pub const IL_DUTIES_HTTP_POST: &str = "il_duties_http_post"; pub const PROPOSER_DUTIES_HTTP_GET: &str = "proposer_duties_http_get"; pub const VALIDATOR_DUTIES_SYNC_HTTP_POST: &str = "validator_duties_sync_http_post"; diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index bc1bf94d45d..711399f96c0 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -2061,7 +2061,11 @@ async fn poll_beacon_ptc_attesters_for_epoch< async fn poll_beacon_il_committee_duties( duties_service: &Arc>, ) -> Result<(), Error> { - // figure out current time + let curren_epoch_timer = validator_metrics::start_timer_vec( + &validator_metrics::DUTIES_SERVICE_TIMES, + &[validator_metrics::UPDATE_IL_DUTIES_CURRENT_EPOCH], + ); + let current_slot = duties_service .slot_clock .now() @@ -2093,6 +2097,12 @@ async fn poll_beacon_il_committee_duties { // Dependent root must have changed, so we do complete replacement. - // We cannot support partial updates for the same dependent_root. let (existing_root, _existing_duties) = entry.get(); debug!( old_root = %existing_root, From 8fe7be03940a96a202ede914a2009624297cf73a Mon Sep 17 00:00:00 2001 From: conache Date: Tue, 4 Aug 2026 18:30:58 +0300 Subject: [PATCH 05/44] Add missing metrics --- validator_client/validator_services/src/duties_service.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index 711399f96c0..0a471017632 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -2062,8 +2062,8 @@ async fn poll_beacon_il_committee_duties>, ) -> Result<(), Error> { let curren_epoch_timer = validator_metrics::start_timer_vec( - &validator_metrics::DUTIES_SERVICE_TIMES, - &[validator_metrics::UPDATE_IL_DUTIES_CURRENT_EPOCH], + &validator_metrics::DUTIES_SERVICE_TIMES, + &[validator_metrics::UPDATE_IL_DUTIES_CURRENT_EPOCH], ); let current_slot = duties_service From ac487d578e49d20195d49b8cc5cdd2df2610a72d Mon Sep 17 00:00:00 2001 From: conache Date: Tue, 4 Aug 2026 19:02:50 +0300 Subject: [PATCH 06/44] Add more fixes and metrics --- validator_client/http_metrics/src/lib.rs | 10 ++++++++ validator_client/validator_metrics/src/lib.rs | 7 ++++++ .../validator_services/src/duties_service.rs | 25 ++++++++++++++----- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/validator_client/http_metrics/src/lib.rs b/validator_client/http_metrics/src/lib.rs index a4802158682..594242dbbf8 100644 --- a/validator_client/http_metrics/src/lib.rs +++ b/validator_client/http_metrics/src/lib.rs @@ -205,6 +205,16 @@ pub fn gather_prometheus_metrics( &[NEXT_EPOCH], duties_service.ptc_count(next_epoch) as i64, ); + set_int_gauge( + &IL_COUNT, + &[CURRENT_EPOCH], + duties_service.il_duties_count(current_epoch) as i64, + ); + set_int_gauge( + &IL_COUNT, + &[NEXT_EPOCH], + duties_service.il_duties_count(next_epoch) as i64, + ); } } diff --git a/validator_client/validator_metrics/src/lib.rs b/validator_client/validator_metrics/src/lib.rs index 60a4d4dd9c0..812140d018a 100644 --- a/validator_client/validator_metrics/src/lib.rs +++ b/validator_client/validator_metrics/src/lib.rs @@ -179,6 +179,13 @@ pub static PTC_COUNT: LazyLock> = LazyLock::new(|| { &["task"], ) }); +pub static IL_COUNT: LazyLock> = LazyLock::new(|| { + try_create_int_gauge_vec( + "vc_beacon_il_count", + "Number of IL (Inclusion List Committee) validators on this host", + &["task"], + ) +}); pub static PROPOSAL_CHANGED: LazyLock> = LazyLock::new(|| { try_create_int_counter( "vc_beacon_block_proposal_changed", diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index 0a471017632..aaa4c39f2b7 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -506,6 +506,15 @@ impl DutiesService { .unwrap_or(0) } + /// Returns the total number of validators that have IL duties in the given epoch. + pub fn il_duties_count(&self, epoch: Epoch) -> usize { + self.il_duties + .read() + .get(&epoch) + .map(|(_, duties)| duties.len()) + .unwrap_or(0) + } + /// Returns the total number of validators that are in a doppelganger detection period. pub fn doppelganger_detecting_count(&self) -> usize { self.validator_store @@ -1468,7 +1477,7 @@ async fn post_validator_il_duties( .first_success(|beacon_node| async move { let _timer = validator_metrics::start_timer_vec( &validator_metrics::DUTIES_SERVICE_TIMES, - &[validator_metrics::PTC_DUTIES_HTTP_POST], + &[validator_metrics::IL_DUTIES_HTTP_POST], ); beacon_node .post_validator_duties_inclusion_list(epoch, validator_indices) @@ -2061,7 +2070,7 @@ async fn poll_beacon_ptc_attesters_for_epoch< async fn poll_beacon_il_committee_duties( duties_service: &Arc>, ) -> Result<(), Error> { - let curren_epoch_timer = validator_metrics::start_timer_vec( + let current_epoch_timer = validator_metrics::start_timer_vec( &validator_metrics::DUTIES_SERVICE_TIMES, &[validator_metrics::UPDATE_IL_DUTIES_CURRENT_EPOCH], ); @@ -2097,7 +2106,7 @@ async fn poll_beacon_il_committee_duties Date: Tue, 4 Aug 2026 19:21:05 +0300 Subject: [PATCH 07/44] Cleanup --- validator_client/validator_services/src/duties_service.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index aaa4c39f2b7..e45d96ad54b 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -432,7 +432,7 @@ pub struct DutiesService { pub sync_duties: SyncDutiesMap, /// Maps an epoch to PTC duties for locally-managed validators. pub ptc_duties: RwLock, - // Maps an epoch to the IL committee duties for locally-managed validators. + /// Maps an epoch to the IL committee duties for locally-managed validators. pub il_duties: RwLock, /// Provides the canonical list of locally-managed validators. pub validator_store: Arc, @@ -2120,7 +2120,7 @@ async fn poll_beacon_il_committee_duties Date: Thu, 13 Aug 2026 15:40:51 +0300 Subject: [PATCH 08/44] Add method for fetching IL duties for slot --- .../validator_services/src/duties_service.rs | 19 +++++++++++++++++++ .../src/notifier_service.rs | 3 +++ 2 files changed, 22 insertions(+) diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index e45d96ad54b..c4ec40045ed 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -596,6 +596,25 @@ impl DutiesService { }) .unwrap_or_default() } + + /// Get IL committee duties for a specific slot. + /// + /// Returns duties for local validators who have IL committee assignments at the given slot. + pub fn get_il_duties_for_slot(&self, slot: Slot) -> Vec { + let epoch = slot.epoch(S::E::slots_per_epoch()); + + self.il_duties + .read() + .get(&epoch) + .map(|(_, il_duties)| { + il_duties + .iter() + .filter(|il_duty| il_duty.slot == slot) + .cloned() + .collect() + }) + .unwrap_or_default() + } } /// Start the service that periodically polls the beacon node for validator duties. This will start diff --git a/validator_client/validator_services/src/notifier_service.rs b/validator_client/validator_services/src/notifier_service.rs index e6e7a678640..38ee32e7bdb 100644 --- a/validator_client/validator_services/src/notifier_service.rs +++ b/validator_client/validator_services/src/notifier_service.rs @@ -110,6 +110,7 @@ pub async fn notify( let proposing_validators = duties_service.proposer_count(epoch); let attesting_validators = duties_service.attester_count(epoch); let ptc_validators = duties_service.ptc_count(epoch); + let il_validators = duties_service.il_duties_count(epoch); let doppelganger_detecting_validators = duties_service.doppelganger_detecting_count(); if doppelganger_detecting_validators > 0 { @@ -128,6 +129,7 @@ pub async fn notify( info!( current_epoch_proposers = proposing_validators, current_epoch_ptc = ptc_validators, + current_epoch_il = il_validators, active_validators = attesting_validators, total_validators = total_validators, %epoch, @@ -138,6 +140,7 @@ pub async fn notify( info!( current_epoch_proposers = proposing_validators, current_epoch_ptc = ptc_validators, + current_epoch_il = il_validators, active_validators = attesting_validators, total_validators = total_validators, %epoch, From a8539f126a243b05914aa36ff211498b892468cd Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 13 Aug 2026 19:34:51 +0300 Subject: [PATCH 09/44] Return dependent root for each il duties list for slot --- .../validator_services/src/duties_service.rs | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index c4ec40045ed..64add327bff 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -599,21 +599,28 @@ impl DutiesService { /// Get IL committee duties for a specific slot. /// - /// Returns duties for local validators who have IL committee assignments at the given slot. - pub fn get_il_duties_for_slot(&self, slot: Slot) -> Vec { + /// Returns the epoch's dependent root alongside the duties for local validators who have + /// IL committee assignments at the given slot, or `None` if the duties for the slot's epoch + /// have not been downloaded yet. + pub fn get_il_duties_for_slot( + &self, + slot: Slot, + ) -> Option<(DependentRoot, Vec)> { let epoch = slot.epoch(S::E::slots_per_epoch()); self.il_duties .read() .get(&epoch) - .map(|(_, il_duties)| { - il_duties - .iter() - .filter(|il_duty| il_duty.slot == slot) - .cloned() - .collect() + .map(|(dependent_root, il_duties)| { + ( + *dependent_root, + il_duties + .iter() + .filter(|il_duty| il_duty.slot == slot) + .cloned() + .collect(), + ) }) - .unwrap_or_default() } } From e93dd6ac16a4ec690f4a4203db51add6f0d4d05f Mon Sep 17 00:00:00 2001 From: conache Date: Fri, 21 Aug 2026 18:38:50 +0300 Subject: [PATCH 10/44] Remove inclusion_list_committee_root from the il duty type --- common/eth2/src/types.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index fa23c3d737a..6cac94189bd 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -867,7 +867,6 @@ pub struct InclusionListDuty { #[serde(with = "serde_utils::quoted_u64")] pub validator_index: u64, pub slot: Slot, - pub inclusion_list_committee_root: Hash256, } #[derive(Clone, Deserialize)] From e766740b71fa296cd38e98bd4a13de82ece78bee Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 12 Aug 2026 19:29:14 +0300 Subject: [PATCH 11/44] Add inclusion_list_due wiring to the chain specs --- consensus/types/src/core/chain_spec.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index 817517de2e5..24466520248 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -131,6 +131,7 @@ pub struct ChainSpec { pub aggregate_attestation_due: Duration, pub sync_message_due: Duration, pub contribution_and_proof_due: Duration, + pub inclusion_list_due: Duration, /* * Reward and penalty quotients @@ -966,6 +967,12 @@ impl ChainSpec { self.sync_message_due } + /// Spec: `get_inclusion_list_due_ms`. + /// Get the duration into a slot in which an inclusion list production duty is due. + pub fn get_inclusion_list_due(&self) -> Duration { + self.inclusion_list_due + } + /// Calculate the duration into a slot for a given slot component pub fn compute_slot_component_duration( &self, @@ -1043,6 +1050,9 @@ impl ChainSpec { self.contribution_and_proof_due = self .compute_slot_component_duration(self.contribution_due_bps) .expect("invalid chain spec: cannot compute contribution_and_proof_due"); + self.inclusion_list_due = self + .compute_slot_component_duration(self.inclusion_list_due_bps) + .expect("invalid chain spec: cannot compute inclusion_list_due"); self.attestation_subnet_prefix_bits = compute_attestation_subnet_prefix_bits( self.attestation_subnet_count, @@ -1185,6 +1195,7 @@ impl ChainSpec { aggregate_attestation_due: Duration::from_millis(8000), sync_message_due: Duration::from_millis(3999), contribution_and_proof_due: Duration::from_millis(8000), + inclusion_list_due: Duration::from_millis(8000), /* * Reward and penalty quotients @@ -1528,6 +1539,7 @@ impl ChainSpec { aggregate_attestation_due: Duration::from_millis(4000), sync_message_due: Duration::from_millis(1999), contribution_and_proof_due: Duration::from_millis(4000), + inclusion_list_due: Duration::from_millis(4000), // Networking Fulu blob_schedule: BlobSchedule::default(), @@ -1631,6 +1643,7 @@ impl ChainSpec { aggregate_attestation_due: Duration::from_millis(3333), sync_message_due: Duration::from_millis(1666), contribution_and_proof_due: Duration::from_millis(3333), + inclusion_list_due: Duration::from_millis(3333), /* * Reward and penalty quotients From cb4f4981c07ce6d5759cda28d54d13a794f5539f Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 13 Aug 2026 12:38:03 +0300 Subject: [PATCH 12/44] Add basic il service and basic wiring to the validator client --- validator_client/src/lib.rs | 19 ++++ .../src/inclusion_list_service.rs | 89 +++++++++++++++++++ .../validator_services/src/lib.rs | 1 + 3 files changed, 109 insertions(+) create mode 100644 validator_client/validator_services/src/inclusion_list_service.rs diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 88844918431..0c0bfc2f2e6 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -44,6 +44,7 @@ use validator_services::{ attestation_service::{AttestationService, AttestationServiceBuilder}, block_service::{BlockService, BlockServiceBuilder}, duties_service::{self, DutiesService, DutiesServiceBuilder}, + inclusion_list_service::InclusionListService, latency_service, payload_attestation_service::PayloadAttestationService, preparation_service::{PreparationService, PreparationServiceBuilder}, @@ -90,6 +91,7 @@ pub struct ProductionValidatorClient { ProposerPreferencesService, SystemTimeSlotClock>, doppelganger_service: Option>, preparation_service: PreparationService, SystemTimeSlotClock>, + inclusion_list_service: InclusionListService, SystemTimeSlotClock>, validator_store: Arc>, slot_clock: SystemTimeSlotClock, http_api_listen_addr: Option, @@ -577,6 +579,15 @@ impl ProductionValidatorClient { context.eth2_config.spec.clone(), ); + let inclusion_list_service = InclusionListService::new( + duties_service.clone(), + validator_store.clone(), + slot_clock.clone(), + beacon_nodes.clone(), + context.executor.clone(), + context.eth2_config.spec.clone(), + ); + Ok(Self { context, duties_service, @@ -585,6 +596,7 @@ impl ProductionValidatorClient { sync_committee_service, payload_attestation_service, proposer_preferences_service, + inclusion_list_service, doppelganger_service, preparation_service, validator_store, @@ -669,6 +681,13 @@ impl ProductionValidatorClient { .map_err(|e| format!("Unable to start proposer preferences service: {}", e))?; } + if self.context.eth2_config.spec.is_heze_scheduled() { + self.inclusion_list_service + .clone() + .start_update_service() + .map_err(|e| format!("Unable to start inclusion list service: {}", e))?; + } + self.preparation_service .clone() .start_update_service(&self.context.eth2_config.spec) diff --git a/validator_client/validator_services/src/inclusion_list_service.rs b/validator_client/validator_services/src/inclusion_list_service.rs new file mode 100644 index 00000000000..374756b2388 --- /dev/null +++ b/validator_client/validator_services/src/inclusion_list_service.rs @@ -0,0 +1,89 @@ +use crate::duties_service::DutiesService; +use beacon_node_fallback::BeaconNodeFallback; +use slot_clock::SlotClock; +use std::ops::Deref; +use std::sync::Arc; +use task_executor::TaskExecutor; +use tracing::{error, info}; +use types::ChainSpec; +use validator_store::ValidatorStore; + +pub struct Inner { + duties_service: Arc>, + validator_store: Arc, + slot_clock: T, + beacon_nodes: Arc>, + executor: TaskExecutor, + chain_spec: Arc, +} + +pub struct InclusionListService { + inner: Arc>, +} + +impl Clone for InclusionListService { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl Deref for InclusionListService { + type Target = Inner; + + fn deref(&self) -> &Self::Target { + self.inner.deref() + } +} + +impl InclusionListService +where + S: ValidatorStore + 'static, + T: SlotClock + 'static, +{ + pub fn new( + duties_service: Arc>, + validator_store: Arc, + slot_clock: T, + beacon_nodes: Arc>, + executor: TaskExecutor, + chain_spec: Arc, + ) -> Self { + Self { + inner: Arc::new(Inner { + duties_service, + validator_store, + slot_clock, + beacon_nodes, + executor, + chain_spec, + }), + } + } + + pub fn start_update_service(self) -> Result<(), String> { + info!( + inclusion_list_due_ms = self.chain_spec.get_inclusion_list_due().as_millis(), + "Inclusion list service started" + ); + let executor = self.executor.clone(); + + let interval_fut = async move { + loop { + if let Err(e) = self.spawn_inclusion_list_tasks().await { + error!(error = e, "Failed to produce inclusion lists"); + } + } + }; + + executor.spawn(interval_fut, "inclusion_list_service"); + + Ok(()) + } + + async fn spawn_inclusion_list_tasks(&self) -> Result<(), String> { + todo!("spawn_inclusion_list_tasks not yet implemented"); + Ok(()) + } +} diff --git a/validator_client/validator_services/src/lib.rs b/validator_client/validator_services/src/lib.rs index c39ef4499b7..0b4f12b6ec2 100644 --- a/validator_client/validator_services/src/lib.rs +++ b/validator_client/validator_services/src/lib.rs @@ -1,6 +1,7 @@ pub mod attestation_service; pub mod block_service; pub mod duties_service; +pub mod inclusion_list_service; pub mod latency_service; pub mod notifier_service; pub mod payload_attestation_service; From 4153d76521cd06d5fe4a950aaaa872491b5c9593 Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 13 Aug 2026 19:40:56 +0300 Subject: [PATCH 13/44] Setup il tasks per slot --- common/eth2/src/lib.rs | 18 +++ common/eth2/src/types.rs | 7 + .../src/inclusion_list_service.rs | 143 +++++++++++++++++- 3 files changed, 165 insertions(+), 3 deletions(-) diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 979b6ba3f09..08036f041b6 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -3555,6 +3555,24 @@ impl BeaconNodeHttpClient { self.get_opt(path).await } + /// `GET validator/inclusion_list` + pub async fn get_validator_inclusion_list( + &self, + slot: Slot, + ) -> Result, Error> { + let mut path = self.eth_path(V1)?; + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("inclusion_list"); + + path.query_pairs_mut() + .append_pair("slot", &slot.to_string()); + + self.get_with_timeout(path, self.timeouts.inclusion_list_duties) + .await + } + /// `POST lighthouse/liveness` pub async fn post_lighthouse_liveness( &self, diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 6cac94189bd..fe74f856211 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -902,6 +902,13 @@ impl TryFrom> for SkipRandaoVerification { } } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct InclusionListTransactions { + #[serde(with = "ssz_types::serde_utils::prog_list_of_hex_prog_var_list")] + pub transactions: ProgressiveTransactions, +} + #[derive(Clone, Serialize, Deserialize)] pub struct ValidatorAttestationDataQuery { pub slot: Slot, diff --git a/validator_client/validator_services/src/inclusion_list_service.rs b/validator_client/validator_services/src/inclusion_list_service.rs index 374756b2388..078ef9b4c35 100644 --- a/validator_client/validator_services/src/inclusion_list_service.rs +++ b/validator_client/validator_services/src/inclusion_list_service.rs @@ -1,13 +1,24 @@ use crate::duties_service::DutiesService; use beacon_node_fallback::BeaconNodeFallback; +use eth2::types::{InclusionListDuty, InclusionListTransactions}; +use logging::crit; use slot_clock::SlotClock; use std::ops::Deref; use std::sync::Arc; use task_executor::TaskExecutor; -use tracing::{error, info}; -use types::ChainSpec; +use tokio::time::sleep; +use tracing::{debug, error, info}; +use types::{ChainSpec, EthSpec, Hash256, Slot}; use validator_store::ValidatorStore; +type DependentRoot = Hash256; + +struct InclusionListData { + slot: Slot, + dependent_root: DependentRoot, + transactions: InclusionListTransactions, +} + pub struct Inner { duties_service: Arc>, validator_store: Arc, @@ -83,7 +94,133 @@ where } async fn spawn_inclusion_list_tasks(&self) -> Result<(), String> { - todo!("spawn_inclusion_list_tasks not yet implemented"); + let Some(slot) = self.wait_to_next_slot().await else { + return Ok(()); + }; + + let Some((duties, attestation_data)) = + self.produce_inclusion_list_duties_data(slot).await? + else { + return Ok(()); + }; + + let service = self.clone(); + self.executor.spawn( + async move { + if let Err(e) = service + .sign_and_publish(slot, duties, attestation_data) + .await + { + crit!(error = e, %slot, "Failed to publish inclusion lists"); + } + }, + "inclusion_list_producer", + ); Ok(()) } + + async fn wait_to_next_slot(&self) -> Option { + let slot_duration = self.chain_spec.get_slot_duration(); + + let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() else { + error!("Failed to read slot clock"); + sleep(slot_duration).await; + return None; + }; + + let Some(current_slot) = self.slot_clock.now() else { + error!("Failed to read slot clock after trigger"); + return None; + }; + + // Ensure that the current slot is in the Heze fork + if !self + .chain_spec + .fork_name_at_slot::(current_slot) + .heze_enabled() + { + let duration_to_next_epoch = self + .slot_clock + .duration_to_next_epoch(S::E::slots_per_epoch()) + .unwrap_or_else(|| { + self.chain_spec.get_slot_duration() * S::E::slots_per_epoch() as u32 + }); + sleep(duration_to_next_epoch).await; + return None; + } + + sleep(duration_to_next_slot).await; + + let Some(current_slot) = self.slot_clock.now() else { + error!("Failed to read slot clock after sleep"); + return None; + }; + + Some(current_slot) + } + + /// Produce the inclusion list data for `slot`, returned alongside the duties to sign. + /// + /// Returns `Ok(None)` when there is nothing to publish (no duties, or no inclusion list data for slot) + /// and `Err` when data production failed. + async fn produce_inclusion_list_duties_data( + &self, + slot: Slot, + ) -> Result, InclusionListData)>, String> { + let Some((dependent_root, duties)) = self.duties_service.get_il_duties_for_slot(slot) + else { + return Ok(None); + }; + + if duties.is_empty() { + return Ok(None); + } + + debug!( + %slot, + il_duties_count = duties.len(), + "Producing inclusion lists" + ); + + // Fetch the inclusion list transactions for the given slot + let transactions = self + .beacon_nodes + .first_success(|beacon_node| async move { + beacon_node + .get_validator_inclusion_list(slot) + .await + .map(|resp| resp.data) + }) + .await + .map_err(|e| e.to_string())?; + + debug!( + %slot, + ?dependent_root, + tx_count = transactions.transactions.len(), + tx_bytes = transactions + .transactions + .iter() + .map(|tx| tx.len()) + .sum::(), + "Received inclusion list transactions" + ); + + let il_duties = InclusionListData { + dependent_root, + slot, + transactions, + }; + + Ok(Some((duties, il_duties))) + } + + async fn sign_and_publish( + &self, + slot: Slot, + duties: Vec, + inclusion_list_data: InclusionListData, + ) -> Result<(), String> { + todo!("Signing and publishing ILs is not yet implemented") + } } From 3fe2945c31975c0eb08d402545150e8615420e93 Mon Sep 17 00:00:00 2001 From: conache Date: Fri, 14 Aug 2026 12:08:33 +0300 Subject: [PATCH 14/44] Add client support for publishing inclusion lists --- common/eth2/src/lib.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 08036f041b6..69b5abf0014 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -3573,6 +3573,46 @@ impl BeaconNodeHttpClient { .await } + /// `POST validator/inclusion_list` + pub async fn post_validator_inclusion_list( + &self, + signed_inclusion_list: SignedInclusionList, + fork_name: ForkName, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("inclusion_list"); + + self.post_generic_with_consensus_version(path, &signed_inclusion_list, None, fork_name) + .await?; + + Ok(()) + } + + /// `POST validator/inclusion_list` (SSZ) + pub async fn post_validator_inclusion_list_ssz( + &self, + signed_inclusion_list: SignedInclusionList, + fork_name: ForkName, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("proposer_preferences"); + + let ssz_body = signed_inclusion_list.as_ssz_bytes(); + + self.post_generic_with_consensus_version_and_ssz_body(path, ssz_body, None, fork_name) + .await?; + + Ok(()) + } + /// `POST lighthouse/liveness` pub async fn post_lighthouse_liveness( &self, From 5d02da0e7996384ef3686fa7e6ca43f53e887831 Mon Sep 17 00:00:00 2001 From: conache Date: Fri, 14 Aug 2026 12:31:17 +0300 Subject: [PATCH 15/44] Update endpoints --- common/eth2/src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 69b5abf0014..233ff5d4d85 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -3574,9 +3574,9 @@ impl BeaconNodeHttpClient { } /// `POST validator/inclusion_list` - pub async fn post_validator_inclusion_list( + pub async fn post_validator_inclusion_lists( &self, - signed_inclusion_list: SignedInclusionList, + signed_inclusion_lists: &[SignedInclusionList], fork_name: ForkName, ) -> Result<(), Error> { let mut path = self.eth_path(V1)?; @@ -3586,16 +3586,16 @@ impl BeaconNodeHttpClient { .push("validator") .push("inclusion_list"); - self.post_generic_with_consensus_version(path, &signed_inclusion_list, None, fork_name) + self.post_generic_with_consensus_version(path, &signed_inclusion_lists, None, fork_name) .await?; Ok(()) } /// `POST validator/inclusion_list` (SSZ) - pub async fn post_validator_inclusion_list_ssz( + pub async fn post_validator_inclusion_lists_ssz( &self, - signed_inclusion_list: SignedInclusionList, + signed_inclusion_lists: &Vec, fork_name: ForkName, ) -> Result<(), Error> { let mut path = self.eth_path(V1)?; @@ -3603,9 +3603,9 @@ impl BeaconNodeHttpClient { path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("validator") - .push("proposer_preferences"); + .push("inclusion_list"); - let ssz_body = signed_inclusion_list.as_ssz_bytes(); + let ssz_body = signed_inclusion_lists.as_ssz_bytes(); self.post_generic_with_consensus_version_and_ssz_body(path, ssz_body, None, fork_name) .await?; From a8e58672e17436625b3ff0ba4de0f85046185a5b Mon Sep 17 00:00:00 2001 From: conache Date: Fri, 14 Aug 2026 12:55:48 +0300 Subject: [PATCH 16/44] Correct publish path function to be aligned with the spec --- common/eth2/src/lib.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 233ff5d4d85..99b52d014e2 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -3574,9 +3574,9 @@ impl BeaconNodeHttpClient { } /// `POST validator/inclusion_list` - pub async fn post_validator_inclusion_lists( + pub async fn post_validator_inclusion_list( &self, - signed_inclusion_lists: &[SignedInclusionList], + signed_inclusion_list: &SignedInclusionList, fork_name: ForkName, ) -> Result<(), Error> { let mut path = self.eth_path(V1)?; @@ -3586,16 +3586,18 @@ impl BeaconNodeHttpClient { .push("validator") .push("inclusion_list"); - self.post_generic_with_consensus_version(path, &signed_inclusion_lists, None, fork_name) + // The request body is wrapped in a `data` object, per the beacon-APIs specs + let body = serde_json::json!({ "data": signed_inclusion_list }); + self.post_generic_with_consensus_version(path, &body, None, fork_name) .await?; Ok(()) } /// `POST validator/inclusion_list` (SSZ) - pub async fn post_validator_inclusion_lists_ssz( + pub async fn post_validator_inclusion_list_ssz( &self, - signed_inclusion_lists: &Vec, + signed_inclusion_list: &SignedInclusionList, fork_name: ForkName, ) -> Result<(), Error> { let mut path = self.eth_path(V1)?; @@ -3605,7 +3607,7 @@ impl BeaconNodeHttpClient { .push("validator") .push("inclusion_list"); - let ssz_body = signed_inclusion_lists.as_ssz_bytes(); + let ssz_body = signed_inclusion_list.as_ssz_bytes(); self.post_generic_with_consensus_version_and_ssz_body(path, ssz_body, None, fork_name) .await?; From 8f78f9011bb3ed6b87312c92a7285b0080f97768 Mon Sep 17 00:00:00 2001 From: conache Date: Fri, 14 Aug 2026 14:20:11 +0300 Subject: [PATCH 17/44] Implement sign and publish logic in the inclusion list service --- .../lighthouse_validator_store/src/lib.rs | 23 +++- .../src/inclusion_list_service.rs | 124 ++++++++++++++++-- validator_client/validator_store/src/lib.rs | 17 ++- 3 files changed, 142 insertions(+), 22 deletions(-) diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index ce2b85f3af5..c27ae935e06 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -21,13 +21,14 @@ use tracing::{Instrument, debug, error, info, info_span, instrument, warn}; use types::{ AbstractExecPayload, Address, AggregateAndProof, Attestation, AttestationData, BeaconBlock, BlindedPayload, ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, - ExecutionPayloadEnvelope, Fork, FullPayload, Graffiti, Hash256, PayloadAttestationData, - PayloadAttestationMessage, ProposerPreferences, SelectionProof, SignedAggregateAndProof, - SignedBeaconBlock, SignedContributionAndProof, SignedExecutionPayloadEnvelope, - SignedProposerPreferences, SignedRoot, SignedValidatorRegistrationData, SignedVoluntaryExit, - SingleAttestation, Slot, SyncAggregatorSelectionData, SyncCommitteeContribution, - SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, - VoluntaryExit, graffiti::GraffitiString, + ExecutionPayloadEnvelope, Fork, FullPayload, Graffiti, Hash256, InclusionList, + PayloadAttestationData, PayloadAttestationMessage, ProposerPreferences, SelectionProof, + SignedAggregateAndProof, SignedBeaconBlock, SignedContributionAndProof, + SignedExecutionPayloadEnvelope, SignedInclusionList, SignedProposerPreferences, SignedRoot, + SignedValidatorRegistrationData, SignedVoluntaryExit, SingleAttestation, Slot, + SyncAggregatorSelectionData, SyncCommitteeContribution, SyncCommitteeMessage, + SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, VoluntaryExit, + graffiti::GraffitiString, }; use validator_store::{ AggregateToSign, AttestationToSign, ContributionToSign, DoppelgangerStatus, @@ -1502,4 +1503,12 @@ impl ValidatorStore for LighthouseValidatorS signature, }) } + + async fn sign_inclusion_list( + &self, + validator_pubkey: PublicKeyBytes, + inclusion_list: InclusionList, + ) -> Result { + todo!("sign_inclusion_list not implemented yet"); + } } diff --git a/validator_client/validator_services/src/inclusion_list_service.rs b/validator_client/validator_services/src/inclusion_list_service.rs index 078ef9b4c35..25be9d4b55e 100644 --- a/validator_client/validator_services/src/inclusion_list_service.rs +++ b/validator_client/validator_services/src/inclusion_list_service.rs @@ -8,13 +8,12 @@ use std::sync::Arc; use task_executor::TaskExecutor; use tokio::time::sleep; use tracing::{debug, error, info}; -use types::{ChainSpec, EthSpec, Hash256, Slot}; +use types::{ChainSpec, EthSpec, ForkName, Hash256, InclusionList, SignedInclusionList, Slot}; use validator_store::ValidatorStore; type DependentRoot = Hash256; struct InclusionListData { - slot: Slot, dependent_root: DependentRoot, transactions: InclusionListTransactions, } @@ -94,11 +93,17 @@ where } async fn spawn_inclusion_list_tasks(&self) -> Result<(), String> { + // TODO(heze): consider producing the inclusion list after the slot's envelope is + // revealed instead of right at the start of the slot, keeping the current approach + // as a fallback. Producing at slot start means the list can include transactions + // that the current slot's payload already includes. These would mean redundant constraints + // that put no pressure on the next builder. Building after the envelopes reveal would + // keep only still-pending transactions let Some(slot) = self.wait_to_next_slot().await else { return Ok(()); }; - let Some((duties, attestation_data)) = + let Some((duties, inclusion_list_data)) = self.produce_inclusion_list_duties_data(slot).await? else { return Ok(()); @@ -108,7 +113,7 @@ where self.executor.spawn( async move { if let Err(e) = service - .sign_and_publish(slot, duties, attestation_data) + .sign_and_publish(slot, duties, inclusion_list_data) .await { crit!(error = e, %slot, "Failed to publish inclusion lists"); @@ -161,8 +166,9 @@ where /// Produce the inclusion list data for `slot`, returned alongside the duties to sign. /// - /// Returns `Ok(None)` when there is nothing to publish (no duties, or no inclusion list data for slot) - /// and `Err` when data production failed. + /// Returns `Ok(None)` when there is nothing to produce (the slot's duties have not been + /// downloaded yet, or no local validator has an IL duty at the slot) and `Err` when + /// fetching the inclusion list transactions failed. async fn produce_inclusion_list_duties_data( &self, slot: Slot, @@ -206,13 +212,12 @@ where "Received inclusion list transactions" ); - let il_duties = InclusionListData { + let inclusion_list_data = InclusionListData { dependent_root, - slot, transactions, }; - Ok(Some((duties, il_duties))) + Ok(Some((duties, inclusion_list_data))) } async fn sign_and_publish( @@ -221,6 +226,105 @@ where duties: Vec, inclusion_list_data: InclusionListData, ) -> Result<(), String> { - todo!("Signing and publishing ILs is not yet implemented") + let mut signed_ils = Vec::with_capacity(duties.len()); + + for duty in duties { + let inclusion_list = InclusionList { + slot, + validator_index: duty.validator_index, + dependent_root: inclusion_list_data.dependent_root, + transactions: inclusion_list_data.transactions.transactions.clone(), + }; + + match self + .validator_store + .sign_inclusion_list(duty.pubkey, inclusion_list) + .await + { + Ok(signed_il) => signed_ils.push(signed_il), + Err(e) => { + crit!( + error = ?e, + validator = ?duty.pubkey, + %slot, + "Failed to sign inclusion list" + ); + } + } + } + + if signed_ils.is_empty() { + return Ok(()); + } + + let mut ils_published = 0; + let fork_name = self.chain_spec.fork_name_at_slot::(slot); + for signed_il in &signed_ils { + match self.publish_inclusion_list(signed_il, fork_name).await { + Ok(()) => ils_published += 1, + Err(e) => error!( + %slot, + validator_index = signed_il.message.validator_index, + error = %e, + "Failed to publish inclusion list" + ), + } + } + + if ils_published == 0 { + return Err(format!( + "Failed to publish any of the {} signed inclusion lists", + signed_ils.len() + )); + } + + info!( + %slot, + published = ils_published, + total = signed_ils.len(), + "Published inclusion lists" + ); + + Ok(()) + } + + async fn publish_inclusion_list( + &self, + signed_il: &SignedInclusionList, + fork_name: ForkName, + ) -> Result<(), String> { + let result = self + .beacon_nodes + .first_success(|beacon_node| { + let inclusion_list = signed_il.clone(); + async move { + beacon_node + .post_validator_inclusion_list_ssz(&inclusion_list, fork_name) + .await + .map_err(|e| format!("Failed to publish inclusion list (SSZ): {e:?}")) + } + }) + .await; + + match result { + Ok(()) => Ok(()), + Err(ssz_err) => { + debug!(error = %ssz_err, "SSZ publish failed, falling back to JSON"); + self.beacon_nodes + .first_success(|beacon_node| { + let inclusion_list = signed_il.clone(); + async move { + beacon_node + .post_validator_inclusion_list(&inclusion_list, fork_name) + .await + .map_err(|e| { + format!("Failed to publish inclusion list (JSON): {e:?}") + }) + } + }) + .await + .map_err(|e| e.to_string()) + } + } } } diff --git a/validator_client/validator_store/src/lib.rs b/validator_client/validator_store/src/lib.rs index dde82a2a5bb..eaaa2c8364a 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -7,11 +7,12 @@ use std::future::Future; use std::sync::Arc; use types::{ Address, Attestation, AttestationData, BlindedBeaconBlock, Epoch, EthSpec, - ExecutionPayloadEnvelope, Graffiti, Hash256, PayloadAttestationData, PayloadAttestationMessage, - ProposerPreferences, SelectionProof, SignedAggregateAndProof, SignedBlindedBeaconBlock, - SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedProposerPreferences, - SignedValidatorRegistrationData, SingleAttestation, Slot, SyncCommitteeContribution, - SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, + ExecutionPayloadEnvelope, Graffiti, Hash256, InclusionList, PayloadAttestationData, + PayloadAttestationMessage, ProposerPreferences, SelectionProof, SignedAggregateAndProof, + SignedBlindedBeaconBlock, SignedContributionAndProof, SignedExecutionPayloadEnvelope, + SignedInclusionList, SignedProposerPreferences, SignedValidatorRegistrationData, + SingleAttestation, Slot, SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, + SyncSubnetId, ValidatorRegistrationData, }; #[derive(Debug, PartialEq, Clone)] @@ -217,6 +218,12 @@ pub trait ValidatorStore: Send + Sync { /// `ProposalData` fields include defaulting logic described in `get_fee_recipient_defaulting`, /// `get_gas_limit_defaulting`, and `get_builder_proposals_defaulting`. fn proposal_data(&self, pubkey: &PublicKeyBytes) -> Option; + + fn sign_inclusion_list( + &self, + validator_pubkey: PublicKeyBytes, + data: InclusionList, + ) -> impl Future>> + Send; } #[derive(Debug)] From f8e7a9601800db908983184a7bf101cb4cd28e19 Mon Sep 17 00:00:00 2001 From: conache Date: Fri, 14 Aug 2026 15:02:54 +0300 Subject: [PATCH 18/44] Add signature wiring --- .../lighthouse_validator_store/src/lib.rs | 23 ++++++++++++++++++- validator_client/signing_method/src/lib.rs | 3 +++ .../signing_method/src/web3signer.rs | 4 ++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index c27ae935e06..36ff0aa3d88 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -1509,6 +1509,27 @@ impl ValidatorStore for LighthouseValidatorS validator_pubkey: PublicKeyBytes, inclusion_list: InclusionList, ) -> Result { - todo!("sign_inclusion_list not implemented yet"); + let signing_context = self.signing_context( + Domain::InclusionListCommittee, + inclusion_list.slot.epoch(E::slots_per_epoch()), + ); + + // Inclusion list signing is not slashable, bypass doppelganger protection. + let signing_method = self.doppelganger_bypassed_signing_method(validator_pubkey)?; + + let signature = signing_method + .get_signature::>( + SignableMessage::InclusionList(&inclusion_list), + signing_context, + &self.spec, + &self.task_executor, + ) + .await + .map_err(Error::SpecificError)?; + + Ok(SignedInclusionList { + message: inclusion_list, + signature, + }) } } diff --git a/validator_client/signing_method/src/lib.rs b/validator_client/signing_method/src/lib.rs index 0dfde989464..aafbccfe367 100644 --- a/validator_client/signing_method/src/lib.rs +++ b/validator_client/signing_method/src/lib.rs @@ -52,6 +52,7 @@ pub enum SignableMessage<'a, E: EthSpec, Payload: AbstractExecPayload = FullP ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope), PayloadAttestationData(&'a PayloadAttestationData), ProposerPreferences(&'a ProposerPreferences), + InclusionList(&'a InclusionList), } impl> SignableMessage<'_, E, Payload> { @@ -76,6 +77,7 @@ impl> SignableMessage<'_, E, Payload SignableMessage::ExecutionPayloadEnvelope(e) => e.signing_root(domain), SignableMessage::PayloadAttestationData(d) => d.signing_root(domain), SignableMessage::ProposerPreferences(p) => p.signing_root(domain), + SignableMessage::InclusionList(l) => l.signing_root(domain), } } } @@ -248,6 +250,7 @@ impl SigningMethod { SignableMessage::ProposerPreferences(p) => { Web3SignerObject::ProposerPreferences(p) } + SignableMessage::InclusionList(l) => Web3SignerObject::InclusionList(l), }; // Determine the Web3Signer message type. diff --git a/validator_client/signing_method/src/web3signer.rs b/validator_client/signing_method/src/web3signer.rs index 8548a933e66..2c0b7660970 100644 --- a/validator_client/signing_method/src/web3signer.rs +++ b/validator_client/signing_method/src/web3signer.rs @@ -23,6 +23,8 @@ pub enum MessageType { ExecutionPayloadEnvelope, PayloadAttestation, ProposerPreferences, + // TODO(heze) verify w/ web3signer specs + InclusionList, } #[derive(Debug, PartialEq, Copy, Clone, Serialize)] @@ -83,6 +85,7 @@ pub enum Web3SignerObject<'a, E: EthSpec, Payload: AbstractExecPayload> { ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope), PayloadAttestationData(&'a PayloadAttestationData), ProposerPreferences(&'a ProposerPreferences), + InclusionList(&'a InclusionList), } impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Payload> { @@ -156,6 +159,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Pa Web3SignerObject::ExecutionPayloadEnvelope(_) => MessageType::ExecutionPayloadEnvelope, Web3SignerObject::PayloadAttestationData(_) => MessageType::PayloadAttestation, Web3SignerObject::ProposerPreferences(_) => MessageType::ProposerPreferences, + Web3SignerObject::InclusionList(_) => MessageType::InclusionList, } } } From 84c9ec13a33cb8dc6a457475e23e663de99a266b Mon Sep 17 00:00:00 2001 From: conache Date: Fri, 14 Aug 2026 19:06:57 +0300 Subject: [PATCH 19/44] Scaffold test harness and add tests for slot advancement and production path error cases --- .../src/mock_beacon_node.rs | 110 +++++++- .../src/inclusion_list_service.rs | 238 ++++++++++++++++++ 2 files changed, 346 insertions(+), 2 deletions(-) diff --git a/testing/validator_test_rig/src/mock_beacon_node.rs b/testing/validator_test_rig/src/mock_beacon_node.rs index d01905c0c7e..74973979c40 100644 --- a/testing/validator_test_rig/src/mock_beacon_node.rs +++ b/testing/validator_test_rig/src/mock_beacon_node.rs @@ -1,4 +1,4 @@ -use eth2::types::{GenericResponse, PublishBlockRequest, SyncingData}; +use eth2::types::{GenericResponse, InclusionListTransactions, PublishBlockRequest, SyncingData}; use eth2::{BLOB_DATA_INCLUDED_HEADER, BeaconNodeHttpClient, CONSENSUS_VERSION_HEADER, Timeouts}; use mockito::{Matcher, Mock, Server, ServerGuard}; use regex::Regex; @@ -13,7 +13,7 @@ use tracing::info; use types::{ BeaconBlock, ChainSpec, ConfigAndPreset, EthSpec, ExecutionPayloadEnvelope, ForkName, Hash256, PayloadAttestationData, PayloadAttestationMessage, SignedBlindedBeaconBlock, - SignedExecutionPayloadEnvelope, Slot, + SignedExecutionPayloadEnvelope, SignedInclusionList, Slot, }; pub struct MockBeaconNode { @@ -24,6 +24,7 @@ pub struct MockBeaconNode { pub received_full_blocks: Arc>>>, pub execution_payload_envelope: Arc>>>, pub payload_attestation_message: Arc>>, + pub received_inclusion_lists: Arc>>, } impl MockBeaconNode { @@ -42,6 +43,7 @@ impl MockBeaconNode { received_full_blocks: Arc::new(Mutex::new(Vec::new())), execution_payload_envelope: Arc::new(Mutex::new(Vec::new())), payload_attestation_message: Arc::new(Mutex::new(Vec::new())), + received_inclusion_lists: Arc::new(Mutex::new(Vec::new())), } } @@ -270,6 +272,38 @@ impl MockBeaconNode { .create() } + /// Mocks `GET /eth/v1/validator/inclusion_list` + pub fn mock_get_validator_inclusion_list( + &mut self, + transactions: &InclusionListTransactions, + slot: Slot, + ) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/validator/inclusion_list$").unwrap(); + + let data = GenericResponse::from(transactions.clone()); + + self.server + .mock("GET", Matcher::Regex(path_pattern.to_string())) + .match_query(Matcher::UrlEncoded("slot".into(), slot.to_string())) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(serde_json::to_string(&data).unwrap()) + .create() + } + + /// Mocks `GET /eth/v1/validator/inclusion_list` returning error + pub fn mock_get_validator_inclusion_list_error(&mut self, slot: Slot) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/validator/inclusion_list$").unwrap(); + + self.server + .mock("GET", Matcher::Regex(path_pattern.to_string())) + .match_query(Matcher::UrlEncoded("slot".into(), slot.to_string())) + .with_status(500) + .with_header("content-type", "application/json") + .with_body(r#"{"message":"Internal server error"}"#) + .create() + } + /// Mocks the `post_beacon_blinded_blocks_v2_ssz` response with an optional `delay`. pub fn mock_post_beacon_blinded_blocks_v2_ssz(&mut self, delay: Duration) -> Mock { let path_pattern = Regex::new(r"^/eth/v2/beacon/blinded_blocks$").unwrap(); @@ -459,4 +493,76 @@ impl MockBeaconNode { .with_body(r#"{"message":"Internal server error"}"#) .create() } + + /// Mocks `POST /eth/v1/validator/inclusion_list` (JSON) to receive signed inclusion lists. + pub fn mock_post_validator_inclusion_list_json(&mut self, fork_name: ForkName) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/validator/inclusion_list$").unwrap(); + + let received_inclusion_lists = Arc::clone(&self.received_inclusion_lists); + + self.server + .mock("POST", Matcher::Regex(path_pattern.to_string())) + .match_header("content-type", "application/json") + .match_header(CONSENSUS_VERSION_HEADER, fork_name.to_string().as_str()) + .with_status(200) + .with_body_from_request(move |request| { + let body = request.body().expect("Failed to get request body"); + // The request body is wrapped in a `data` object, per the beacon-APIs specs + let wrapper: GenericResponse = serde_json::from_slice(body) + .expect("Failed to deserialize SignedInclusionList from JSON"); + received_inclusion_lists.lock().unwrap().push(wrapper.data); + vec![] + }) + .create() + } + + /// Mocks `POST /eth/v1/validator/inclusion_list` (JSON) returning error + pub fn mock_post_validator_inclusion_list_json_error(&mut self, fork_name: ForkName) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/validator/inclusion_list$").unwrap(); + + self.server + .mock("POST", Matcher::Regex(path_pattern.to_string())) + .match_header("content-type", "application/json") + .match_header(CONSENSUS_VERSION_HEADER, fork_name.to_string().as_str()) + .with_status(500) + .with_body(r#"{"message":"Internal server error"}"#) + .create() + } + + /// Mocks `POST /eth/v1/validator/inclusion_list` (SSZ) to receive signed inclusion lists. + pub fn mock_post_validator_inclusion_list_ssz(&mut self, fork_name: ForkName) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/validator/inclusion_list$").unwrap(); + + let received_inclusion_lists = Arc::clone(&self.received_inclusion_lists); + + self.server + .mock("POST", Matcher::Regex(path_pattern.to_string())) + .match_header("content-type", "application/octet-stream") + .match_header(CONSENSUS_VERSION_HEADER, fork_name.to_string().as_str()) + .with_status(200) + .with_body_from_request(move |request| { + let body = request.body().expect("Failed to get request body"); + let inclusion_list = SignedInclusionList::from_ssz_bytes(body) + .expect("Failed to deserialize SignedInclusionList from SSZ"); + received_inclusion_lists + .lock() + .unwrap() + .push(inclusion_list); + vec![] + }) + .create() + } + + /// Mocks `POST /eth/v1/validator/inclusion_list` (SSZ) returning error + pub fn mock_post_validator_inclusion_list_ssz_error(&mut self, fork_name: ForkName) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/validator/inclusion_list$").unwrap(); + + self.server + .mock("POST", Matcher::Regex(path_pattern.to_string())) + .match_header("content-type", "application/octet-stream") + .match_header(CONSENSUS_VERSION_HEADER, fork_name.to_string().as_str()) + .with_status(500) + .with_body(r#"{"message":"Internal server error"}"#) + .create() + } } diff --git a/validator_client/validator_services/src/inclusion_list_service.rs b/validator_client/validator_services/src/inclusion_list_service.rs index 25be9d4b55e..f32014867ee 100644 --- a/validator_client/validator_services/src/inclusion_list_service.rs +++ b/validator_client/validator_services/src/inclusion_list_service.rs @@ -328,3 +328,241 @@ where } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::duties_service::DutiesServiceBuilder; + use futures::FutureExt; + use slot_clock::ManualSlotClock; + use std::time::Duration; + use types::{Epoch, MainnetEthSpec}; + use validator_test_rig::validator_client_harness::{S, ValidatorClientHarness}; + + type E = MainnetEthSpec; + + struct TestHarness { + harness: ValidatorClientHarness, + service: InclusionListService, + } + + impl TestHarness { + async fn new_with_validators(num_validators: usize) -> Self { + Self::new_with_heze_at(num_validators, Epoch::new(0)).await + } + + async fn new_with_heze_at(num_validators: usize, heze_fork_epoch: Epoch) -> Self { + let harness = ValidatorClientHarness::new(num_validators).await; + + let mut spec = (*harness.spec).clone(); + spec.heze_fork_epoch = Some(heze_fork_epoch); + let spec = Arc::new(spec); + + let duties_service = Arc::new( + DutiesServiceBuilder::new() + .validator_store(harness.validator_store.clone()) + .slot_clock(harness.slot_clock.clone()) + .beacon_nodes(harness.beacon_nodes.clone()) + .executor(harness.test_runtime.task_executor.clone()) + .spec(spec.clone()) + .build() + .unwrap(), + ); + + let service = InclusionListService::new( + duties_service, + harness.validator_store.clone(), + harness.slot_clock.clone(), + harness.beacon_nodes.clone(), + harness.test_runtime.task_executor.clone(), + spec, + ); + + Self { harness, service } + } + + fn insert_il_duties(&self, slot: Slot, dependent_root: Hash256) { + let duties = self + .harness + .pubkeys + .iter() + .enumerate() + .map(|(i, pubkey)| InclusionListDuty { + pubkey: *pubkey, + validator_index: i as u64, + slot, + inclusion_list_committee_root: Hash256::ZERO, + }) + .collect(); + self.service + .duties_service + .il_duties + .write() + .insert(slot.epoch(E::slots_per_epoch()), (dependent_root, duties)); + } + } + + async fn advance_time(slot_clock: &ManualSlotClock, duration: Duration) { + slot_clock.advance_time(duration); + tokio::time::advance(duration).await; + } + + /// Pre-Heze the wait sleeps to the next epoch and returns `None` + /// No duties are read and no BN request is made + #[tokio::test] + async fn waits_until_next_epoch_before_heze_fork() { + tokio::time::pause(); + + let harness = TestHarness::new_with_heze_at(1, Epoch::new(1)).await; + let service = &harness.service; + + // Add duties for a pre-Heze slot + // If the il task execution leaks past the wait_for_next_slot check, + // it would fetch the transactions from the mock BNs and fail the final assertion + // in the test + harness.insert_il_duties(Slot::new(1), Hash256::repeat_byte(0xab)); + let service_wait = service.spawn_inclusion_list_tasks(); + tokio::pin!(service_wait); + + // This first call of .now_or_never() starts the timer and registers the sleep timer with tokio + // It calls sleep(duration_to_next_epoch).await which registers a timer with a deadline of 12s * 32 + assert!(service_wait.as_mut().now_or_never().is_none()); + + // Advance both slot_clock and tokio::time slot by slot up to 384s (the sleep deadline) + // This verifies that wait_to_next_slot waits a whole epoch (not just a slot) before completing + for _ in 0..E::slots_per_epoch() { + let duration_to_next_slot = harness.service.slot_clock.duration_to_next_slot().unwrap(); + advance_time(&harness.service.slot_clock, duration_to_next_slot).await; + assert!( + service_wait.as_mut().now_or_never().is_none(), + "Function should return None before the sleep duration has elapsed" + ); + } + + // Advance time for 1 more second, past the epoch boundary. + // The epoch sleep should have completed and the execution should complete as a no-op. + // This call should yield no slot, so nothing should be produced, signed or published. + advance_time(&harness.service.slot_clock, Duration::from_secs(1)).await; + assert_eq!(service_wait.as_mut().now_or_never(), Some(Ok(()))); + } + + #[tokio::test] + async fn waits_until_next_slot() { + tokio::time::pause(); + + let harness = TestHarness::new_with_validators(1).await; + let service = &harness.service; + let service_wait = service.wait_to_next_slot(); + tokio::pin!(service_wait); + + // Start the timer and registers the sleep timer with tokio + assert!(service_wait.as_mut().now_or_never().is_none()); + + let duration_to_wait = harness.service.slot_clock.duration_to_next_slot().unwrap(); + // Advance both slot_clock and tokio::time to 12s + advance_time(&harness.service.slot_clock, duration_to_wait).await; + assert!( + service_wait.as_mut().now_or_never().is_none(), + "Function should return None before the sleep duration has elapsed" + ); + + advance_time(&harness.service.slot_clock, Duration::from_secs(1)).await; + assert_eq!( + service_wait.as_mut().now_or_never().unwrap(), + Some(Slot::new(1)) + ); + } + + #[tokio::test] + async fn no_duties_no_fetch_no_publish() { + let mut harness = TestHarness::new_with_validators(3).await; + let current_slot = harness.service.slot_clock.now().unwrap(); + + let transactions = InclusionListTransactions { + transactions: Default::default(), + }; + let fetch_mock = harness + .harness + .mock_beacon_node_1 + .mock_get_validator_inclusion_list(&transactions, current_slot); + let publish_mock = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_ssz(ForkName::Heze); + + // Duties for the slot's epoch not downloaded yet + let result = harness + .service + .produce_inclusion_list_duties_data(current_slot) + .await; + assert!(result.unwrap().is_none()); + + // Add duty for next slot, not for the current one + harness.insert_il_duties(current_slot + 1, Hash256::repeat_byte(0xab)); + let result = harness + .service + .produce_inclusion_list_duties_data(current_slot) + .await; + assert!(result.unwrap().is_none()); + + fetch_mock.expect(0).assert(); + publish_mock.expect(0).assert(); + } + + /// Produce endpoint fails on all BNs: produce returns `Err`, nothing is signed or + /// published, and the main service loop handles the error + #[tokio::test] + async fn produce_fetch_error_aborts_slot() { + let mut harness = TestHarness::new_with_validators(3).await; + let slot = Slot::new(1); + harness.insert_il_duties(slot, Hash256::repeat_byte(0xab)); + + harness + .harness + .mock_beacon_node_1 + .mock_get_validator_inclusion_list_error(slot); + harness + .harness + .mock_beacon_node_2 + .mock_get_validator_inclusion_list_error(slot); + + let result = harness + .service + .produce_inclusion_list_duties_data(slot) + .await; + assert!(result.is_err()); + } + + /// First BN errors on the produce endpoint, second serves: `first_success` walks + /// past and transactions come from the second BN. + #[tokio::test] + async fn produce_falls_back_to_second_bn() { + let mut harness = TestHarness::new_with_validators(3).await; + let slot = Slot::new(1); + let dependent_root = Hash256::repeat_byte(0xab); + harness.insert_il_duties(slot, dependent_root); + + let transactions = InclusionListTransactions { + transactions: vec![vec![0xaa; 3].into()].into(), + }; + harness + .harness + .mock_beacon_node_1 + .mock_get_validator_inclusion_list_error(slot); + let bn2_mock = harness + .harness + .mock_beacon_node_2 + .mock_get_validator_inclusion_list(&transactions, slot); + + let (duties, data) = harness + .service + .produce_inclusion_list_duties_data(slot) + .await + .unwrap() + .unwrap(); + assert_eq!(duties.len(), 3); + assert_eq!(data.dependent_root, dependent_root); + assert_eq!(data.transactions, transactions); + bn2_mock.expect(1).assert(); + } +} From 26b931949801cb2b76617788d3b7c8cdf9d58f26 Mon Sep 17 00:00:00 2001 From: conache Date: Fri, 14 Aug 2026 19:44:45 +0300 Subject: [PATCH 20/44] Add publish tests --- .../src/inclusion_list_service.rs | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) diff --git a/validator_client/validator_services/src/inclusion_list_service.rs b/validator_client/validator_services/src/inclusion_list_service.rs index f32014867ee..cb2f047abc5 100644 --- a/validator_client/validator_services/src/inclusion_list_service.rs +++ b/validator_client/validator_services/src/inclusion_list_service.rs @@ -565,4 +565,204 @@ mod tests { assert_eq!(data.transactions, transactions); bn2_mock.expect(1).assert(); } + + #[tokio::test] + async fn publishes_each_inclusion_list_via_ssz() { + let mut harness = TestHarness::new_with_validators(3).await; + let slot = Slot::new(1); + let dependent_root = Hash256::repeat_byte(0xab); + harness.insert_il_duties(slot, dependent_root); + + let transactions = InclusionListTransactions { + transactions: vec![vec![0xaa; 3].into()].into(), + }; + harness + .harness + .mock_beacon_node_1 + .mock_get_validator_inclusion_list(&transactions, slot); + let ssz_mock = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_ssz(ForkName::Heze); + let json_mock = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_json(ForkName::Heze); + + let (duties, data) = harness + .service + .produce_inclusion_list_duties_data(slot) + .await + .unwrap() + .unwrap(); + harness + .service + .sign_and_publish(slot, duties, data) + .await + .unwrap(); + + // One POST per duty,and no JSON fallback + ssz_mock.expect(3).assert(); + json_mock.expect(0).assert(); + + let messages = harness + .harness + .mock_beacon_node_1 + .received_inclusion_lists + .lock() + .unwrap(); + assert_eq!(messages.len(), 3); + + // Check each message + for (i, signed_il) in messages.iter().enumerate() { + assert_eq!(signed_il.message.validator_index, i as u64); + assert_eq!(signed_il.message.slot, slot); + assert_eq!(signed_il.message.dependent_root, dependent_root); + assert_eq!(signed_il.message.transactions, transactions.transactions); + } + } + + #[tokio::test] + async fn inclusion_list_ssz_publish_falls_back_to_json() { + let mut harness = TestHarness::new_with_validators(1).await; + let slot = Slot::new(1); + harness.insert_il_duties(slot, Hash256::repeat_byte(0xab)); + + let transactions = InclusionListTransactions { + transactions: Default::default(), + }; + harness + .harness + .mock_beacon_node_1 + .mock_get_validator_inclusion_list(&transactions, slot); + let ssz_mock = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_ssz_error(ForkName::Heze); + let json_mock = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_json(ForkName::Heze); + + let (duties, data) = harness + .service + .produce_inclusion_list_duties_data(slot) + .await + .unwrap() + .unwrap(); + harness + .service + .sign_and_publish(slot, duties, data) + .await + .unwrap(); + + // `first_success` makes two passes over the BNs, so the failing SSZ mock is hit twice + ssz_mock.expect(2).assert(); + json_mock.expect(1).assert(); + + let messages = harness + .harness + .mock_beacon_node_1 + .received_inclusion_lists + .lock() + .unwrap(); + assert_eq!(messages.len(), 1); + } + + /// One duty's publish fails on both encodings + /// so the succeeded calls are total - 1 + #[tokio::test] + async fn partial_publish_failure_still_publishes_siblings() { + let mut harness = TestHarness::new_with_validators(2).await; + let slot = Slot::new(1); + harness.insert_il_duties(slot, Hash256::repeat_byte(0xab)); + + let transactions = InclusionListTransactions { + transactions: Default::default(), + }; + harness + .harness + .mock_beacon_node_1 + .mock_get_validator_inclusion_list(&transactions, slot); + + // Each error mock answers exactly one request: the first duty's publish fails on + // both `first_success` passes of both encodings + let ssz_err_1 = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_ssz_error(ForkName::Heze); + let json_err_1 = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_json_error(ForkName::Heze); + let ssz_err_2 = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_ssz_error(ForkName::Heze); + let json_err_2 = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_json_error(ForkName::Heze); + // With the error mocks above spent, the sibling duty publishes here + let ssz_mock = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_ssz(ForkName::Heze); + + let (duties, data) = harness + .service + .produce_inclusion_list_duties_data(slot) + .await + .unwrap() + .unwrap(); + harness + .service + .sign_and_publish(slot, duties, data) + .await + .unwrap(); + + ssz_err_1.expect(1).assert(); + ssz_err_2.expect(1).assert(); + json_err_1.expect(1).assert(); + json_err_2.expect(1).assert(); + // successful call + ssz_mock.expect(1).assert(); + + let messages = harness + .harness + .mock_beacon_node_1 + .received_inclusion_lists + .lock() + .unwrap(); + let indices: Vec = messages + .iter() + .map(|il| il.message.validator_index) + .collect(); + assert_eq!(indices, vec![1]); + } + + #[tokio::test] + async fn total_publish_failure_returns_error() { + let mut harness = TestHarness::new_with_validators(2).await; + let slot = Slot::new(1); + harness.insert_il_duties(slot, Hash256::repeat_byte(0xab)); + + let transactions = InclusionListTransactions { + transactions: Default::default(), + }; + // No POST routes are registered: every publish fails on both encodings + harness + .harness + .mock_beacon_node_1 + .mock_get_validator_inclusion_list(&transactions, slot); + + let (duties, data) = harness + .service + .produce_inclusion_list_duties_data(slot) + .await + .unwrap() + .unwrap(); + let result = harness.service.sign_and_publish(slot, duties, data).await; + assert!(result.is_err()); + } } From 1995a483db347688af3ba62c94bb6601693e6600 Mon Sep 17 00:00:00 2001 From: conache Date: Sun, 16 Aug 2026 12:53:00 +0300 Subject: [PATCH 21/44] Add signature check tests --- .../src/inclusion_list_service.rs | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/validator_client/validator_services/src/inclusion_list_service.rs b/validator_client/validator_services/src/inclusion_list_service.rs index cb2f047abc5..3c9de0b4a90 100644 --- a/validator_client/validator_services/src/inclusion_list_service.rs +++ b/validator_client/validator_services/src/inclusion_list_service.rs @@ -336,7 +336,8 @@ mod tests { use futures::FutureExt; use slot_clock::ManualSlotClock; use std::time::Duration; - use types::{Epoch, MainnetEthSpec}; + use types::test_utils::generate_deterministic_keypair; + use types::{Domain, Epoch, MainnetEthSpec, SignedRoot}; use validator_test_rig::validator_client_harness::{S, ValidatorClientHarness}; type E = MainnetEthSpec; @@ -620,6 +621,9 @@ mod tests { assert_eq!(signed_il.message.dependent_root, dependent_root); assert_eq!(signed_il.message.transactions, transactions.transactions); } + // Each message carries its own validator's signature + assert_ne!(messages[0].signature, messages[1].signature); + assert_ne!(messages[1].signature, messages[2].signature); } #[tokio::test] @@ -765,4 +769,78 @@ mod tests { let result = harness.service.sign_and_publish(slot, duties, data).await; assert!(result.is_err()); } + + #[tokio::test] + async fn signed_inclusion_list_verifies_against_consensus_signature_check() { + let harness = TestHarness::new_with_validators(1).await; + let slot = Slot::new(1); + let pubkey = harness.harness.pubkeys[0]; + + let inclusion_list = InclusionList { + slot, + validator_index: 0, + dependent_root: Hash256::repeat_byte(0xab), + transactions: Default::default(), + }; + let signed = harness + .harness + .validator_store + .sign_inclusion_list(pubkey, inclusion_list) + .await + .unwrap(); + + // Independent consensus-side derivation + let spec = &harness.harness.spec; + let epoch = slot.epoch(E::slots_per_epoch()); + let fork = spec.fork_at_epoch(epoch); + let domain = spec.get_domain(epoch, Domain::InclusionListCommittee, &fork, Hash256::ZERO); + let signing_root = signed.message.signing_root(domain); + assert!( + signed + .signature + .verify(&pubkey.decompress().unwrap(), signing_root) + ); + } + + #[tokio::test] + async fn sign_failure_for_unknown_validator_skips_publish() { + let mut harness = TestHarness::new_with_validators(1).await; + let slot = Slot::new(1); + + // A duty for a validator the store does not hold + let duty = InclusionListDuty { + pubkey: generate_deterministic_keypair(99).pk.into(), + validator_index: 99, + slot, + inclusion_list_committee_root: Hash256::ZERO, + }; + harness.service.duties_service.il_duties.write().insert( + slot.epoch(E::slots_per_epoch()), + (Hash256::repeat_byte(0xab), vec![duty]), + ); + + let transactions = InclusionListTransactions { + transactions: Default::default(), + }; + harness + .harness + .mock_beacon_node_1 + .mock_get_validator_inclusion_list(&transactions, slot); + let ssz_mock = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_ssz(ForkName::Heze); + + let (duties, data) = harness + .service + .produce_inclusion_list_duties_data(slot) + .await + .unwrap() + .unwrap(); + let result = harness.service.sign_and_publish(slot, duties, data).await; + + // Nothing signed means nothing to publish, not a total publish failure + assert_eq!(result, Ok(())); + ssz_mock.expect(0).assert(); + } } From 66d0a9ef33be4adc4f066d2d75c36f89697af48c Mon Sep 17 00:00:00 2001 From: conache Date: Sun, 16 Aug 2026 13:56:04 +0300 Subject: [PATCH 22/44] Cleanup --- common/eth2/src/lib.rs | 6 +++++- validator_client/validator_store/src/lib.rs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 99b52d014e2..a35eeae499a 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -81,6 +81,7 @@ const HTTP_SYNC_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_SYNC_AGGREGATOR_TIMEOUT_QUOTIENT: u32 = 24; // For DVT involving middleware only // TODO(EIP-7732): Determine what this quotient should be const HTTP_PTC_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; +const HTTP_INCLUSION_LIST_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_INCLUSION_LIST_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT: u32 = 4; @@ -104,6 +105,7 @@ pub struct Timeouts { pub sync_duties: Duration, pub sync_aggregators: Duration, pub ptc_duties: Duration, + pub inclusion_list: Duration, pub inclusion_list_duties: Duration, pub get_beacon_blocks_ssz: Duration, pub get_debug_beacon_states: Duration, @@ -127,6 +129,7 @@ impl Timeouts { sync_duties: timeout, sync_aggregators: timeout, ptc_duties: timeout, + inclusion_list: timeout, inclusion_list_duties: timeout, get_beacon_blocks_ssz: timeout, get_debug_beacon_states: timeout, @@ -152,6 +155,7 @@ impl Timeouts { sync_duties: base_timeout / HTTP_SYNC_DUTIES_TIMEOUT_QUOTIENT, sync_aggregators: base_timeout / HTTP_SYNC_AGGREGATOR_TIMEOUT_QUOTIENT, ptc_duties: base_timeout / HTTP_PTC_DUTIES_TIMEOUT_QUOTIENT, + inclusion_list: base_timeout / HTTP_INCLUSION_LIST_TIMEOUT_QUOTIENT, inclusion_list_duties: base_timeout / HTTP_INCLUSION_LIST_DUTIES_TIMEOUT_QUOTIENT, get_beacon_blocks_ssz: base_timeout / HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT, get_debug_beacon_states: base_timeout / HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT, @@ -3569,7 +3573,7 @@ impl BeaconNodeHttpClient { path.query_pairs_mut() .append_pair("slot", &slot.to_string()); - self.get_with_timeout(path, self.timeouts.inclusion_list_duties) + self.get_with_timeout(path, self.timeouts.inclusion_list) .await } diff --git a/validator_client/validator_store/src/lib.rs b/validator_client/validator_store/src/lib.rs index eaaa2c8364a..e5462b2aabe 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -222,7 +222,7 @@ pub trait ValidatorStore: Send + Sync { fn sign_inclusion_list( &self, validator_pubkey: PublicKeyBytes, - data: InclusionList, + inclusion_list: InclusionList, ) -> impl Future>> + Send; } From d651d8ebe7a48810223da0da91d1235e7b2b33d0 Mon Sep 17 00:00:00 2001 From: conache Date: Fri, 21 Aug 2026 19:31:55 +0300 Subject: [PATCH 23/44] Drop inclusion_list_committee_root --- .../validator_services/src/inclusion_list_service.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/validator_client/validator_services/src/inclusion_list_service.rs b/validator_client/validator_services/src/inclusion_list_service.rs index 3c9de0b4a90..50dc979de2f 100644 --- a/validator_client/validator_services/src/inclusion_list_service.rs +++ b/validator_client/validator_services/src/inclusion_list_service.rs @@ -392,7 +392,6 @@ mod tests { pubkey: *pubkey, validator_index: i as u64, slot, - inclusion_list_committee_root: Hash256::ZERO, }) .collect(); self.service @@ -812,7 +811,6 @@ mod tests { pubkey: generate_deterministic_keypair(99).pk.into(), validator_index: 99, slot, - inclusion_list_committee_root: Hash256::ZERO, }; harness.service.duties_service.il_duties.write().insert( slot.epoch(E::slots_per_epoch()), From 77eae3cb9f406d73cd8d5cf401e1de14bfb3c172 Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 3 Sep 2026 11:57:24 +0300 Subject: [PATCH 24/44] Use fully spelled out metric name for IL committee count --- validator_client/validator_metrics/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validator_client/validator_metrics/src/lib.rs b/validator_client/validator_metrics/src/lib.rs index 812140d018a..517197844fa 100644 --- a/validator_client/validator_metrics/src/lib.rs +++ b/validator_client/validator_metrics/src/lib.rs @@ -181,7 +181,7 @@ pub static PTC_COUNT: LazyLock> = LazyLock::new(|| { }); pub static IL_COUNT: LazyLock> = LazyLock::new(|| { try_create_int_gauge_vec( - "vc_beacon_il_count", + "vc_beacon_inclusion_list_committee_count", "Number of IL (Inclusion List Committee) validators on this host", &["task"], ) From c95581e8fa5729f06ba03cbbf53402997270d4d4 Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 3 Sep 2026 11:57:56 +0300 Subject: [PATCH 25/44] Remove redundant epoch checks for both heze and gloas --- .../validator_services/src/duties_service.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index 64add327bff..f0bb858cd58 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -753,7 +753,7 @@ pub fn start_update_service // Spawn the task which keeps track of local PTC duties. // Only start PTC duties service if Gloas fork is scheduled. - if core_duties_service.spec.is_gloas_scheduled() { + if let Some(gloas_fork_epoch) = core_duties_service.spec.gloas_fork_epoch { let duties_service = core_duties_service.clone(); core_duties_service.executor.spawn( async move { @@ -766,10 +766,6 @@ pub fn start_update_service }; let current_epoch = current_slot.epoch(S::E::slots_per_epoch()); - let Some(gloas_fork_epoch) = duties_service.spec.gloas_fork_epoch else { - // Gloas fork epoch not configured, should not reach here - break; - }; if current_epoch + 1 < gloas_fork_epoch { // Wait until the next slot and check again @@ -808,7 +804,7 @@ pub fn start_update_service // Spawn the task which keeps track of the inclusion list committee duties. // Only track IL committee duties if the heze fork is scheduled - if core_duties_service.spec.is_heze_scheduled() { + if let Some(heze_fork_epoch) = core_duties_service.spec.heze_fork_epoch { let duties_service = core_duties_service.clone(); core_duties_service.executor.spawn( async move { @@ -820,10 +816,6 @@ pub fn start_update_service }; let current_epoch = current_slot.epoch(S::E::slots_per_epoch()); - let Some(heze_fork_epoch) = duties_service.spec.heze_fork_epoch else { - // Heze fork epoch not configured - break; - }; if current_epoch + 1 < heze_fork_epoch { // Wait until the next slot and check again if the Heze fork epoch is close From 33e3294b1a15129331bb43f70ccedfb792141da7 Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 3 Sep 2026 12:39:50 +0300 Subject: [PATCH 26/44] Cleanup logic for detecting if we need to update validators il duties for an epoch --- .../validator_services/src/duties_service.rs | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index f0bb858cd58..83c91e8821e 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -2186,33 +2186,27 @@ async fn poll_beacon_il_duties_for_epoch>() - } else { - Vec::new() - } + }) }; - if validators_to_update.is_empty() { + if !needs_update { // No validators have conflicting (epoch, dependent_root) values for this epoch. return Ok(()); } // Make a request for all indices that require updating which we have not already made a request for. - let indices_to_request = validators_to_update + let indices_to_request = local_indices .iter() - .filter_map(|pubkey| duties_service.validator_store.validator_index(pubkey)) .filter(|validator_index| !initial_indices_to_request.contains(validator_index)) + .copied() .collect::>(); // Filter the initial duties by their relevance so that we don't hit warnings about @@ -2220,7 +2214,7 @@ async fn poll_beacon_il_duties_for_epoch Date: Thu, 3 Sep 2026 18:12:02 +0300 Subject: [PATCH 27/44] Consolidate PTC and IL duties polling into a single generic function --- Cargo.lock | 1 + .../validator_services/Cargo.toml | 1 + .../validator_services/src/duties_service.rs | 395 +++++++++--------- 3 files changed, 188 insertions(+), 209 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2900ac29632..895c2d21c17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9982,6 +9982,7 @@ dependencies = [ "parking_lot", "reqwest", "safe_arith", + "serde", "slot_clock", "task_executor", "tokio", diff --git a/validator_client/validator_services/Cargo.toml b/validator_client/validator_services/Cargo.toml index 625eee85bdb..2f9a4c2b5d3 100644 --- a/validator_client/validator_services/Cargo.toml +++ b/validator_client/validator_services/Cargo.toml @@ -15,6 +15,7 @@ logging = { workspace = true } parking_lot = { workspace = true } reqwest = { workspace = true } safe_arith = { workspace = true } +serde = { workspace = true } slot_clock = { workspace = true } task_executor = { workspace = true } tokio = { workspace = true } diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index 83c91e8821e..de255ef5806 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -21,9 +21,11 @@ use futures::{ }; use parking_lot::{RwLock, RwLockWriteGuard}; use safe_arith::{ArithError, SafeArith}; +use serde::{Serialize, de::DeserializeOwned}; use slot_clock::SlotClock; use std::cmp::min; use std::collections::{BTreeMap, HashMap, HashSet, hash_map}; +use std::fmt::{Display, Formatter}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; @@ -46,8 +48,6 @@ const VALIDATOR_METRICS_MIN_COUNT: usize = 64; /// The initial request is used to determine if further requests are required, so that it /// reduces the amount of data that needs to be transferred. const INITIAL_DUTIES_QUERY_SIZE: usize = 1; -const INITIAL_PTC_DUTIES_QUERY_SIZE: usize = 1; -const INITIAL_IL_DUTIES_QUERY_SIZE: usize = 1; /// Offsets from the attestation duty slot at which a subscription should be sent. const ATTESTATION_SUBSCRIPTION_OFFSETS: [u64; 8] = [3, 4, 5, 6, 7, 8, 16, 32]; @@ -282,6 +282,67 @@ impl SubscriptionSlots { } } +enum LookaheadDutyKind { + Ptc, + InclusionList, +} + +impl Display for LookaheadDutyKind { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + LookaheadDutyKind::Ptc => write!(f, "PTC"), + LookaheadDutyKind::InclusionList => write!(f, "IL"), + } + } +} + +impl LookaheadDutyKind { + /// The `DUTIES_SERVICE_TIMES` label for this duty type's fetch phase. + fn fetch_metric(&self) -> &'static str { + match self { + LookaheadDutyKind::Ptc => validator_metrics::UPDATE_PTC_FETCH, + LookaheadDutyKind::InclusionList => validator_metrics::UPDATE_IL_DUTIES_FETCH, + } + } + + /// The `DUTIES_SERVICE_TIMES` label for this duty type's store phase. + fn store_metric(&self) -> &'static str { + match self { + LookaheadDutyKind::Ptc => validator_metrics::UPDATE_PTC_STORE, + LookaheadDutyKind::InclusionList => validator_metrics::UPDATE_IL_DUTIES_STORE, + } + } +} + +trait LookaheadDuty { + fn pubkey(&self) -> PublicKeyBytes; + /// This duty type's identity, used for log messages and metric labels. + /// + /// An associated fn (no `self`) so the generic polling code can use it without + /// holding a duty instance — same pattern as `StoreItem::db_column`. + fn kind() -> LookaheadDutyKind; +} + +impl LookaheadDuty for PtcDuty { + fn pubkey(&self) -> PublicKeyBytes { + self.pubkey + } + + fn kind() -> LookaheadDutyKind { + LookaheadDutyKind::Ptc + } +} + +impl LookaheadDuty for InclusionListDuty { + fn pubkey(&self) -> PublicKeyBytes { + self.pubkey + } + + fn kind() -> LookaheadDutyKind { + LookaheadDutyKind::InclusionList + } +} + /// To assist with readability, the dependent root for attester/proposer duties. type DependentRoot = Hash256; @@ -1882,6 +1943,109 @@ async fn fetch_and_store_proposer_duties( + epoch: Epoch, + local_indices: &[u64], + local_pubkeys: &HashSet, + duties_map: &RwLock)>>, + fetch_duties: F, +) -> Result<(), E> +where + D: LookaheadDuty + Serialize + DeserializeOwned, + F: Fn(Epoch, Vec) -> Fut, + Fut: Future>, E>>, +{ + if local_indices.is_empty() { + debug!( + %epoch, + duty_kind = %D::kind(), + "No validators, not downloading duties" + ); + return Ok(()); + } + + let fetch_timer = validator_metrics::start_timer_vec( + &validator_metrics::DUTIES_SERVICE_TIMES, + &[D::kind().fetch_metric()], + ); + + // TODO(gloas) limitation: only `dependent_root` changes are detected here, so validators + // added mid-epoch won't get duties until the next epoch boundary. + let initial_indices_to_request = + &local_indices[0..min(INITIAL_DUTIES_QUERY_SIZE, local_indices.len())]; + + let response = fetch_duties(epoch, initial_indices_to_request.to_vec()).await?; + let dependent_root = response.dependent_root; + + // Check if we need to update duties for this epoch. + // We update if we have no epoch data OR if the dependent_root changed. + let needs_update = duties_map.read().get(&epoch).is_none_or(|(prior_root, _)| { + // Update if dependent_root changed + *prior_root != dependent_root + }); + + if !needs_update { + // No validators have conflicting (epoch, dependent_root) values for this epoch. + return Ok(()); + } + // Make a request for all indices that require updating which we have not already made a request for. + let indices_to_request = local_indices + .iter() + .filter(|validator_index| !initial_indices_to_request.contains(validator_index)) + .copied() + .collect::>(); + + // Filter the initial duties by their relevance so that we don't hit warnings about + // overwriting duties. + let new_initial_duties = response + .data + .into_iter() + .filter(|duty| local_pubkeys.contains(&duty.pubkey())); + + let mut new_duties = if !indices_to_request.is_empty() { + fetch_duties(epoch, indices_to_request.to_vec()).await?.data + } else { + vec![] + }; + new_duties.extend(new_initial_duties); + + drop(fetch_timer); + + let _store_timer = validator_metrics::start_timer_vec( + &validator_metrics::DUTIES_SERVICE_TIMES, + &[D::kind().store_metric()], + ); + + debug!( + %dependent_root, + duty_kind = %D::kind(), + num_new_duties = new_duties.len(), + "Downloaded duties" + ); + + // Update duties - we only reach here if dependent_root changed or epoch is missing + match duties_map.write().entry(epoch) { + hash_map::Entry::Occupied(mut entry) => { + // Dependent root must have changed, so we do complete replacement. + let (existing_root, _existing_duties) = entry.get(); + debug!( + old_root = %existing_root, + new_root = %dependent_root, + duty_kind = %D::kind(), + "Dependent root changed, replacing all duties" + ); + + *entry.get_mut() = (dependent_root, new_duties); + } + hash_map::Entry::Vacant(entry) => { + // No existing duties for this epoch + entry.insert((dependent_root, new_duties)); + } + } + + Ok(()) +} + /// Query the beacon node for ptc duties for any known validators. async fn poll_beacon_ptc_attesters( duties_service: &Arc>, @@ -1974,115 +2138,16 @@ async fn poll_beacon_ptc_attesters_for_epoch< local_indices: &[u64], local_pubkeys: &HashSet, ) -> Result<(), Error> { - // No need to bother the BN if we don't have any validators. - if local_indices.is_empty() { - debug!( - %epoch, - "No validators, not downloading PTC duties" - ); - return Ok(()); - } - - let fetch_timer = validator_metrics::start_timer_vec( - &validator_metrics::DUTIES_SERVICE_TIMES, - &[validator_metrics::UPDATE_PTC_FETCH], - ); - - // TODO(gloas) Unlike attester duties which use `get_uninitialized_validators` to detect - // newly-added validators, PTC duties only check dependent_root changes. Validators added - // mid-epoch won't get PTC duties until the next epoch boundary. We should probably fix this. - let initial_indices_to_request = - &local_indices[0..min(INITIAL_PTC_DUTIES_QUERY_SIZE, local_indices.len())]; - - let response = - post_validator_duties_ptc(duties_service, epoch, initial_indices_to_request).await?; - let dependent_root = response.dependent_root; - - // Check if we need to update duties for this epoch and collect validators to update. - // We update if we have no epoch data OR if the dependent_root changed. - let validators_to_update = { - // Avoid holding the read-lock for any longer than required. - let ptc_duties = duties_service.ptc_duties.read(); - let needs_update = ptc_duties.get(&epoch).is_none_or(|(prior_root, _duties)| { - // Update if dependent_root changed - *prior_root != dependent_root - }); - - if needs_update { - local_pubkeys.iter().collect::>() - } else { - Vec::new() - } - }; - - if validators_to_update.is_empty() { - // No validators have conflicting (epoch, dependent_root) values for this epoch. - return Ok(()); - } - - // Make a request for all indices that require updating which we have not already made a request for. - let indices_to_request = validators_to_update - .iter() - .filter_map(|pubkey| duties_service.validator_store.validator_index(pubkey)) - .filter(|validator_index| !initial_indices_to_request.contains(validator_index)) - .collect::>(); - - // Filter the initial duties by their relevance so that we don't hit warnings about - // overwriting duties. - let new_initial_duties = response - .data - .into_iter() - .filter(|duty| validators_to_update.contains(&&duty.pubkey)); - - let mut new_duties = if !indices_to_request.is_empty() { - post_validator_duties_ptc(duties_service, epoch, indices_to_request.as_slice()) - .await? - .data - } else { - vec![] - }; - new_duties.extend(new_initial_duties); - - drop(fetch_timer); - - let _store_timer = validator_metrics::start_timer_vec( - &validator_metrics::DUTIES_SERVICE_TIMES, - &[validator_metrics::UPDATE_PTC_STORE], - ); - - debug!( - %dependent_root, - num_new_duties = new_duties.len(), - "Downloaded PTC duties" - ); - - // Update duties - we only reach here if dependent_root changed or epoch is missing - let mut ptc_duties = duties_service.ptc_duties.write(); - - match ptc_duties.entry(epoch) { - hash_map::Entry::Occupied(mut entry) => { - // Dependent root must have changed, so we do complete replacement. - // We cannot support partial updates for the same dependent_root. - // The beacon node may return incomplete duty lists and we cannot distinguish between "no duties" and - // "duties not included in this response". We could query all local validators in each - // `post_validator_duties_ptc` call regardless of dependent_root changes, but the bandwidth - // cost is likely not justified since PTC assignments are sparse. - let (existing_root, _existing_duties) = entry.get(); - debug!( - old_root = %existing_root, - new_root = %dependent_root, - "PTC dependent root changed, replacing all duties" - ); - - *entry.get_mut() = (dependent_root, new_duties); - } - hash_map::Entry::Vacant(entry) => { - // No existing duties for this epoch - entry.insert((dependent_root, new_duties)); - } - } - - Ok(()) + poll_lookahead_duties_for_epoch( + epoch, + local_indices, + local_pubkeys, + &duties_service.ptc_duties, + |epoch, indices| async move { + post_validator_duties_ptc(duties_service, epoch, &indices).await + }, + ) + .await } async fn poll_beacon_il_committee_duties( @@ -2163,106 +2228,18 @@ async fn poll_beacon_il_duties_for_epoch, ) -> Result<(), Error> { - if local_indices.is_empty() { - debug!( - %epoch, - "No validators, not downloading IL duties" - ); - return Ok(()); - } - - let fetch_timer = validator_metrics::start_timer_vec( - &validator_metrics::DUTIES_SERVICE_TIMES, - &[validator_metrics::UPDATE_IL_DUTIES_FETCH], - ); - - // TODO(heze) Same limitation as PTC duties: - // only `dependent_root` changes are detected, so validators added mid-epoch won't get IL duties - // until the next epoch boundary. - let initial_indices_to_request = - &local_indices[0..min(INITIAL_IL_DUTIES_QUERY_SIZE, local_indices.len())]; - - let response = - post_validator_il_duties(duties_service, epoch, initial_indices_to_request).await?; - let dependent_root = response.dependent_root; - - // Check if we need to update duties for this epoch. - // We update if we have no epoch data OR if the dependent_root changed. - let needs_update = { - // Avoid holding the read-lock for any longer than required. - let il_duties = duties_service.il_duties.read(); - il_duties.get(&epoch).is_none_or(|(prior_root, _)| { - // Update if dependent_root changed - *prior_root != dependent_root - }) - }; - - if !needs_update { - // No validators have conflicting (epoch, dependent_root) values for this epoch. - return Ok(()); - } - - // Make a request for all indices that require updating which we have not already made a request for. - let indices_to_request = local_indices - .iter() - .filter(|validator_index| !initial_indices_to_request.contains(validator_index)) - .copied() - .collect::>(); - - // Filter the initial duties by their relevance so that we don't hit warnings about - // overwriting duties. - let new_initial_duties = response - .data - .into_iter() - .filter(|duty| local_pubkeys.contains(&duty.pubkey)); - - let mut new_duties = if !indices_to_request.is_empty() { - post_validator_il_duties(duties_service, epoch, indices_to_request.as_slice()) - .await? - .data - } else { - vec![] - }; - new_duties.extend(new_initial_duties); - - drop(fetch_timer); - - let _store_timer = validator_metrics::start_timer_vec( - &validator_metrics::DUTIES_SERVICE_TIMES, - &[validator_metrics::UPDATE_IL_DUTIES_STORE], - ); - - debug!( - %dependent_root, - num_new_duties = new_duties.len(), - "Downloaded IL duties" - ); - - // Update duties - we only reach here if dependent_root changed or epoch is missing - let mut il_duties = duties_service.il_duties.write(); - - match il_duties.entry(epoch) { - hash_map::Entry::Occupied(mut entry) => { - // Dependent root must have changed, so we do complete replacement. - let (existing_root, _existing_duties) = entry.get(); - debug!( - old_root = %existing_root, - new_root = %dependent_root, - "IL dependent root changed, replacing all duties" - ); - - *entry.get_mut() = (dependent_root, new_duties); - } - hash_map::Entry::Vacant(entry) => { - // No existing duties for this epoch - entry.insert((dependent_root, new_duties)); - } - } - - Ok(()) + poll_lookahead_duties_for_epoch( + epoch, + local_indices, + local_pubkeys, + &duties_service.il_duties, + |epoch, indices| async move { + post_validator_il_duties(duties_service, epoch, &indices).await + }, + ) + .await } -/// Notify the block service if it should produce a block. async fn notify_block_production_service( current_slot: Slot, block_proposers: &HashSet, From eee62e4f2194c19ba40b9dd93ec9463c5a1673da Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 3 Sep 2026 18:53:04 +0300 Subject: [PATCH 28/44] Remove unnecessary comments --- validator_client/validator_services/src/duties_service.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index de255ef5806..c5ded3d4395 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -297,7 +297,6 @@ impl Display for LookaheadDutyKind { } impl LookaheadDutyKind { - /// The `DUTIES_SERVICE_TIMES` label for this duty type's fetch phase. fn fetch_metric(&self) -> &'static str { match self { LookaheadDutyKind::Ptc => validator_metrics::UPDATE_PTC_FETCH, @@ -305,7 +304,6 @@ impl LookaheadDutyKind { } } - /// The `DUTIES_SERVICE_TIMES` label for this duty type's store phase. fn store_metric(&self) -> &'static str { match self { LookaheadDutyKind::Ptc => validator_metrics::UPDATE_PTC_STORE, From d8fa3ce5d6fccecfe2a6de252d216f642540d003 Mon Sep 17 00:00:00 2001 From: conache Date: Tue, 15 Sep 2026 11:08:25 +0300 Subject: [PATCH 29/44] Cleanup: use correct error name and remove redundant validator indices clone --- validator_client/validator_services/src/duties_service.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index c5ded3d4395..2a4c05b3667 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -86,7 +86,7 @@ pub enum Error { UnableToReadSlotClock, FailedToDownloadAttesters(#[allow(dead_code)] String), FailedToDownloadPtc(#[allow(dead_code)] String), - FailedToDownloadILs(#[allow(dead_code)] String), + FailedToDownloadInclusionListDuties(#[allow(dead_code)] String), FailedToProduceSelectionProof(#[allow(dead_code)] ValidatorStoreError), InvalidModulo(#[allow(dead_code)] ArithError), Arith(#[allow(dead_code)] ArithError), @@ -889,7 +889,7 @@ pub fn start_update_service if let Err(e) = poll_beacon_il_committee_duties(&duties_service).await { error!( error = ?e, - "Failed to poll il committee duties" + "Failed to poll IL committee duties" ) } @@ -1561,7 +1561,7 @@ async fn post_validator_il_duties( .await }) .await - .map_err(|e| Error::FailedToDownloadILs(e.to_string())) + .map_err(|e| Error::FailedToDownloadInclusionListDuties(e.to_string())) } /// Compute the attestation selection proofs for the `duties` and add them to the `attesters` map. @@ -2001,7 +2001,7 @@ where .filter(|duty| local_pubkeys.contains(&duty.pubkey())); let mut new_duties = if !indices_to_request.is_empty() { - fetch_duties(epoch, indices_to_request.to_vec()).await?.data + fetch_duties(epoch, indices_to_request).await?.data } else { vec![] }; From 2afb95f7156b97e17ed1fb9c8f3e1571c471cc9a Mon Sep 17 00:00:00 2001 From: conache Date: Tue, 15 Sep 2026 13:33:47 +0300 Subject: [PATCH 30/44] Consolidate the PTC and IL duties polling into a single generic function --- .../validator_services/src/duties_service.rs | 268 +++++------------- 1 file changed, 77 insertions(+), 191 deletions(-) diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index 2a4c05b3667..76d29faddfa 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -25,7 +25,6 @@ use serde::{Serialize, de::DeserializeOwned}; use slot_clock::SlotClock; use std::cmp::min; use std::collections::{BTreeMap, HashMap, HashSet, hash_map}; -use std::fmt::{Display, Formatter}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; @@ -282,63 +281,42 @@ impl SubscriptionSlots { } } -enum LookaheadDutyKind { - Ptc, - InclusionList, -} - -impl Display for LookaheadDutyKind { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - LookaheadDutyKind::Ptc => write!(f, "PTC"), - LookaheadDutyKind::InclusionList => write!(f, "IL"), - } - } -} +/// A duty that is known one epoch in advance and keyed by dependent root, so it can be polled +/// for the current and next epoch with the same logic. +trait LookaheadDuty: Serialize + DeserializeOwned { + /// Used in log messages. + const NAME: &'static str; + /// Task labels for the duties service timing metrics. + const FETCH_METRIC: &'static str; + const STORE_METRIC: &'static str; + const CURRENT_EPOCH_METRIC: &'static str; + const NEXT_EPOCH_METRIC: &'static str; -impl LookaheadDutyKind { - fn fetch_metric(&self) -> &'static str { - match self { - LookaheadDutyKind::Ptc => validator_metrics::UPDATE_PTC_FETCH, - LookaheadDutyKind::InclusionList => validator_metrics::UPDATE_IL_DUTIES_FETCH, - } - } - - fn store_metric(&self) -> &'static str { - match self { - LookaheadDutyKind::Ptc => validator_metrics::UPDATE_PTC_STORE, - LookaheadDutyKind::InclusionList => validator_metrics::UPDATE_IL_DUTIES_STORE, - } - } -} - -trait LookaheadDuty { fn pubkey(&self) -> PublicKeyBytes; - /// This duty type's identity, used for log messages and metric labels. - /// - /// An associated fn (no `self`) so the generic polling code can use it without - /// holding a duty instance — same pattern as `StoreItem::db_column`. - fn kind() -> LookaheadDutyKind; } impl LookaheadDuty for PtcDuty { + const NAME: &'static str = "PTC"; + const FETCH_METRIC: &'static str = validator_metrics::UPDATE_PTC_FETCH; + const STORE_METRIC: &'static str = validator_metrics::UPDATE_PTC_STORE; + const CURRENT_EPOCH_METRIC: &'static str = validator_metrics::UPDATE_PTC_CURRENT_EPOCH; + const NEXT_EPOCH_METRIC: &'static str = validator_metrics::UPDATE_PTC_NEXT_EPOCH; + fn pubkey(&self) -> PublicKeyBytes { self.pubkey } - - fn kind() -> LookaheadDutyKind { - LookaheadDutyKind::Ptc - } } impl LookaheadDuty for InclusionListDuty { + const NAME: &'static str = "Inclusion List"; + const FETCH_METRIC: &'static str = validator_metrics::UPDATE_IL_DUTIES_FETCH; + const STORE_METRIC: &'static str = validator_metrics::UPDATE_IL_DUTIES_STORE; + const CURRENT_EPOCH_METRIC: &'static str = validator_metrics::UPDATE_IL_DUTIES_CURRENT_EPOCH; + const NEXT_EPOCH_METRIC: &'static str = validator_metrics::UPDATE_IL_DUTIES_NEXT_EPOCH; + fn pubkey(&self) -> PublicKeyBytes { self.pubkey } - - fn kind() -> LookaheadDutyKind { - LookaheadDutyKind::InclusionList - } } /// To assist with readability, the dependent root for attester/proposer duties. @@ -836,10 +814,17 @@ pub fn start_update_service continue; } - if let Err(e) = poll_beacon_ptc_attesters(&duties_service).await { + let service = &duties_service; + let fetch_ptc_duties = |epoch: Epoch, indices: Vec| async move { + post_validator_duties_ptc(service, epoch, &indices).await + }; + if let Err(e) = + poll_beacon_lookahead_duties(service, &service.ptc_duties, fetch_ptc_duties) + .await + { error!( error = ?e, - "Failed to poll PTC duties" + "Failed to poll PTC duties" ); } @@ -886,7 +871,14 @@ pub fn start_update_service continue; } - if let Err(e) = poll_beacon_il_committee_duties(&duties_service).await { + let service = &duties_service; + let fetch_il_duties = |epoch: Epoch, indices: Vec| async move { + post_validator_il_duties(service, epoch, &indices).await + }; + if let Err(e) = + poll_beacon_lookahead_duties(service, &service.il_duties, fetch_il_duties) + .await + { error!( error = ?e, "Failed to poll IL committee duties" @@ -1949,14 +1941,14 @@ async fn poll_lookahead_duties_for_epoch( fetch_duties: F, ) -> Result<(), E> where - D: LookaheadDuty + Serialize + DeserializeOwned, + D: LookaheadDuty, F: Fn(Epoch, Vec) -> Fut, Fut: Future>, E>>, { if local_indices.is_empty() { debug!( %epoch, - duty_kind = %D::kind(), + duty = D::NAME, "No validators, not downloading duties" ); return Ok(()); @@ -1964,7 +1956,7 @@ where let fetch_timer = validator_metrics::start_timer_vec( &validator_metrics::DUTIES_SERVICE_TIMES, - &[D::kind().fetch_metric()], + &[D::FETCH_METRIC], ); // TODO(gloas) limitation: only `dependent_root` changes are detected here, so validators @@ -2011,12 +2003,12 @@ where let _store_timer = validator_metrics::start_timer_vec( &validator_metrics::DUTIES_SERVICE_TIMES, - &[D::kind().store_metric()], + &[D::STORE_METRIC], ); debug!( %dependent_root, - duty_kind = %D::kind(), + duty = D::NAME, num_new_duties = new_duties.len(), "Downloaded duties" ); @@ -2029,7 +2021,7 @@ where debug!( old_root = %existing_root, new_root = %dependent_root, - duty_kind = %D::kind(), + duty = D::NAME, "Dependent root changed, replacing all duties" ); @@ -2044,116 +2036,23 @@ where Ok(()) } -/// Query the beacon node for ptc duties for any known validators. -async fn poll_beacon_ptc_attesters( +/// Download the lookahead duties for the current and next epoch, store them in `duties_map`, +/// then prune duties older than `HISTORICAL_DUTIES_EPOCHS`. +async fn poll_beacon_lookahead_duties( duties_service: &Arc>, -) -> Result<(), Error> { - let current_epoch_timer = validator_metrics::start_timer_vec( - &validator_metrics::DUTIES_SERVICE_TIMES, - &[validator_metrics::UPDATE_PTC_CURRENT_EPOCH], - ); - - let current_slot = duties_service - .slot_clock - .now() - .ok_or(Error::UnableToReadSlotClock)?; - let current_epoch = current_slot.epoch(S::E::slots_per_epoch()); - - // Collect *all* pubkeys, even those undergoing doppelganger protection. - let local_pubkeys: HashSet<_> = duties_service - .validator_store - .voting_pubkeys(DoppelgangerStatus::ignored); - - let local_indices = { - let mut local_indices = Vec::with_capacity(local_pubkeys.len()); - - for &pubkey in &local_pubkeys { - if let Some(validator_index) = duties_service.validator_store.validator_index(&pubkey) { - local_indices.push(validator_index) - } - } - local_indices - }; - - // Poll for current epoch - if let Err(e) = poll_beacon_ptc_attesters_for_epoch( - duties_service, - current_epoch, - &local_indices, - &local_pubkeys, - ) - .await - { - error!( - %current_epoch, - request_epoch = %current_epoch, - err = ?e, - "Failed to download PTC duties" - ); - } - drop(current_epoch_timer); - let next_epoch_timer = validator_metrics::start_timer_vec( - &validator_metrics::DUTIES_SERVICE_TIMES, - &[validator_metrics::UPDATE_PTC_NEXT_EPOCH], - ); - - // Poll for next epoch - let next_epoch = current_epoch + 1; - if let Err(e) = poll_beacon_ptc_attesters_for_epoch( - duties_service, - next_epoch, - &local_indices, - &local_pubkeys, - ) - .await - { - error!( - %current_epoch, - request_epoch = %next_epoch, - err = ?e, - "Failed to download PTC duties" - ); - } - drop(next_epoch_timer); - - // Prune old duties. - duties_service - .ptc_duties - .write() - .retain(|&epoch, _| epoch + HISTORICAL_DUTIES_EPOCHS >= current_epoch); - - Ok(()) -} - -/// For the given `local_indices` and `local_pubkeys`, download the PTC duties for the given `epoch` and -/// store them in `duties_service.ptc_duties` using bandwidth optimization. -async fn poll_beacon_ptc_attesters_for_epoch< + duties_map: &RwLock)>>, + fetch_duties: F, +) -> Result<(), Error> +where + D: LookaheadDuty, S: ValidatorStore + 'static, T: SlotClock + 'static, ->( - duties_service: &Arc>, - epoch: Epoch, - local_indices: &[u64], - local_pubkeys: &HashSet, -) -> Result<(), Error> { - poll_lookahead_duties_for_epoch( - epoch, - local_indices, - local_pubkeys, - &duties_service.ptc_duties, - |epoch, indices| async move { - post_validator_duties_ptc(duties_service, epoch, &indices).await - }, - ) - .await -} - -async fn poll_beacon_il_committee_duties( - duties_service: &Arc>, -) -> Result<(), Error> { + F: Fn(Epoch, Vec) -> Fut, + Fut: Future>, Error>>, +{ let current_epoch_timer = validator_metrics::start_timer_vec( &validator_metrics::DUTIES_SERVICE_TIMES, - &[validator_metrics::UPDATE_IL_DUTIES_CURRENT_EPOCH], + &[D::CURRENT_EPOCH_METRIC], ); let current_slot = duties_service @@ -2172,72 +2071,59 @@ async fn poll_beacon_il_committee_duties>(); // Poll for current epoch - if let Err(e) = poll_beacon_il_duties_for_epoch( - duties_service, + if let Err(e) = poll_lookahead_duties_for_epoch( current_epoch, &local_indices, &local_pubkeys, + duties_map, + &fetch_duties, ) .await { error!( %current_epoch, request_epoch = %current_epoch, - err = ?e, - "Failed to download IL committee duties" + duty = D::NAME, + error = ?e, + "Failed to download duties" ); } drop(current_epoch_timer); let next_epoch_timer = validator_metrics::start_timer_vec( &validator_metrics::DUTIES_SERVICE_TIMES, - &[validator_metrics::UPDATE_IL_DUTIES_NEXT_EPOCH], + &[D::NEXT_EPOCH_METRIC], ); // Poll for next epoch let next_epoch = current_epoch + 1; - if let Err(e) = - poll_beacon_il_duties_for_epoch(duties_service, next_epoch, &local_indices, &local_pubkeys) - .await + if let Err(e) = poll_lookahead_duties_for_epoch( + next_epoch, + &local_indices, + &local_pubkeys, + duties_map, + &fetch_duties, + ) + .await { error!( %current_epoch, request_epoch = %next_epoch, - err = ?e, - "Failed to download IL committee duties" + duty = D::NAME, + error = ?e, + "Failed to download duties" ); } drop(next_epoch_timer); - // Prune old IL committee duties - duties_service - .il_duties + // Prune old duties + duties_map .write() .retain(|&epoch, _| epoch + HISTORICAL_DUTIES_EPOCHS >= current_epoch); Ok(()) } -/// For the given `local_indices` and `local_pubkeys`, download the IL committee duties -/// for the given `epoch` and store them in `duties_service.il_duties` using bandwidth optimization. -async fn poll_beacon_il_duties_for_epoch( - duties_service: &Arc>, - epoch: Epoch, - local_indices: &[u64], - local_pubkeys: &HashSet, -) -> Result<(), Error> { - poll_lookahead_duties_for_epoch( - epoch, - local_indices, - local_pubkeys, - &duties_service.il_duties, - |epoch, indices| async move { - post_validator_il_duties(duties_service, epoch, &indices).await - }, - ) - .await -} - async fn notify_block_production_service( current_slot: Slot, block_proposers: &HashSet, From 70922a3edf3a5a33e060dd28098df83fc0073536 Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 16 Sep 2026 16:27:16 +0300 Subject: [PATCH 31/44] Fix forks scheduled checks --- .../validator_services/src/duties_service.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index 76d29faddfa..99e9956c818 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -790,7 +790,7 @@ pub fn start_update_service // Spawn the task which keeps track of local PTC duties. // Only start PTC duties service if Gloas fork is scheduled. - if let Some(gloas_fork_epoch) = core_duties_service.spec.gloas_fork_epoch { + if core_duties_service.spec.is_gloas_scheduled() { let duties_service = core_duties_service.clone(); core_duties_service.executor.spawn( async move { @@ -803,6 +803,10 @@ pub fn start_update_service }; let current_epoch = current_slot.epoch(S::E::slots_per_epoch()); + let Some(gloas_fork_epoch) = duties_service.spec.gloas_fork_epoch else { + // Gloas fork epoch not configured, should not reach here + break; + }; if current_epoch + 1 < gloas_fork_epoch { // Wait until the next slot and check again @@ -848,7 +852,7 @@ pub fn start_update_service // Spawn the task which keeps track of the inclusion list committee duties. // Only track IL committee duties if the heze fork is scheduled - if let Some(heze_fork_epoch) = core_duties_service.spec.heze_fork_epoch { + if core_duties_service.spec.is_heze_scheduled() { let duties_service = core_duties_service.clone(); core_duties_service.executor.spawn( async move { @@ -860,6 +864,10 @@ pub fn start_update_service }; let current_epoch = current_slot.epoch(S::E::slots_per_epoch()); + let Some(heze_fork_epoch) = duties_service.spec.heze_fork_epoch else { + // Heze fork epoch not configured, should not reach here + break; + }; if current_epoch + 1 < heze_fork_epoch { // Wait until the next slot and check again if the Heze fork epoch is close From 92f05908eb6ee640e613d7ecff103fc113b922d5 Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 17 Sep 2026 20:04:57 +0300 Subject: [PATCH 32/44] Add unit tests for poll_lookahead_duties_for_epoch --- .../validator_services/src/duties_service.rs | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index 99e9956c818..fd15e0c7241 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -2163,6 +2163,11 @@ async fn notify_block_production_service( #[cfg(test)] mod test { use super::*; + use std::future::{Ready, ready}; + use types::test_utils::generate_deterministic_keypair; + + type InclusionListDutiesMap = RwLock)>>; + type FetchDutiesResult = Result>, ()>; #[test] fn subscription_slots_exact() { @@ -2243,4 +2248,259 @@ mod test { assert_eq!(subscription_slots.slots.len(), 1); assert!(subscription_slots.should_send_subscription_at(current_slot + 1),); } + + /// Generate `n` local validators' details, containing their indices and their pubkeys. + fn local_validators(n: usize) -> (Vec, Vec) { + let indices = (0..n as u64).collect(); + let pubkeys = (0..n) + .map(|i| generate_deterministic_keypair(i).pk.compress()) + .collect(); + (indices, pubkeys) + } + + /// One inclusion list duty per pubkey, with `validator_index` equal to its position. + fn inclusion_list_duties(pubkeys: &[PublicKeyBytes]) -> Vec { + pubkeys + .iter() + .enumerate() + .map(|(i, pubkey)| InclusionListDuty { + pubkey: *pubkey, + validator_index: i as u64, + slot: Slot::new(0), + }) + .collect() + } + + /// Returns a `fetch_duties` function that answers like the endpoint does: + /// retrieve only the duties of the requested validator indices, under `dependent_root`. + fn mock_fetch_duties( + dependent_root: DependentRoot, + duties: Vec, + ) -> impl Fn(Epoch, Vec) -> Ready { + move |_epoch, validator_indices| { + let data = duties + .iter() + .filter(|duty| validator_indices.contains(&duty.validator_index)) + .cloned() + .collect(); + ready(Ok(DutiesResponse { + dependent_root, + execution_optimistic: Some(false), + data, + })) + } + } + + fn sorted(duties: &[InclusionListDuty]) -> Vec { + let mut duties = duties.to_vec(); + duties.sort_by_key(|duty| duty.validator_index); + duties + } + + #[tokio::test] + async fn poll_duties_for_epoch_with_no_indices_is_noop() { + let epoch = Epoch::new(1); + let (_, pubkeys) = local_validators(10); + let local_pubkeys: HashSet = pubkeys.iter().copied().collect(); + let duties_map = InclusionListDutiesMap::default(); + + let dependent_root = Hash256::repeat_byte(1); + let duties = inclusion_list_duties(&pubkeys); + let fetch_duties = async |_epoch: Epoch, _validator_indices: Vec| { + // the function should not be executed if local_indices is an empty array + Ok::<_, ()>(DutiesResponse { + dependent_root, + execution_optimistic: Some(false), + data: duties.clone(), + }) + }; + + // polling with an empty local_indices list should trigger an early return + poll_lookahead_duties_for_epoch(epoch, &[], &local_pubkeys, &duties_map, fetch_duties) + .await + .unwrap(); + + // nothing was stored in the duties map + assert!(duties_map.read().is_empty()); + } + + #[tokio::test] + async fn poll_duties_for_epoch_populates_empty_duties_map() { + let epoch = Epoch::new(1); + let (local_indices, pubkeys) = local_validators(10); + let local_pubkeys: HashSet = pubkeys.iter().copied().collect(); + let duties_map = InclusionListDutiesMap::default(); + + let expected_dependent_root = Hash256::repeat_byte(1); + let expected_duties = inclusion_list_duties(&pubkeys); + let fetch_duties = mock_fetch_duties(expected_dependent_root, expected_duties.clone()); + + poll_lookahead_duties_for_epoch( + epoch, + &local_indices, + &local_pubkeys, + &duties_map, + fetch_duties, + ) + .await + .unwrap(); + + let map = duties_map.read(); + let (dependent_root, duties) = map.get(&epoch).expect("should have duties for epoch"); + assert_eq!(*dependent_root, expected_dependent_root); + // the duties map stores probe + the full-fetch results concatenated, + // so we need to sort the stored duties before comparing + assert_eq!(sorted(duties), expected_duties); + } + + #[tokio::test] + async fn poll_duties_for_epoch_same_root_keeps_stored_duties() { + let epoch = Epoch::new(1); + let validators_count = 10; + let (local_indices, pubkeys) = local_validators(validators_count); + let local_pubkeys: HashSet = pubkeys.iter().copied().collect(); + let dependent_root = Hash256::repeat_byte(1); + + // The map already holds duties for the epoch and `dependent_root` + let stored_duties = inclusion_list_duties(&pubkeys); + let duties_map = InclusionListDutiesMap::default(); + duties_map + .write() + .insert(epoch, (dependent_root, stored_duties.clone())); + + let newer_duties: Vec = stored_duties + .iter() + .map(|duty| InclusionListDuty { + slot: Slot::new(1), + validator_index: duty.validator_index, + pubkey: duty.pubkey, + }) + .collect(); + let fetch_duties = mock_fetch_duties(dependent_root, newer_duties); + + poll_lookahead_duties_for_epoch( + epoch, + &local_indices, + &local_pubkeys, + &duties_map, + fetch_duties, + ) + .await + .unwrap(); + + // The duties corresponding to the epoch and the `dependent_root` should be unchanged + let map = duties_map.read(); + let (root, duties) = map.get(&epoch).expect("should have duties for epoch"); + assert_eq!(*root, dependent_root); + assert_eq!(sorted(duties), stored_duties); + } + + #[tokio::test] + async fn poll_duties_for_epoch_changed_dependent_root_updates_duties() { + let epoch = Epoch::new(1); + let validators_count = 10; + let (local_indices, pubkeys) = local_validators(validators_count); + let local_pubkeys: HashSet = pubkeys.iter().copied().collect(); + let first_dependent_root = Hash256::repeat_byte(1); + let second_dependent_root = Hash256::repeat_byte(2); + + // The map already holds duties for the epoch and `first_dependent_root` + let stored_duties = inclusion_list_duties(&pubkeys); + let duties_map = InclusionListDutiesMap::default(); + duties_map + .write() + .insert(epoch, (first_dependent_root, stored_duties.clone())); + + let new_duties: Vec = stored_duties + .iter() + .map(|duty| InclusionListDuty { + slot: Slot::new(1), + validator_index: duty.validator_index, + pubkey: duty.pubkey, + }) + .collect(); + + let fetch_duties = mock_fetch_duties(second_dependent_root, new_duties.clone()); + + poll_lookahead_duties_for_epoch( + epoch, + &local_indices, + &local_pubkeys, + &duties_map, + fetch_duties, + ) + .await + .unwrap(); + + // The duties corresponding to the epoch should now be changed to the ones corresponding + // to the `second_dependent_root` + let map = duties_map.read(); + let (root, duties) = map.get(&epoch).expect("should have duties for epoch"); + assert_eq!(*root, second_dependent_root); + assert_eq!(sorted(duties), new_duties); + } + + #[tokio::test] + async fn poll_duties_for_epoch_only_stores_local_validators_duties() { + let epoch = Epoch::new(1); + let (local_indices, pubkeys) = local_validators(10); + let local_pubkeys: HashSet = pubkeys.iter().copied().collect(); + let external_pubkeys: HashSet = (10..15) + .map(|i| generate_deterministic_keypair(i).pk.compress()) + .collect(); + let all_pubkeys: HashSet = + local_pubkeys.union(&external_pubkeys).copied().collect(); + let duties_map = InclusionListDutiesMap::default(); + + let expected_dependent_root = Hash256::repeat_byte(1); + let expected_duties = inclusion_list_duties(&pubkeys); + let fetch_duties = mock_fetch_duties(expected_dependent_root, expected_duties.clone()); + + poll_lookahead_duties_for_epoch( + epoch, + &local_indices, + &all_pubkeys, + &duties_map, + fetch_duties, + ) + .await + .unwrap(); + + let map = duties_map.read(); + let (dependent_root, duties) = map.get(&epoch).expect("should have duties for epoch"); + assert_eq!(*dependent_root, expected_dependent_root); + assert_eq!(sorted(duties), expected_duties); + } + #[tokio::test] + async fn poll_duties_for_epoch_fetch_error_leaves_map_unchanged() { + let epoch = Epoch::new(1); + let (local_indices, pubkeys) = local_validators(10); + let local_pubkeys: HashSet = pubkeys.iter().copied().collect(); + let dependent_root = Hash256::repeat_byte(1); + + let stored_duties = inclusion_list_duties(&pubkeys); + let duties_map = InclusionListDutiesMap::default(); + duties_map + .write() + .insert(epoch, (dependent_root, stored_duties.clone())); + + let fetch_duties = async |_epoch: Epoch, _validator_indices: Vec| { + Err::>, ()>(()) + }; + + let result = poll_lookahead_duties_for_epoch( + epoch, + &local_indices, + &local_pubkeys, + &duties_map, + fetch_duties, + ) + .await; + assert!(result.is_err()); + + let map = duties_map.read(); + let (root, duties) = map.get(&epoch).expect("should have duties for epoch"); + assert_eq!(*root, dependent_root); + assert_eq!(sorted(duties), stored_duties); + } } From 87536d117d69eb9c1552a7a872f9c0e9f48ba28b Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 30 Sep 2026 12:23:51 +0300 Subject: [PATCH 33/44] Add optimization to produce inclusion lists shortly before the inclusion list deadline --- .../src/inclusion_list_service.rs | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/validator_client/validator_services/src/inclusion_list_service.rs b/validator_client/validator_services/src/inclusion_list_service.rs index d7cc55ff4c9..ba515615015 100644 --- a/validator_client/validator_services/src/inclusion_list_service.rs +++ b/validator_client/validator_services/src/inclusion_list_service.rs @@ -5,6 +5,7 @@ use logging::crit; use slot_clock::SlotClock; use std::ops::Deref; use std::sync::Arc; +use std::time::Duration; use task_executor::TaskExecutor; use tokio::time::sleep; use tracing::{debug, error, info}; @@ -13,6 +14,11 @@ use validator_store::ValidatorStore; type DependentRoot = Hash256; +/// How long before the inclusion list deadline inclusion lists are produced. By then the execution +/// layer has usually received the `forkchoiceUpdated` for the slot's payload, which drops the +/// payload's transactions from its mempool. +const INCLUSION_LIST_PRODUCTION_MARGIN: Duration = Duration::from_secs(1); + struct InclusionListData { dependent_root: DependentRoot, transactions: InclusionListTransactions, @@ -75,6 +81,7 @@ where pub fn start_update_service(self) -> Result<(), String> { info!( inclusion_list_due_ms = self.chain_spec.get_inclusion_list_due().as_millis(), + inclusion_list_production_due_ms = self.inclusion_list_production_due().as_millis(), "Inclusion list service started" ); let executor = self.executor.clone(); @@ -93,13 +100,7 @@ where } async fn spawn_inclusion_list_tasks(&self) -> Result<(), String> { - // TODO(heze): consider producing the inclusion list after the slot's envelope is - // revealed instead of right at the start of the slot, keeping the current approach - // as a fallback. Producing at slot start means the list can include transactions - // that the current slot's payload already includes. These would mean redundant constraints - // that put no pressure on the next builder. Building after the envelopes reveal would - // keep only still-pending transactions - let Some(slot) = self.wait_to_next_slot().await else { + let Some(slot) = self.wait_for_inclusion_list_production_due().await else { return Ok(()); }; @@ -124,8 +125,16 @@ where Ok(()) } - async fn wait_to_next_slot(&self) -> Option { + /// Duration into the slot at which inclusion lists are produced. + fn inclusion_list_production_due(&self) -> Duration { + self.chain_spec + .get_inclusion_list_due() + .saturating_sub(INCLUSION_LIST_PRODUCTION_MARGIN) + } + + async fn wait_for_inclusion_list_production_due(&self) -> Option { let slot_duration = self.chain_spec.get_slot_duration(); + let inclusion_list_production_due = self.inclusion_list_production_due(); let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() else { error!("Failed to read slot clock"); @@ -154,7 +163,7 @@ where return None; } - sleep(duration_to_next_slot).await; + sleep(duration_to_next_slot + inclusion_list_production_due).await; let Some(current_slot) = self.slot_clock.now() else { error!("Failed to read slot clock after sleep"); @@ -417,7 +426,7 @@ mod tests { let service = &harness.service; // Add duties for a pre-Heze slot - // If the il task execution leaks past the wait_for_next_slot check, + // If the il task execution leaks past the wait_for_inclusion_list_production_due check, // it would fetch the transactions from the mock BNs and fail the final assertion // in the test harness.insert_il_duties(Slot::new(1), Hash256::repeat_byte(0xab)); @@ -429,7 +438,7 @@ mod tests { assert!(service_wait.as_mut().now_or_never().is_none()); // Advance both slot_clock and tokio::time slot by slot up to 384s (the sleep deadline) - // This verifies that wait_to_next_slot waits a whole epoch (not just a slot) before completing + // This verifies that wait_for_inclusion_list_production_due waits a whole epoch (not just a slot) before completing for _ in 0..E::slots_per_epoch() { let duration_to_next_slot = harness.service.slot_clock.duration_to_next_slot().unwrap(); advance_time(&harness.service.slot_clock, duration_to_next_slot).await; @@ -447,19 +456,22 @@ mod tests { } #[tokio::test] - async fn waits_until_next_slot() { + async fn waits_until_production_due_in_next_slot() { tokio::time::pause(); let harness = TestHarness::new_with_validators(1).await; let service = &harness.service; - let service_wait = service.wait_to_next_slot(); + let service_wait = service.wait_for_inclusion_list_production_due(); tokio::pin!(service_wait); // Start the timer and registers the sleep timer with tokio assert!(service_wait.as_mut().now_or_never().is_none()); - let duration_to_wait = harness.service.slot_clock.duration_to_next_slot().unwrap(); - // Advance both slot_clock and tokio::time to 12s + // 7s into the next slot: 1s before the inclusion list deadline + let production_due = service.inclusion_list_production_due(); + assert_eq!(production_due, Duration::from_secs(7)); + let duration_to_wait = service.slot_clock.duration_to_next_slot().unwrap() + production_due; + // Advance both slot_clock and tokio::time to 19s advance_time(&harness.service.slot_clock, duration_to_wait).await; assert!( service_wait.as_mut().now_or_never().is_none(), From a7c38f7310309360c339489ca1a669e15192bee5 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 7 Oct 2026 03:50:37 +0800 Subject: [PATCH 34/44] Revert the execution proof availability gate (#10233) ## Issue Addressed - Current approach for optional proofs is #10198 ## Proposed Changes - Revert #9808 --- beacon_node/beacon_chain/src/beacon_chain.rs | 19 +-- beacon_node/beacon_chain/src/builder.rs | 10 +- .../beacon_chain/src/fetch_blobs/tests.rs | 2 +- .../src/pending_payload_cache/mod.rs | 135 +----------------- .../pending_components.rs | 54 ++----- .../tests/envelope_verification.rs | 26 ---- .../gossip_methods.rs | 28 ---- 7 files changed, 17 insertions(+), 257 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index bd183cca589..d9ae09db588 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -35,7 +35,7 @@ use crate::envelope_times_cache::EnvelopeTimesCache; use crate::errors::{BeaconChainError as Error, BlockProductionError}; use crate::events::ServerSentEventHandler; use crate::execution_payload::{NotifyExecutionLayer, PreparePayloadHandle, get_execution_payload}; -use crate::execution_proof_verification::{GossipVerifiedExecutionProof, ObservedExecutionProofs}; +use crate::execution_proof_verification::ObservedExecutionProofs; use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx}; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiSettings}; use crate::inclusion_list_store::{DependentRoot, InclusionListStore}; @@ -4230,23 +4230,6 @@ impl BeaconChain { } } - /// Caches an execution proof, importing the payload envelope if that was the last piece. - pub async fn check_execution_proof_availability_and_import( - self: &Arc, - verified_proof: GossipVerifiedExecutionProof, - ) -> Result { - let GossipVerifiedExecutionProof { proof, block_slot } = verified_proof; - let bid = self - .get_or_load_gloas_payload_bid(proof.beacon_block_root()) - .await?; - let availability = self - .pending_payload_cache - .put_execution_proof(proof, &bid) - .map_err(BlockError::from)?; - self.process_payload_envelope_availability(block_slot, availability, || Ok(())) - .await - } - /// Load a persisted Gloas bid without blocking the async runtime. pub(crate) async fn get_or_load_gloas_payload_bid( self: &Arc, diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index a6b7d9e6f79..3af923a33e7 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -13,7 +13,7 @@ use crate::kzg_utils::{build_data_column_sidecars_fulu, build_data_column_sideca use crate::light_client_server_cache::LightClientServerCache; use crate::migrate::{BackgroundMigrator, MigratorConfig}; use crate::observed_data_sidecars::ObservedDataSidecars; -use crate::pending_payload_cache::{PendingPayloadCache, REQUIRED_EXECUTION_PROOFS}; +use crate::pending_payload_cache::PendingPayloadCache; use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::persisted_custody::load_custody_context; use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; @@ -1006,13 +1006,6 @@ where debug!(?custody_context, "Loaded persisted custody context"); let custody_context = Arc::new(custody_context); - // Without a proof engine we can't verify proofs, so we don't require them. - let required_execution_proofs = if self.proof_engine.is_some() { - REQUIRED_EXECUTION_PROOFS - } else { - 0 - }; - let beacon_chain = BeaconChain { spec: self.spec.clone(), config: self.chain_config, @@ -1104,7 +1097,6 @@ where self.kzg.clone(), custody_context, disable_get_blobs, - required_execution_proofs, self.spec.clone(), ) .map_err(|e| format!("Error initializing PendingPayloadCache: {:?}", e))?, diff --git a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs index 10795562159..d8dcdf86931 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs @@ -543,7 +543,7 @@ mod get_blobs_v4 { spec.clone(), )); let cache = Arc::new( - PendingPayloadCache::::new(get_kzg(&spec), custody_context, false, 0, spec.clone()) + PendingPayloadCache::::new(get_kzg(&spec), custody_context, false, spec.clone()) .expect("create pending payload cache"), ); mock_adapter diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index 8846d570ae4..c2509eb1d79 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -41,7 +41,6 @@ use crate::metrics::{ use crate::observed_data_sidecars::ObservationStrategy; use crate::partial_data_column_assembler::PartialMergeResult; use pending_components::{PendingComponents, ReconstructColumnsDecision}; -use types::execution::SignedExecutionProof; use types::{SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope}; /// The LRU Cache stores `PendingComponents`, which store the block root, the execution payload bid, and its associated column data. @@ -52,15 +51,6 @@ use types::{SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope}; /// eviction to prevent race conditions (#7961), so we expect this cache to be full all the time. const AVAILABILITY_CACHE_CAPACITY: usize = 32; -/// Distinct proof systems that must prove a payload before import. More than one means a -/// soundness bug in a single prover isn't enough to fool us. -/// -/// Which systems count is the engine's call, we only see its `VALID`. The count is ours because -/// we're the only ones who see the whole set. -/// -/// TODO(9658): make configurable. https://github.com/sigp/lighthouse/issues/9658 -pub const REQUIRED_EXECUTION_PROOFS: usize = 2; - /// What `update_pending_components` returns: the value that the update closure computed, next to /// a read guard on the entry it updated. type UpdatedComponents<'a, E, R> = (R, MappedRwLockReadGuard<'a, PendingComponents>); @@ -111,8 +101,6 @@ pub struct PendingPayloadCache { kzg: Arc, custody_context: Arc>, disable_get_blobs: bool, - /// Distinct proof systems required before a payload is available. Zero disables the gate. - required_execution_proofs: usize, spec: Arc, } @@ -121,7 +109,6 @@ impl PendingPayloadCache { kzg: Arc, custody_context: Arc>, disable_get_blobs: bool, - required_execution_proofs: usize, spec: Arc, ) -> Result { Ok(Self { @@ -129,7 +116,6 @@ impl PendingPayloadCache { kzg, custody_context, disable_get_blobs, - required_execution_proofs, spec, }) } @@ -268,8 +254,7 @@ impl PendingPayloadCache { pending_components.span.in_scope(|| { debug!( component = "executed envelope", - status = pending_components - .status_str(&self.custody_context, self.required_execution_proofs), + status = pending_components.status_str(&self.custody_context), "Component added to data availability checker" ); }); @@ -286,39 +271,6 @@ impl PendingPayloadCache { .or_insert_with(|| PendingComponents::new(block_root, bid)); } - /// Cache an execution proof and check availability. - /// - /// Only the proof that reaches the requirement can unblock the payload, so repeats and - /// extras skip the check rather than importing twice. - pub(crate) fn put_execution_proof( - &self, - proof: Arc, - bid: &Arc>, - ) -> Result, AvailabilityCheckError> { - let block_root = proof.beacon_block_root(); - - let (inserted, pending_components) = - self.update_pending_components(block_root, bid, |pending_components| { - pending_components.insert_execution_proof(proof) - })?; - - pending_components.span.in_scope(|| { - debug!( - component = "execution proof", - status = pending_components - .status_str(&self.custody_context, self.required_execution_proofs), - "Component added to data availability checker" - ); - }); - - match inserted { - Some(count) if count <= self.required_execution_proofs => { - self.check_availability(block_root, pending_components) - } - _ => Ok(Availability::MissingComponents(block_root)), - } - } - /// Perform KZG verification on RPC custody columns and insert them into the cache. /// After insertion check if the envelope becomes available. #[instrument(skip_all, level = "trace")] @@ -460,8 +412,7 @@ impl PendingPayloadCache { pending_components.span.in_scope(|| { debug!( component = "data_columns", - status = pending_components - .status_str(&self.custody_context, self.required_execution_proofs), + status = pending_components.status_str(&self.custody_context), "Component added to data availability checker" ); }); @@ -565,8 +516,8 @@ impl PendingPayloadCache { block_root: Hash256, pending_components: MappedRwLockReadGuard<'_, PendingComponents>, ) -> Result, AvailabilityCheckError> { - if let Some(available_envelope) = pending_components - .make_available(&self.custody_context, self.required_execution_proofs)? + if let Some(available_envelope) = + pending_components.make_available(&self.custody_context)? { // Explicitly drop read lock before acquiring write lock drop(pending_components); @@ -696,7 +647,6 @@ mod data_availability_checker_tests { use slot_clock::{SlotClock, TestingSlotClock}; use ssz_types::ProgressiveVariableList; use std::time::Duration; - use types::execution::{ExecutionProof, ProofData, ProofType, PublicInput}; use types::{ Cell, CellBitmap, ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequestsGloas, ForkName, MinimalEthSpec, PartialDataColumnGloas, PartialDataColumnSidecarGloas, @@ -718,24 +668,7 @@ mod data_availability_checker_tests { setup_with(node_custody, NumBlobs::Number(0)) } - /// `setup`, with the proof gate on. - fn setup_gated(node_custody: NodeCustodyType) -> Setup { - setup_full( - node_custody, - NumBlobs::Number(NUM_BLOBS), - REQUIRED_EXECUTION_PROOFS, - ) - } - fn setup_with(node_custody: NodeCustodyType, num_blobs: NumBlobs) -> Setup { - setup_full(node_custody, num_blobs, 0) - } - - fn setup_full( - node_custody: NodeCustodyType, - num_blobs: NumBlobs, - required_execution_proofs: usize, - ) -> Setup { create_test_tracing_subscriber(); let spec = Arc::new(ForkName::Gloas.make_genesis_spec(E::default_spec())); let kzg = get_kzg(&spec); @@ -753,14 +686,8 @@ mod data_availability_checker_tests { spec.clone(), )); let cache = Arc::new( - PendingPayloadCache::::new( - kzg, - custody_context, - false, - required_execution_proofs, - spec.clone(), - ) - .expect("create cache"), + PendingPayloadCache::::new(kzg, custody_context, false, spec.clone()) + .expect("create cache"), ); let mut u = test_unstructured(); @@ -822,29 +749,6 @@ mod data_availability_checker_tests { .cached_data_column_indexes(&self.block_root) .expect("entry") } - - fn put_proof(&self, proof_type: ProofType) -> Availability { - let bid = self.cache.get_bid(&self.block_root).expect("bid"); - self.cache - .put_execution_proof(Arc::new(execution_proof(self.block_root, proof_type)), &bid) - .expect("put execution proof") - } - } - - /// Unverified proof: the cache only reads `beacon_block_root` and `proof_type`. - fn execution_proof(block_root: Hash256, proof_type: ProofType) -> SignedExecutionProof { - SignedExecutionProof { - message: ExecutionProof { - proof_data: ProofData::new(vec![1_u8]).expect("proof data"), - proof_type, - public_input: PublicInput { - new_payload_request_root: Hash256::random(), - }, - beacon_block_root: block_root, - }, - validator_index: 0, - signature: bls::Signature::infinity().expect("infinity sig"), - } } /// Hand-rolled executed envelope with bypassed verification; the cache only inspects @@ -1015,33 +919,6 @@ mod data_availability_checker_tests { assert_lengths_match(&s); } - // ─── Tier 4: EIP-8025 execution proof gating ──────────────────────────── - - /// Envelope and columns alone don't import: the gate wants proofs from - /// `REQUIRED_EXECUTION_PROOFS` distinct provers. Repeats of one type don't count, and a proof - /// beyond the requirement doesn't import the envelope a second time. - #[tokio::test] - async fn execution_proof_gates_availability() { - let s = setup_gated(NodeCustodyType::Fullnode); - s.put_envelope(); - assert_missing(s.put_columns(s.custody.clone())); - - // One prover, however many proofs, is never enough. - for _ in 0..=REQUIRED_EXECUTION_PROOFS { - assert_missing(s.put_proof(0)); - } - - // Distinct provers up to the requirement flip it to available. - let mut availability = None; - for proof_type in 1..REQUIRED_EXECUTION_PROOFS { - availability = Some(s.put_proof(proof_type as ProofType)); - } - let envelope = assert_available(availability.expect("gate needs two provers or more")); - assert_eq!(envelope.block_root, s.block_root); - - assert_missing(s.put_proof(REQUIRED_EXECUTION_PROOFS as ProofType)); - } - // ────────── Gloas partial column merge ───────────────────────────────── // // `merge_partial_data_columns` accumulates cells from partial (incomplete) columns until each diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index ed327a02073..e2808b50c56 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -14,7 +14,6 @@ use std::collections::HashMap; use std::sync::Arc; use tracing::{Span, debug, debug_span}; use types::DataColumnSidecar; -use types::execution::{ProofType, SignedExecutionProof}; use types::{ColumnIndex, EthSpec, Hash256, SignedExecutionPayloadBid}; /// This represents the components of a payload pending data availability. @@ -28,9 +27,6 @@ pub struct PendingComponents { pub envelope: Option>, /// A column entry in this map may only have some cells filled in (i.e. a partial data column) pub verified_data_columns: HashMap>, - /// Execution proofs keyed by proof type, so repeats from one prover count once. Empty - /// without a proof engine. - pub execution_proofs: HashMap>, pub reconstruction_started: bool, /// True once the local `getBlobs` attempt has settled. It is set whether or not the EL /// returned anything. Until then, republished partials request no cells, because the EL @@ -201,19 +197,6 @@ impl PendingComponents { } } - /// Inserts an execution proof, returning the distinct proof type count, or `None` if we - /// already had this type. - pub fn insert_execution_proof(&mut self, proof: Arc) -> Option { - if self - .execution_proofs - .insert(proof.proof_type(), proof) - .is_some() - { - return None; - } - Some(self.execution_proofs.len()) - } - /// Inserts an executed payload envelope into the cache. pub fn insert_executed_payload_envelope( &mut self, @@ -232,11 +215,10 @@ impl PendingComponents { self.num_completed_columns() >= num_expected_columns } - /// Returns `Some` once the envelope, the required columns and enough proofs have arrived. + /// Returns `Some` if the envelope and all required data columns have been received. pub fn make_available( &self, custody_context: &CustodyContext, - required_execution_proofs: usize, ) -> Result>, AvailabilityCheckError> where T: BeaconChainTypes, @@ -246,11 +228,6 @@ impl PendingComponents { return Ok(None); }; - // Proofs are recursive, so we only need them for this payload, not its ancestors. - if self.execution_proofs.len() < required_execution_proofs { - return Ok(None); - } - let AvailabilityPendingExecutedEnvelope { envelope, block_root, @@ -310,38 +287,23 @@ impl PendingComponents { bid, envelope: None, verified_data_columns: HashMap::new(), - execution_proofs: HashMap::new(), reconstruction_started: false, local_fetch_settled: false, span, } } - pub fn status_str( - &self, - custody_context: &CustodyContext, - required_execution_proofs: usize, - ) -> String + pub fn status_str(&self, custody_context: &CustodyContext) -> String where T: BeaconChainTypes, { let num_columns_required = self.num_columns_required(custody_context); - if required_execution_proofs == 0 { - format!( - "envelope {}, data_columns {}/{}", - self.envelope.is_some(), - self.num_completed_columns(), - num_columns_required - ) - } else { - format!( - "envelope {}, data_columns {}/{}, execution_proofs {}", - self.envelope.is_some(), - self.num_completed_columns(), - num_columns_required, - self.execution_proofs.len() - ) - } + format!( + "envelope {}, data_columns {}/{}", + self.envelope.is_some(), + self.num_completed_columns(), + num_columns_required + ) } } diff --git a/beacon_node/beacon_chain/tests/envelope_verification.rs b/beacon_node/beacon_chain/tests/envelope_verification.rs index e85f53430c9..2f5b07f21e8 100644 --- a/beacon_node/beacon_chain/tests/envelope_verification.rs +++ b/beacon_node/beacon_chain/tests/envelope_verification.rs @@ -1,6 +1,5 @@ use beacon_chain::AvailabilityProcessingStatus::{Imported, MissingComponents}; use beacon_chain::NotifyExecutionLayer; -use beacon_chain::execution_proof_verification::GossipVerifiedExecutionProof; use beacon_chain::payload_envelope_verification::{EnvelopeError, EnvelopeSource}; use beacon_chain::test_utils::{ BeaconChainHarness, fork_name_from_env, generate_data_column_sidecars_from_block, test_spec, @@ -9,7 +8,6 @@ use bls::PublicKeyBytes; use eth2::types::EventKind; use proto_array::ExecutionStatus; use std::sync::Arc; -use types::execution::{ExecutionProof, ProofData, PublicInput, SignedExecutionProof}; use types::{ Address, BlockImportSource, Epoch, ExecPayload, ForkName, Hash256, MinimalEthSpec, Slot, WithdrawalRequest, @@ -162,30 +160,6 @@ async fn lookup_imports_gloas_payload_after_restart() { cache.get_bid(&block_root).is_none(), "the pending bid cache should start empty after restart" ); - let proof_status = chain - .check_execution_proof_availability_and_import(GossipVerifiedExecutionProof { - proof: Arc::new(SignedExecutionProof { - message: ExecutionProof { - proof_data: ProofData::new(vec![1]).expect("proof data"), - proof_type: 0, - public_input: PublicInput { - new_payload_request_root: Hash256::random(), - }, - beacon_block_root: block_root, - }, - validator_index: 0, - signature: bls::Signature::infinity().expect("infinity signature"), - }), - block_slot: target_slot, - }) - .await - .expect("execution proof should be accepted after restart"); - assert!(matches!(proof_status, MissingComponents(..))); - assert!(cache.get_bid(&block_root).is_some()); - - // Evict the recovered bid so columns must also handle a cache miss. - cache.do_maintenance(Epoch::new(1)).unwrap(); - assert!(cache.get_bid(&block_root).is_none()); let column_status = chain .process_rpc_custody_columns(custody_columns.clone()) .await 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 be1f2a6c823..b52c9919fa0 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4171,34 +4171,6 @@ impl NetworkBeaconProcessor { "Verified execution proof from gossip" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); - - // This may be the proof the block's envelope was waiting on. - match self - .chain - .check_execution_proof_availability_and_import(verified) - .await - { - Ok(AvailabilityProcessingStatus::Imported(slot, block_root)) => { - info!( - ?block_root, - %slot, - "Execution payload envelope imported after execution proof" - ); - self.chain.recompute_head_at_current_slot().await; - // The payload envelope is imported (`is_payload_received` is now true); - // release any attestations awaiting this block's payload. - self.notify_payload_envelope_imported(block_root, EnvelopeSource::Gossip); - } - Ok(AvailabilityProcessingStatus::MissingComponents(..)) => {} - Err(error) => { - debug!( - %beacon_block_root, - proof_type, - ?error, - "Could not cache execution proof" - ); - } - } } Err(error) => { debug!(%beacon_block_root, proof_type, ?error, "Could not verify execution proof"); From 6360bbbe90c9d2c44ea99a60e125247a967c207e Mon Sep 17 00:00:00 2001 From: Nikhil Sharma Date: Wed, 7 Oct 2026 07:14:08 +0530 Subject: [PATCH 35/44] Fix Web3Signer Gloas request types for remote-signing-api v1.4.0 (#10207) ## Description Web3Signer 26.9.0 follows remote-signing-api v1.4.0 for Gloas signing. Lighthouse still sent the old request shapes (`PAYLOAD_ATTESTATION` / `REQUEST_AUTH`, unwrapped payloads), so Web3Signer would reject them. Rename the message types, wrap Gloas objects as `{version, data}`, and omit `fork_info` for `BUILDER_REQUEST_AUTH`. Add Sepolia Gloas coverage in `web3signer_tests`, pin Web3Signer 26.9.0 from GitHub, and require Java 25 for those tests. Fixes #9252 ## Test plan - [x] `cargo nextest run -p signing_method --release` - [x] `cargo nextest run -p web3signer_tests sepolia_gloas_types --release` --- .github/workflows/test-suite.yml | 9 +- .../warpbuild-ubuntu-latest-snapshot.yml | 4 +- Cargo.lock | 1 + book/src/contributing_setup.md | 2 +- .../web3signer_tests/src/get_web3signer.rs | 39 ++-- testing/web3signer_tests/src/lib.rs | 110 +++++++++++ validator_client/signing_method/Cargo.toml | 3 + validator_client/signing_method/src/lib.rs | 14 +- .../signing_method/src/web3signer.rs | 174 +++++++++++++++++- 9 files changed, 318 insertions(+), 38 deletions(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 80f3041d30a..913d45da774 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -108,13 +108,12 @@ jobs: runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }} steps: - uses: actions/checkout@v5 - # Set Java version to 21. (required since Web3Signer 24.12.0). - # On sigp/lighthouse, Java 21 is baked into the snapshot. - - if: github.repository != 'sigp/lighthouse' - uses: actions/setup-java@v4 + # Set Java version to 25. (required since Web3Signer 26.7.0). + # Always install; do not rely on the WarpBuild snapshot containing Java 25. + - uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '21' + java-version: '25' - if: github.repository != 'sigp/lighthouse' name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 diff --git a/.github/workflows/warpbuild-ubuntu-latest-snapshot.yml b/.github/workflows/warpbuild-ubuntu-latest-snapshot.yml index 3cbd7377480..46cd26750fd 100644 --- a/.github/workflows/warpbuild-ubuntu-latest-snapshot.yml +++ b/.github/workflows/warpbuild-ubuntu-latest-snapshot.yml @@ -52,11 +52,11 @@ jobs: cargo install --locked cargo-sort cargo install --locked cargo-hack - - name: Install Java (Temurin 21) + - name: Install Java (Temurin 25) uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '21' + java-version: '25' - name: Save snapshot uses: WarpBuilds/snapshot-save@v1 diff --git a/Cargo.lock b/Cargo.lock index 2d08b579c52..6e17b5d6615 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8475,6 +8475,7 @@ dependencies = [ "parking_lot", "reqwest", "serde", + "serde_json", "task_executor", "tracing", "types", diff --git a/book/src/contributing_setup.md b/book/src/contributing_setup.md index 62e590e28f8..7e4dfad01fb 100644 --- a/book/src/contributing_setup.md +++ b/book/src/contributing_setup.md @@ -12,7 +12,7 @@ The additional requirements for developers are: - [`cmake`](https://cmake.org/cmake/help/latest/command/install.html). Used by some dependencies. See [`Installation Guide`](./installation.md) for more info. -- [`java 17 runtime`](https://openjdk.java.net/projects/jdk/). 17 is the minimum, +- [`java 25 runtime`](https://openjdk.java.net/projects/jdk/). 25 is the minimum, used by web3signer_tests. ## Using `make` diff --git a/testing/web3signer_tests/src/get_web3signer.rs b/testing/web3signer_tests/src/get_web3signer.rs index 0c3d9b02db5..ede7aa9d43e 100644 --- a/testing/web3signer_tests/src/get_web3signer.rs +++ b/testing/web3signer_tests/src/get_web3signer.rs @@ -1,5 +1,4 @@ -//! This build script downloads the latest Web3Signer release and places it in the `OUT_DIR` so it -//! can be used for integration testing. +//! Downloads a Web3Signer release and places it in a temp directory for integration testing. use reqwest::Client; use std::env; @@ -7,33 +6,41 @@ use std::fs; use std::path::{Path, PathBuf}; use zip::ZipArchive; -/// Use `None` to download the latest Github release. -/// Use `Some("21.8.1")` to download a specific version. -const FIXED_VERSION_STRING: Option<&str> = None; +/// Use `None` to require `LIGHTHOUSE_WEB3SIGNER_VERSION`. +/// Use `Some("26.9.0")` to download a specific version. +/// +/// Pin to 26.9.0 for Gloas (remote-signing-api v1.4.0). +const FIXED_VERSION_STRING: Option<&str> = Some("26.9.0"); // This function no longer makes any attempt to avoid downloads, because in practice we use it // with a fresh temp directory every time we run the tests. We might want to change this in future // to enable reproducible/offline testing. pub async fn download_binary(dest_dir: PathBuf) { - let version = if let Some(version) = FIXED_VERSION_STRING { - version.to_string() - } else if let Ok(env_version) = env::var("LIGHTHOUSE_WEB3SIGNER_VERSION") { + let version = if let Ok(env_version) = env::var("LIGHTHOUSE_WEB3SIGNER_VERSION") { env_version + } else if let Some(version) = FIXED_VERSION_STRING { + version.to_string() } else { - // The Consenys artifact server resolves `latest` to the latest release. We previously hit - // the Github API to establish the version, but that is no longer necessary. - "latest".to_string() + panic!("set FIXED_VERSION_STRING or LIGHTHOUSE_WEB3SIGNER_VERSION"); }; eprintln!("Downloading web3signer version: {version}"); - // Download the release zip. + // Download the release zip from GitHub (artifacts.consensys.net does not host 26.9.0). let client = Client::builder().build().unwrap(); let zip_url = format!( - "https://artifacts.consensys.net/public/web3signer/raw/names/web3signer.zip/versions/{}/web3signer-{}.zip", - version, version + "https://github.com/Consensys-Incorporated/web3signer/releases/download/{version}/web3signer-{version}.zip" ); - let zip_response = client - .get(zip_url) + let mut request = client + .get(&zip_url) + .header("User-Agent", "lighthouse-web3signer-tests"); + if let Some(token) = env::var("LIGHTHOUSE_GITHUB_TOKEN") + .ok() + .or_else(|| env::var("GITHUB_TOKEN").ok()) + .filter(|token| !token.is_empty()) + { + request = request.header("Authorization", format!("Bearer {token}")); + } + let zip_response = request .send() .await .unwrap() diff --git a/testing/web3signer_tests/src/lib.rs b/testing/web3signer_tests/src/lib.rs index c4a67ba8e17..379211bc318 100644 --- a/testing/web3signer_tests/src/lib.rs +++ b/testing/web3signer_tests/src/lib.rs @@ -319,6 +319,7 @@ mod tests { slashing_protection_config: SlashingProtectionConfig, using_web3signer: bool, spec: Arc, + initial_slot: Slot, ) -> Self { let validator_dir = TempDir::new().unwrap(); @@ -352,6 +353,7 @@ mod tests { let slot_clock = TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1)); + slot_clock.set_slot(initial_slot.as_u64()); let config = lighthouse_validator_store::Config { enable_web3signer_slashing_protection: slashing_protection_config.local, ..Default::default() @@ -407,6 +409,23 @@ mod tests { slashing_protection_config: SlashingProtectionConfig, spec: Arc, listen_port: u16, + ) -> Self { + Self::new_at_slot( + network, + slashing_protection_config, + spec, + listen_port, + Slot::new(0), + ) + .await + } + + pub async fn new_at_slot( + network: &str, + slashing_protection_config: SlashingProtectionConfig, + spec: Arc, + listen_port: u16, + initial_slot: Slot, ) -> Self { let signer_rig = Web3SignerRig::new(network, WEB3SIGNER_LISTEN_ADDRESS, listen_port).await; @@ -434,6 +453,7 @@ mod tests { slashing_protection_config, false, spec.clone(), + initial_slot, ) .await }; @@ -462,6 +482,7 @@ mod tests { slashing_protection_config, true, spec, + initial_slot, ) .await }; @@ -963,6 +984,90 @@ mod tests { .await; } + /// Test all the Gloas types. + async fn test_gloas_types(network: &str, listen_port: u16) { + let network_config = Eth2NetworkConfig::constant(network).unwrap().unwrap(); + let spec = Arc::new(network_config.chain_spec::().unwrap()); + let gloas_fork_slot = spec + .gloas_fork_epoch + .expect("network must schedule Gloas for these tests") + .start_slot(E::slots_per_epoch()); + + TestingRig::new_at_slot( + network, + SlashingProtectionConfig::default(), + spec.clone(), + listen_port, + gloas_fork_slot, + ) + .await + .assert_signatures_match( + "execution_payload_envelope_gloas", + |pubkey, validator_store| async move { + let mut envelope = ExecutionPayloadEnvelope::::empty(); + envelope.payload.slot_number = gloas_fork_slot; + validator_store + .sign_execution_payload_envelope(pubkey, envelope) + .await + .unwrap() + .signature + }, + ) + .await + .assert_signatures_match( + "payload_attestation_message_gloas", + |pubkey, validator_store| async move { + validator_store.set_validator_index(&pubkey, 0); + let data = PayloadAttestationData { + beacon_block_root: Hash256::zero(), + slot: gloas_fork_slot, + payload_present: true, + blob_data_available: true, + }; + validator_store + .sign_payload_attestation(pubkey, data) + .await + .unwrap() + .signature + }, + ) + .await + .assert_signatures_match( + "proposer_preferences_gloas", + |pubkey, validator_store| async move { + let preferences = ProposerPreferences { + dependent_root: Hash256::zero(), + proposal_slot: gloas_fork_slot, + validator_index: 0, + fee_recipient: Address::repeat_byte(42), + target_gas_limit: 30_000_000, + }; + validator_store + .sign_proposer_preferences(pubkey, preferences) + .await + .unwrap() + .signature + }, + ) + .await + .assert_signatures_match( + "builder_request_auth_gloas", + |pubkey, validator_store| async move { + let request_auth = eth2::types::RequestAuth { + data: eth2::types::RequestAuthData::new(b"http://builder.example.com".to_vec()) + .unwrap(), + slot: gloas_fork_slot, + }; + validator_store + .sign_request_auth_v1(pubkey, request_auth) + .await + .unwrap() + .signature + }, + ) + .await; + } + #[tokio::test] async fn mainnet_base_types() { test_base_types("mainnet", 4242).await @@ -999,6 +1104,11 @@ mod tests { test_bellatrix_types("sepolia", 4252).await } + #[tokio::test] + async fn sepolia_gloas_types() { + test_gloas_types("sepolia", 4255).await + } + #[tokio::test] async fn slashing_protection_disabled_locally() { test_lighthouse_slashing_protection(SlashingProtectionConfig { local: false }, 4253).await diff --git a/validator_client/signing_method/Cargo.toml b/validator_client/signing_method/Cargo.toml index 2a33382d5e8..f17175ff238 100644 --- a/validator_client/signing_method/Cargo.toml +++ b/validator_client/signing_method/Cargo.toml @@ -18,3 +18,6 @@ tracing = { workspace = true } types = { workspace = true } url = { workspace = true } validator_metrics = { workspace = true } + +[dev-dependencies] +serde_json = { workspace = true } diff --git a/validator_client/signing_method/src/lib.rs b/validator_client/signing_method/src/lib.rs index f877afeaa7e..6d902fe15e0 100644 --- a/validator_client/signing_method/src/lib.rs +++ b/validator_client/signing_method/src/lib.rs @@ -243,20 +243,24 @@ impl SigningMethod { } SignableMessage::VoluntaryExit(e) => Web3SignerObject::VoluntaryExit(e), SignableMessage::ExecutionPayloadEnvelope(e) => { - Web3SignerObject::ExecutionPayloadEnvelope(e) + Web3SignerObject::execution_payload_envelope(e) } SignableMessage::PayloadAttestationData(d) => { - Web3SignerObject::PayloadAttestationData(d) + Web3SignerObject::payload_attestation_message(d) } SignableMessage::ProposerPreferences(p) => { - Web3SignerObject::ProposerPreferences(p) + Web3SignerObject::proposer_preferences(p) } - SignableMessage::RequestAuth(r) => Web3SignerObject::RequestAuth(r), + SignableMessage::RequestAuth(r) => Web3SignerObject::builder_request_auth(r), }; // Determine the Web3Signer message type. let message_type = object.message_type(); - if matches!(message_type, MessageType::ValidatorRegistration) && fork_info.is_some() + // VALIDATOR_REGISTRATION and BUILDER_REQUEST_AUTH must omit fork_info. + if matches!( + message_type, + MessageType::ValidatorRegistration | MessageType::BuilderRequestAuth + ) && fork_info.is_some() { return Err(Error::GenesisForkVersionRequired); } diff --git a/validator_client/signing_method/src/web3signer.rs b/validator_client/signing_method/src/web3signer.rs index 505147a46d4..271ca67dae8 100644 --- a/validator_client/signing_method/src/web3signer.rs +++ b/validator_client/signing_method/src/web3signer.rs @@ -20,11 +20,10 @@ pub enum MessageType { SyncCommitteeSelectionProof, SyncCommitteeContributionAndProof, ValidatorRegistration, - // TODO(gloas) verify w/ web3signer specs ExecutionPayloadEnvelope, - PayloadAttestation, + PayloadAttestationMessage, ProposerPreferences, - RequestAuth, + BuilderRequestAuth, } #[derive(Debug, PartialEq, Copy, Clone, Serialize)] @@ -47,6 +46,23 @@ pub struct ForkInfo { pub genesis_validators_root: Hash256, } +/// `{version, data}` wrapper for Gloas remote-signing payloads. +#[doc(hidden)] +#[derive(Debug, PartialEq, Serialize)] +pub struct VersionedForkData<'a, T> { + version: ForkName, + data: &'a T, +} + +impl<'a, T> VersionedForkData<'a, T> { + fn gloas(data: &'a T) -> Self { + Self { + version: ForkName::Gloas, + data, + } + } +} + #[derive(Debug, PartialEq, Serialize)] #[serde(bound = "E: EthSpec", rename_all = "snake_case")] pub enum Web3SignerObject<'a, E: EthSpec, Payload: AbstractExecPayload> { @@ -82,10 +98,10 @@ pub enum Web3SignerObject<'a, E: EthSpec, Payload: AbstractExecPayload> { SyncAggregatorSelectionData(&'a SyncAggregatorSelectionData), ContributionAndProof(&'a ContributionAndProof), ValidatorRegistration(&'a ValidatorRegistrationData), - ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope), - PayloadAttestationData(&'a PayloadAttestationData), - ProposerPreferences(&'a ProposerPreferences), - RequestAuth(&'a RequestAuth), + ExecutionPayloadEnvelope(VersionedForkData<'a, ExecutionPayloadEnvelope>), + PayloadAttestationMessage(VersionedForkData<'a, PayloadAttestationData>), + ProposerPreferences(VersionedForkData<'a, ProposerPreferences>), + BuilderRequestAuth(VersionedForkData<'a, RequestAuth>), } impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Payload> { @@ -139,6 +155,22 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Pa } } + pub fn execution_payload_envelope(envelope: &'a ExecutionPayloadEnvelope) -> Self { + Web3SignerObject::ExecutionPayloadEnvelope(VersionedForkData::gloas(envelope)) + } + + pub fn payload_attestation_message(data: &'a PayloadAttestationData) -> Self { + Web3SignerObject::PayloadAttestationMessage(VersionedForkData::gloas(data)) + } + + pub fn proposer_preferences(preferences: &'a ProposerPreferences) -> Self { + Web3SignerObject::ProposerPreferences(VersionedForkData::gloas(preferences)) + } + + pub fn builder_request_auth(request_auth: &'a RequestAuth) -> Self { + Web3SignerObject::BuilderRequestAuth(VersionedForkData::gloas(request_auth)) + } + pub fn message_type(&self) -> MessageType { match self { Web3SignerObject::AggregationSlot { .. } => MessageType::AggregationSlot, @@ -157,9 +189,11 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Pa } Web3SignerObject::ValidatorRegistration(_) => MessageType::ValidatorRegistration, Web3SignerObject::ExecutionPayloadEnvelope(_) => MessageType::ExecutionPayloadEnvelope, - Web3SignerObject::PayloadAttestationData(_) => MessageType::PayloadAttestation, + Web3SignerObject::PayloadAttestationMessage(_) => { + MessageType::PayloadAttestationMessage + } Web3SignerObject::ProposerPreferences(_) => MessageType::ProposerPreferences, - Web3SignerObject::RequestAuth(_) => MessageType::RequestAuth, + Web3SignerObject::BuilderRequestAuth(_) => MessageType::BuilderRequestAuth, } } } @@ -181,3 +215,125 @@ pub struct SigningRequest<'a, E: EthSpec, Payload: AbstractExecPayload> { pub struct SigningResponse { pub signature: Signature, } + +#[cfg(test)] +mod tests { + use super::*; + use bls::FixedBytesExtended; + use builder_types::RequestAuthData; + use types::{FullPayload, MainnetEthSpec}; + + type E = MainnetEthSpec; + type Payload = FullPayload; + + fn dummy_fork_info() -> ForkInfo { + ForkInfo { + fork: Fork { + previous_version: [0; 4], + current_version: [1; 4], + epoch: Epoch::new(1), + }, + genesis_validators_root: Hash256::repeat_byte(42), + } + } + + fn serialize( + message_type: MessageType, + fork_info: Option, + object: Web3SignerObject<'_, E, Payload>, + ) -> serde_json::Value { + serde_json::to_value(SigningRequest { + message_type, + fork_info, + signing_root: Hash256::zero(), + object, + }) + .expect("signing request should serialize") + } + + #[test] + fn execution_payload_envelope_request_shape() { + let envelope = ExecutionPayloadEnvelope::::empty(); + let value = serialize( + MessageType::ExecutionPayloadEnvelope, + Some(dummy_fork_info()), + Web3SignerObject::execution_payload_envelope(&envelope), + ); + + assert_eq!(value["type"], "EXECUTION_PAYLOAD_ENVELOPE"); + assert!(value.get("fork_info").is_some()); + assert_eq!(value["execution_payload_envelope"]["version"], "GLOAS"); + assert!(value["execution_payload_envelope"].get("data").is_some()); + assert!(value.get("payload_attestation_data").is_none()); + assert!(value.get("request_auth").is_none()); + } + + #[test] + fn payload_attestation_message_request_shape() { + let data = PayloadAttestationData { + beacon_block_root: Hash256::zero(), + slot: Slot::new(1), + payload_present: true, + blob_data_available: false, + }; + let value = serialize( + MessageType::PayloadAttestationMessage, + Some(dummy_fork_info()), + Web3SignerObject::payload_attestation_message(&data), + ); + + assert_eq!(value["type"], "PAYLOAD_ATTESTATION_MESSAGE"); + assert!(value.get("fork_info").is_some()); + assert_eq!(value["payload_attestation_message"]["version"], "GLOAS"); + assert_eq!( + value["payload_attestation_message"]["data"]["payload_present"], + true + ); + assert!(value.get("payload_attestation_data").is_none()); + assert!(value.get("PAYLOAD_ATTESTATION").is_none()); + } + + #[test] + fn proposer_preferences_request_shape() { + let preferences = ProposerPreferences { + dependent_root: Hash256::zero(), + proposal_slot: Slot::new(32), + validator_index: 1, + fee_recipient: Address::repeat_byte(1), + target_gas_limit: 30_000_000, + }; + let value = serialize( + MessageType::ProposerPreferences, + Some(dummy_fork_info()), + Web3SignerObject::proposer_preferences(&preferences), + ); + + assert_eq!(value["type"], "PROPOSER_PREFERENCES"); + assert!(value.get("fork_info").is_some()); + assert_eq!(value["proposer_preferences"]["version"], "GLOAS"); + assert_eq!( + value["proposer_preferences"]["data"]["target_gas_limit"], + "30000000" + ); + } + + #[test] + fn builder_request_auth_request_shape_omits_fork_info() { + let request_auth = RequestAuth { + data: RequestAuthData::new(b"http://builder.example.com".to_vec()).unwrap(), + slot: Slot::new(32), + }; + let value = serialize( + MessageType::BuilderRequestAuth, + None, + Web3SignerObject::builder_request_auth(&request_auth), + ); + + assert_eq!(value["type"], "BUILDER_REQUEST_AUTH"); + assert!(value.get("fork_info").is_none()); + assert_eq!(value["builder_request_auth"]["version"], "GLOAS"); + assert!(value["builder_request_auth"]["data"].get("data").is_some()); + assert_eq!(value["builder_request_auth"]["data"]["slot"], "32"); + assert!(value.get("request_auth").is_none()); + } +} From c8efbe7149ef853ee63ca843b4071ad4edfab760 Mon Sep 17 00:00:00 2001 From: Mac L Date: Wed, 7 Oct 2026 06:29:34 +0400 Subject: [PATCH 36/44] Split up EF tests to run each preset separately (#10237) ## Issue Addressed Follows on from #10158, another piece of #9229 ## Proposed Changes Move the EF tests to run separately using compile time preset selection. --- .github/workflows/test-suite.yml | 33 +- Makefile | 20 +- testing/ef_tests/check_all_files_accessed.py | 3 + testing/ef_tests/tests/tests.rs | 798 +++++-------------- 4 files changed, 270 insertions(+), 584 deletions(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 913d45da774..065203226d5 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -309,8 +309,8 @@ jobs: cache-target: release - name: Run state_transition_vectors in release. run: make run-state-transition-tests - ef-tests-ubuntu: - name: ef-tests-ubuntu + ef-tests-mainnet-ubuntu: + name: ef-tests-mainnet-ubuntu needs: [check-labels] if: needs.check-labels.outputs.skip_ci != 'true' runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }} @@ -329,8 +329,30 @@ jobs: uses: Swatinem/rust-cache@v2 with: cache-provider: warpbuild - - name: Run consensus-spec-tests with blst and fake_crypto - run: make test-ef + - name: Run mainnet consensus-spec-tests with blst and fake_crypto + run: make test-ef-mainnet + ef-tests-minimal-ubuntu: + name: ef-tests-minimal-ubuntu + needs: [check-labels] + if: needs.check-labels.outputs.skip_ci != 'true' + runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v5 + - if: github.repository != 'sigp/lighthouse' + name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + bins: cargo-nextest + - if: github.repository == 'sigp/lighthouse' + uses: Swatinem/rust-cache@v2 + with: + cache-provider: warpbuild + - name: Run minimal consensus-spec-tests with blst and fake_crypto + run: make test-ef-minimal basic-simulator-ubuntu: name: basic-simulator-ubuntu needs: [check-labels] @@ -548,7 +570,8 @@ jobs: 'http-api-tests', 'debug-tests-ubuntu', 'state-transition-vectors-ubuntu', - 'ef-tests-ubuntu', + 'ef-tests-mainnet-ubuntu', + 'ef-tests-minimal-ubuntu', 'basic-simulator-ubuntu', 'fallback-simulator-ubuntu', 'execution-engine-integration-ubuntu', diff --git a/Makefile b/Makefile index 085e06ea2bd..34b46efe391 100644 --- a/Makefile +++ b/Makefile @@ -202,10 +202,22 @@ check-benches: # Runs EF test vectors run-ef-tests: + $(MAKE) run-ef-tests-mainnet + $(MAKE) run-ef-tests-minimal + +# Runs the mainnet EF test vectors +run-ef-tests-mainnet: rm -rf $(EF_TESTS)/.accessed_file_log.txt cargo nextest run --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES)" cargo nextest run --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES),fake_crypto" - ./$(EF_TESTS)/check_all_files_accessed.py $(EF_TESTS)/.accessed_file_log.txt $(EF_TESTS)/consensus-spec-tests + ./$(EF_TESTS)/check_all_files_accessed.py $(EF_TESTS)/.accessed_file_log.txt $(EF_TESTS)/consensus-spec-tests "tests/minimal/" + +# Runs the minimal EF test vectors +run-ef-tests-minimal: + rm -rf $(EF_TESTS)/.accessed_file_log.txt + cargo nextest run --release -p ef_tests --features "ef_tests,spec-minimal,$(EF_TEST_FEATURES)" + cargo nextest run --release -p ef_tests --features "ef_tests,spec-minimal,$(EF_TEST_FEATURES),fake_crypto" + ./$(EF_TESTS)/check_all_files_accessed.py $(EF_TESTS)/.accessed_file_log.txt $(EF_TESTS)/consensus-spec-tests "tests/mainnet/" # Run the tests in the `beacon_chain` crate for all known forks. test-beacon-chain: $(patsubst %,test-beacon-chain-%,$(RECENT_FORKS)) @@ -259,6 +271,12 @@ run-state-transition-tests: # Downloads and runs the EF test vectors. test-ef: make-ef-tests run-ef-tests +# Downloads and runs the mainnet EF test vectors. +test-ef-mainnet: make-ef-tests run-ef-tests-mainnet + +# Downloads and runs the minimal EF test vectors. +test-ef-minimal: make-ef-tests run-ef-tests-minimal + # Downloads and runs the nightly EF test vectors. test-ef-nightly: make-ef-tests-nightly run-ef-tests diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 2fc9f21c42a..2175d6ef941 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -92,6 +92,9 @@ "tests/.*/.*/networking/gossip_partial_data_column_sidecar/.*", ] +# Extra arguments are ignored paths. Useful for skipping another preset's vectors. +excluded_paths += sys.argv[3:] + def normalize_path(path): return path.split("consensus-spec-tests/")[1] diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 1a166f18f88..f6394ba6bf2 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -23,160 +23,134 @@ fn check_typenum_values() { #[test] fn derived_typenum_values() { - check_typenum_values::(); - check_typenum_values::(); + check_typenum_values::(); } #[test] fn shuffling() { - ShufflingHandler::::default().run(); - ShufflingHandler::::default().run(); + ShufflingHandler::::default().run(); } #[test] fn operations_deposit() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] fn operations_exit() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] fn operations_proposer_slashing() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] fn operations_attester_slashing() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_attestation() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_block_header() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_sync_aggregate() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_execution_payload_full() { - OperationsHandler::>>::default().run(); - OperationsHandler::>>::default().run(); + OperationsHandler::>>::default().run(); } #[test] fn operations_execution_payload_blinded() { - OperationsHandler::>>::default().run(); - OperationsHandler::>>::default().run(); + OperationsHandler::>>::default().run(); } #[test] fn operations_execution_payload_envelope() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_execution_payload_bid() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_parent_execution_payload() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_payload_attestation() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_withdrawals() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_withdrawal_requests() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] #[cfg(not(feature = "fake_crypto"))] fn operations_deposit_requests() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] fn operations_consolidations() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] #[cfg(not(feature = "fake_crypto"))] fn operations_builder_deposit_requests() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] fn operations_builder_exit_requests() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] fn operations_bls_to_execution_change() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] fn operations_voluntary_exit_churn() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] fn sanity_blocks() { - SanityBlocksHandler::::default().run(); - SanityBlocksHandler::::default().run(); + SanityBlocksHandler::::default().run(); } #[test] fn sanity_slots() { - SanitySlotsHandler::::default().run(); - SanitySlotsHandler::::default().run(); + SanitySlotsHandler::::default().run(); } #[test] fn random() { - RandomHandler::::default().run(); - RandomHandler::::default().run(); + RandomHandler::::default().run(); } #[test] @@ -237,7 +211,7 @@ macro_rules! ssz_static_test { /// Generate a function to run the SSZ static tests for a type. /// -/// Quite complex in order to support an optional #[test] attrib, generics, and the two EthSpecs. +/// Quite complex in order to support an optional #[test] attrib and generics. #[cfg(feature = "fake_crypto")] macro_rules! ssz_static_test_no_run { // Top-level @@ -251,8 +225,7 @@ macro_rules! ssz_static_test_no_run { $test_name, $handler, { - ($typ, MinimalEthSpec), - ($typ, MainnetEthSpec) + ($typ, Spec) } ); }; @@ -263,8 +236,7 @@ macro_rules! ssz_static_test_no_run { $test_name, $handler, { - ($typ, MinimalEthSpec), - ($typ, MainnetEthSpec) + ($typ, Spec) } ); }; @@ -308,14 +280,12 @@ mod ssz_static { // Altair and Bellatrix. #[test] fn historical_batch() { - SszStaticHandler::, MinimalEthSpec>::pre_capella().run(); - SszStaticHandler::, MainnetEthSpec>::pre_capella().run(); + SszStaticHandler::, Spec>::pre_capella().run(); } // `PendingAttestation` was removed in Altair, so test vectors only exist for Base. #[test] fn pending_attestation() { - SszStaticHandler::, MinimalEthSpec>::base_only().run(); - SszStaticHandler::, MainnetEthSpec>::base_only().run(); + SszStaticHandler::, Spec>::base_only().run(); } ssz_static_test!(proposer_slashing, ProposerSlashing); ssz_static_test!( @@ -331,802 +301,492 @@ mod ssz_static { #[test] fn attestation() { - SszStaticHandler::, MinimalEthSpec>::pre_electra().run(); - SszStaticHandler::, MainnetEthSpec>::pre_electra().run(); - SszStaticHandler::, MinimalEthSpec>::electra_through_fulu() - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_through_fulu() - .run(); - SszStaticHandler::, MinimalEthSpec>::gloas_and_later() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later() - .run(); + SszStaticHandler::, Spec>::pre_electra().run(); + SszStaticHandler::, Spec>::electra_through_fulu().run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn single_attestation() { - SszStaticHandler::::electra_and_later().run(); - SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); } #[test] fn attester_slashing() { - SszStaticHandler::, MinimalEthSpec>::pre_electra() - .run(); - SszStaticHandler::, MainnetEthSpec>::pre_electra() - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_through_fulu() - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_through_fulu() - .run(); - SszStaticHandler::, MinimalEthSpec>::gloas_and_later( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later( - ) - .run(); + SszStaticHandler::, Spec>::pre_electra().run(); + SszStaticHandler::, Spec>::electra_through_fulu().run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn indexed_attestation() { - SszStaticHandler::, MinimalEthSpec>::pre_electra() - .run(); - SszStaticHandler::, MainnetEthSpec>::pre_electra() - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_through_fulu() - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_through_fulu() - .run(); - SszStaticHandler::, MinimalEthSpec>::gloas_and_later() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later() - .run(); + SszStaticHandler::, Spec>::pre_electra().run(); + SszStaticHandler::, Spec>::electra_through_fulu().run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn signed_aggregate_and_proof() { - SszStaticHandler::, MinimalEthSpec>::pre_electra( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::pre_electra( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_through_fulu( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_through_fulu( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::gloas_and_later( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later( - ) - .run(); + SszStaticHandler::, Spec>::pre_electra().run(); + SszStaticHandler::, Spec>::electra_through_fulu() + .run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn aggregate_and_proof() { - SszStaticHandler::, MinimalEthSpec>::pre_electra() - .run(); - SszStaticHandler::, MainnetEthSpec>::pre_electra() - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_through_fulu( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_through_fulu( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::gloas_and_later( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later( - ) - .run(); + SszStaticHandler::, Spec>::pre_electra().run(); + SszStaticHandler::, Spec>::electra_through_fulu().run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } // BeaconBlockBody has no internal indicator of which fork it is for, so we test it separately. #[test] fn beacon_block_body() { - SszStaticHandler::, MinimalEthSpec>::base_only().run(); - SszStaticHandler::, MainnetEthSpec>::base_only().run(); - SszStaticHandler::, MinimalEthSpec>::altair_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::altair_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::capella_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::capella_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only().run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only().run(); - SszStaticHandler::, MinimalEthSpec>::gloas_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::heze_only().run(); - SszStaticHandler::, MainnetEthSpec>::heze_only().run(); + SszStaticHandler::, Spec>::base_only().run(); + SszStaticHandler::, Spec>::altair_only().run(); + SszStaticHandler::, Spec>::bellatrix_only().run(); + SszStaticHandler::, Spec>::capella_only().run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); + SszStaticHandler::, Spec>::gloas_only().run(); + SszStaticHandler::, Spec>::heze_only().run(); } // Altair and later #[test] fn contribution_and_proof() { - SszStaticHandler::, MinimalEthSpec>::altair_and_later( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::altair_and_later( - ) - .run(); + SszStaticHandler::, Spec>::altair_and_later().run(); } // LightClientBootstrap has no internal indicator of which fork it is for, so we test it separately. #[test] fn light_client_bootstrap() { - SszStaticHandler::, MinimalEthSpec>::altair_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::altair_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::bellatrix_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::bellatrix_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::capella_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::capella_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only() - .run(); + SszStaticHandler::, Spec>::altair_only().run(); + SszStaticHandler::, Spec>::bellatrix_only().run(); + SszStaticHandler::, Spec>::capella_only().run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); } // LightClientHeader has no internal indicator of which fork it is for, so we test it separately. #[test] fn light_client_header() { - SszStaticHandler::, MinimalEthSpec>::altair_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::altair_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::bellatrix_only() - .run(); + SszStaticHandler::, Spec>::altair_only().run(); + SszStaticHandler::, Spec>::bellatrix_only().run(); - SszStaticHandler::, MinimalEthSpec>::capella_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::capella_only( - ) - .run(); + SszStaticHandler::, Spec>::capella_only().run(); - SszStaticHandler::, MinimalEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only() - .run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); } // LightClientOptimisticUpdate has no internal indicator of which fork it is for, so we test it separately. #[test] fn light_client_optimistic_update() { - SszStaticHandler::, MinimalEthSpec>::altair_only().run(); - SszStaticHandler::, MainnetEthSpec>::altair_only().run(); - SszStaticHandler::, MinimalEthSpec>::bellatrix_only().run(); - SszStaticHandler::, MainnetEthSpec>::bellatrix_only().run(); - SszStaticHandler::, MinimalEthSpec>::capella_only().run(); - SszStaticHandler::, MainnetEthSpec>::capella_only().run(); - SszStaticHandler::, MinimalEthSpec>::deneb_only().run(); - SszStaticHandler::, MainnetEthSpec>::deneb_only().run(); - SszStaticHandler::, MinimalEthSpec>::electra_only().run(); - SszStaticHandler::, MainnetEthSpec>::electra_only().run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only().run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only().run(); + SszStaticHandler::, Spec>::altair_only().run(); + SszStaticHandler::, Spec>::bellatrix_only().run(); + SszStaticHandler::, Spec>::capella_only().run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); } // LightClientFinalityUpdate has no internal indicator of which fork it is for, so we test it separately. #[test] fn light_client_finality_update() { - SszStaticHandler::, MinimalEthSpec>::altair_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::altair_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::bellatrix_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::bellatrix_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::capella_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::capella_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::deneb_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::deneb_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only( - ) - .run(); + SszStaticHandler::, Spec>::altair_only().run(); + SszStaticHandler::, Spec>::bellatrix_only().run(); + SszStaticHandler::, Spec>::capella_only().run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); } // LightClientUpdate has no internal indicator of which fork it is for, so we test it separately. #[test] fn light_client_update() { - SszStaticHandler::, MinimalEthSpec>::altair_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::altair_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::capella_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::capella_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only() - .run(); + SszStaticHandler::, Spec>::altair_only().run(); + SszStaticHandler::, Spec>::bellatrix_only().run(); + SszStaticHandler::, Spec>::capella_only().run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); } #[test] fn signed_contribution_and_proof() { - SszStaticHandler::, MinimalEthSpec>::altair_and_later().run(); - SszStaticHandler::, MainnetEthSpec>::altair_and_later().run(); + SszStaticHandler::, Spec>::altair_and_later().run(); } #[test] fn sync_aggregate() { - SszStaticHandler::, MinimalEthSpec>::altair_and_later().run(); - SszStaticHandler::, MainnetEthSpec>::altair_and_later().run(); + SszStaticHandler::, Spec>::altair_and_later().run(); } #[test] fn sync_committee() { - SszStaticHandler::, MinimalEthSpec>::altair_and_later().run(); - SszStaticHandler::, MainnetEthSpec>::altair_and_later().run(); + SszStaticHandler::, Spec>::altair_and_later().run(); } #[test] fn sync_committee_contribution() { - SszStaticHandler::, MinimalEthSpec>::altair_and_later().run(); - SszStaticHandler::, MainnetEthSpec>::altair_and_later().run(); + SszStaticHandler::, Spec>::altair_and_later().run(); } #[test] fn sync_committee_message() { - SszStaticHandler::::altair_and_later().run(); - SszStaticHandler::::altair_and_later().run(); + SszStaticHandler::::altair_and_later().run(); } #[test] fn sync_aggregator_selection_data() { - SszStaticHandler::::altair_and_later().run(); - SszStaticHandler::::altair_and_later().run(); + SszStaticHandler::::altair_and_later().run(); } // Bellatrix and later #[test] fn execution_payload() { - SszStaticHandler::, MinimalEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::capella_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::capella_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only().run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only().run(); - SszStaticHandler::, MinimalEthSpec>::gloas_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::heze_only().run(); - SszStaticHandler::, MainnetEthSpec>::heze_only().run(); + SszStaticHandler::, Spec>::bellatrix_only().run(); + SszStaticHandler::, Spec>::capella_only().run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); + SszStaticHandler::, Spec>::gloas_only().run(); + SszStaticHandler::, Spec>::heze_only().run(); } #[test] fn execution_payload_header() { - SszStaticHandler::, MinimalEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MinimalEthSpec> - ::capella_only().run(); - SszStaticHandler::, MainnetEthSpec> - ::capella_only().run(); - SszStaticHandler::, MinimalEthSpec> - ::deneb_only().run(); - SszStaticHandler::, MainnetEthSpec> - ::deneb_only().run(); - SszStaticHandler::, MinimalEthSpec> - ::electra_only().run(); - SszStaticHandler::, MainnetEthSpec> - ::electra_only().run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only() - .run(); + SszStaticHandler::, Spec>::bellatrix_only().run(); + SszStaticHandler::, Spec>::capella_only().run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); } #[test] fn execution_payload_bid() { - SszStaticHandler::, MinimalEthSpec>::gloas_and_later() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later() - .run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn signed_execution_payload_bid() { - SszStaticHandler::, MinimalEthSpec>::gloas_and_later().run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later().run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn withdrawal() { - SszStaticHandler::::capella_and_later().run(); - SszStaticHandler::::capella_and_later().run(); + SszStaticHandler::::capella_and_later().run(); } #[test] fn bls_to_execution_change() { - SszStaticHandler::::capella_and_later().run(); - SszStaticHandler::::capella_and_later().run(); + SszStaticHandler::::capella_and_later().run(); } #[test] fn signed_bls_to_execution_change() { - SszStaticHandler::::capella_and_later().run(); - SszStaticHandler::::capella_and_later().run(); + SszStaticHandler::::capella_and_later().run(); } #[test] fn blob_sidecar() { - SszStaticHandler::, MinimalEthSpec>::deneb_only().run(); - SszStaticHandler::, MainnetEthSpec>::deneb_only().run(); - SszStaticHandler::, MinimalEthSpec>::electra_only().run(); - SszStaticHandler::, MainnetEthSpec>::electra_only().run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); } #[test] fn blob_identifier() { - SszStaticHandler::::deneb_only().run(); - SszStaticHandler::::deneb_only().run(); - SszStaticHandler::::electra_only().run(); - SszStaticHandler::::electra_only().run(); + SszStaticHandler::::deneb_only().run(); + SszStaticHandler::::electra_only().run(); } #[test] fn historical_summary() { - SszStaticHandler::::capella_and_later().run(); - SszStaticHandler::::capella_and_later().run(); + SszStaticHandler::::capella_and_later().run(); } #[test] fn data_column_sidecar() { - SszStaticHandler::, MinimalEthSpec>::fulu_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::gloas_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::heze_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::heze_only() - .run(); + SszStaticHandler::, Spec>::fulu_only().run(); + SszStaticHandler::, Spec>::gloas_only().run(); + SszStaticHandler::, Spec>::heze_only().run(); } #[test] fn data_column_by_root_identifier() { - SszStaticWithSpecHandler::< - DataColumnsByRootIdentifier, - MinimalEthSpec, - >::fulu_and_later() - .run(); - SszStaticWithSpecHandler::< - DataColumnsByRootIdentifier, - MainnetEthSpec, - >::fulu_and_later() - .run(); + SszStaticWithSpecHandler::, Spec>::fulu_and_later().run(); } #[test] fn consolidation() { - SszStaticHandler::::electra_and_later().run(); - SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); } #[test] fn deposit_request() { - SszStaticHandler::::electra_and_later().run(); - SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); } #[test] fn withdrawal_request() { - SszStaticHandler::::electra_and_later().run(); - SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); } #[test] fn pending_balance_deposit() { - SszStaticHandler::::electra_and_later().run(); - SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); } #[test] fn pending_consolidation() { - SszStaticHandler::::electra_and_later().run(); - SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); } #[test] fn pending_partial_withdrawal() { - SszStaticHandler::::electra_and_later().run(); - SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); } #[test] fn execution_requests() { - SszStaticHandler::, MainnetEthSpec>::electra_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::gloas_only() - .run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); + SszStaticHandler::, Spec>::gloas_only().run(); } // Gloas and later #[test] fn builder() { - SszStaticHandler::::gloas_and_later().run(); - SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); } #[test] fn builder_deposit_request() { - SszStaticHandler::::gloas_and_later().run(); - SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); } #[test] fn builder_exit_request() { - SszStaticHandler::::gloas_and_later().run(); - SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); } #[test] fn builder_pending_payment() { - SszStaticHandler::::gloas_and_later().run(); - SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); } #[test] fn builder_pending_withdrawal() { - SszStaticHandler::::gloas_and_later().run(); - SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); } #[test] fn payload_attestation_data() { - SszStaticHandler::::gloas_and_later().run(); - SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); } #[test] fn payload_attestation() { - SszStaticHandler::, MinimalEthSpec>::gloas_and_later() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later() - .run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn payload_attestation_message() { - SszStaticHandler::::gloas_and_later().run(); - SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); } #[test] fn indexed_payload_attestation() { - SszStaticHandler::, MinimalEthSpec>::gloas_and_later() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later() - .run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn execution_payload_envelope() { - SszStaticHandler::, MinimalEthSpec>::gloas_and_later() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later() - .run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn signed_execution_payload_envelope() { - SszStaticHandler::, MinimalEthSpec>::gloas_and_later() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later() - .run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn proposer_preferences() { - SszStaticHandler::::gloas_and_later().run(); - SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); } #[test] fn signed_proposer_preferences() { - SszStaticHandler::::gloas_and_later().run(); - SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); } } #[test] fn epoch_processing_justification_and_finalization() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_rewards_and_penalties() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_registry_updates() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_slashings() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_eth1_data_reset() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_pending_balance_deposits() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_pending_deposits_churn() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_pending_consolidations() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_effective_balance_updates() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_slashings_reset() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_randao_mixes_reset() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_historical_roots_update() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_historical_summaries_update() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_participation_record_updates() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] +#[cfg(feature = "spec-minimal")] fn epoch_processing_sync_committee_updates() { // There are presently no mainnet tests, see: // https://github.com/ethereum/consensus-spec-tests/issues/29 - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_inactivity_updates() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_participation_flag_updates() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_proposer_lookahead() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_ptc_window() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_builder_pending_payments() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn fork_upgrade() { - ForkHandler::::default().run(); - ForkHandler::::default().run(); + ForkHandler::::default().run(); } #[test] fn transition() { - TransitionHandler::::default().run(); - TransitionHandler::::default().run(); + TransitionHandler::::default().run(); } #[test] fn finality() { - FinalityHandler::::default().run(); - FinalityHandler::::default().run(); + FinalityHandler::::default().run(); } #[test] fn fork_choice_get_head() { - ForkChoiceHandler::::new("get_head").run(); - ForkChoiceHandler::::new("get_head").run(); + ForkChoiceHandler::::new("get_head").run(); } #[test] fn fork_choice_on_attestation() { - ForkChoiceHandler::::new("on_attestation").run(); - ForkChoiceHandler::::new("on_attestation").run(); + ForkChoiceHandler::::new("on_attestation").run(); } #[test] fn fork_choice_on_block() { - ForkChoiceHandler::::new("on_block").run(); - ForkChoiceHandler::::new("on_block").run(); + ForkChoiceHandler::::new("on_block").run(); } #[test] fn fork_choice_ex_ante() { - ForkChoiceHandler::::new("ex_ante").run(); - ForkChoiceHandler::::new("ex_ante").run(); + ForkChoiceHandler::::new("ex_ante").run(); } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_reorg() { - ForkChoiceHandler::::new("reorg").run(); + ForkChoiceHandler::::new("reorg").run(); // There is no mainnet variant for this test. } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_withholding() { - ForkChoiceHandler::::new("withholding").run(); + ForkChoiceHandler::::new("withholding").run(); // There is no mainnet variant for this test. } #[test] fn fork_choice_get_proposer_head() { - ForkChoiceHandler::::new("get_proposer_head").run(); - ForkChoiceHandler::::new("get_proposer_head").run(); + ForkChoiceHandler::::new("get_proposer_head").run(); } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_deposit_with_reorg() { - ForkChoiceHandler::::new("deposit_with_reorg").run(); + ForkChoiceHandler::::new("deposit_with_reorg").run(); // There is no mainnet variant for this test. } @@ -1134,8 +794,9 @@ macro_rules! fast_confirmation_tests { ($($name:ident: $handler:literal),* $(,)?) => { $( #[test] + #[cfg(feature = "spec-minimal")] fn $name() { - FastConfirmationHandler::::new($handler).run(); + FastConfirmationHandler::::new($handler).run(); } )* }; @@ -1156,185 +817,166 @@ fast_confirmation_tests! { #[test] fn fork_choice_on_execution_payload_envelope() { - ForkChoiceHandler::::new("on_execution_payload_envelope").run(); - ForkChoiceHandler::::new("on_execution_payload_envelope").run(); + ForkChoiceHandler::::new("on_execution_payload_envelope").run(); } #[test] fn fork_choice_get_parent_payload_status() { - ForkChoiceHandler::::new("get_parent_payload_status").run(); - ForkChoiceHandler::::new("get_parent_payload_status").run(); + ForkChoiceHandler::::new("get_parent_payload_status").run(); } #[test] fn fork_choice_on_payload_attestation_message() { - ForkChoiceHandler::::new("on_payload_attestation_message").run(); - ForkChoiceHandler::::new("on_payload_attestation_message").run(); + ForkChoiceHandler::::new("on_payload_attestation_message").run(); } #[test] fn fork_choice_payload_timeliness() { - ForkChoiceHandler::::new("payload_timeliness").run(); - ForkChoiceHandler::::new("payload_timeliness").run(); + ForkChoiceHandler::::new("payload_timeliness").run(); } #[test] fn fork_choice_payload_data_availability() { - ForkChoiceHandler::::new("payload_data_availability").run(); - ForkChoiceHandler::::new("payload_data_availability").run(); + ForkChoiceHandler::::new("payload_data_availability").run(); } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_should_apply_proposer_boost() { - ForkChoiceHandler::::new("should_apply_proposer_boost").run(); + ForkChoiceHandler::::new("should_apply_proposer_boost").run(); // There is no mainnet variant for this test. } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_compliance_attester_slashing_test() { - ForkChoiceComplianceHandler::::new("attester_slashing_test").run(); + ForkChoiceComplianceHandler::::new("attester_slashing_test").run(); } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_compliance_block_cover_test() { - ForkChoiceComplianceHandler::::new("block_cover_test").run(); + ForkChoiceComplianceHandler::::new("block_cover_test").run(); } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_compliance_block_tree_test() { - ForkChoiceComplianceHandler::::new("block_tree_test").run(); + ForkChoiceComplianceHandler::::new("block_tree_test").run(); } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_compliance_block_weight_test() { - ForkChoiceComplianceHandler::::new("block_weight_test").run(); + ForkChoiceComplianceHandler::::new("block_weight_test").run(); } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_compliance_invalid_message_test() { - ForkChoiceComplianceHandler::::new("invalid_message_test").run(); + ForkChoiceComplianceHandler::::new("invalid_message_test").run(); } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_compliance_shuffling_test() { - ForkChoiceComplianceHandler::::new("shuffling_test").run(); + ForkChoiceComplianceHandler::::new("shuffling_test").run(); } #[test] fn optimistic_sync() { - OptimisticSyncHandler::::default().run(); - OptimisticSyncHandler::::default().run(); + OptimisticSyncHandler::::default().run(); } #[test] +#[cfg(feature = "spec-minimal")] fn genesis_initialization() { - GenesisInitializationHandler::::default().run(); + GenesisInitializationHandler::::default().run(); } #[test] +#[cfg(feature = "spec-minimal")] fn genesis_validity() { - GenesisValidityHandler::::default().run(); + GenesisValidityHandler::::default().run(); // Note: there are no genesis validity tests for mainnet } #[test] fn light_client_merkle_proof_validity() { - MerkleProofValidityHandler::::default().run(); - MerkleProofValidityHandler::::default().run(); + MerkleProofValidityHandler::::default().run(); } #[test] +#[cfg(feature = "spec-minimal")] fn light_client_update() { - LightClientUpdateHandler::::default().run(); + LightClientUpdateHandler::::default().run(); } #[test] #[cfg(feature = "fake_crypto")] fn kzg_inclusion_merkle_proof_validity() { - KzgInclusionMerkleProofValidityHandler::::default().run(); - KzgInclusionMerkleProofValidityHandler::::default().run(); + KzgInclusionMerkleProofValidityHandler::::default().run(); } #[test] fn rewards() { for handler in &["basic", "leak", "random", "inactivity_scores"] { - RewardsHandler::::new(handler).run(); - RewardsHandler::::new(handler).run(); + RewardsHandler::::new(handler).run(); } } #[test] fn get_custody_groups() { - GetCustodyGroupsHandler::::default().run(); - GetCustodyGroupsHandler::::default().run() + GetCustodyGroupsHandler::::default().run(); } #[test] fn compute_columns_for_custody_group() { - ComputeColumnsForCustodyGroupHandler::::default().run(); - ComputeColumnsForCustodyGroupHandler::::default().run(); + ComputeColumnsForCustodyGroupHandler::::default().run(); } #[test] fn gossip_beacon_block() { - GossipValidationHandler::::new("gossip_beacon_block").run(); - GossipValidationHandler::::new("gossip_beacon_block").run(); + GossipValidationHandler::::new("gossip_beacon_block").run(); } #[test] fn gossip_proposer_slashing() { - GossipValidationHandler::::new("gossip_proposer_slashing").run(); - GossipValidationHandler::::new("gossip_proposer_slashing").run(); + GossipValidationHandler::::new("gossip_proposer_slashing").run(); } #[test] fn gossip_attester_slashing() { - GossipValidationHandler::::new("gossip_attester_slashing").run(); - GossipValidationHandler::::new("gossip_attester_slashing").run(); + GossipValidationHandler::::new("gossip_attester_slashing").run(); } #[test] fn gossip_voluntary_exit() { - GossipValidationHandler::::latest_stable("gossip_voluntary_exit").run(); - GossipValidationHandler::::latest_stable("gossip_voluntary_exit").run(); + GossipValidationHandler::::latest_stable("gossip_voluntary_exit").run(); } #[test] fn gossip_beacon_attestation() { - GossipValidationHandler::::latest_stable("gossip_beacon_attestation").run(); - GossipValidationHandler::::latest_stable("gossip_beacon_attestation").run(); + GossipValidationHandler::::latest_stable("gossip_beacon_attestation").run(); } #[test] fn gossip_beacon_aggregate_and_proof() { - GossipValidationHandler::::latest_stable("gossip_beacon_aggregate_and_proof") - .run(); - GossipValidationHandler::::latest_stable("gossip_beacon_aggregate_and_proof") - .run(); + GossipValidationHandler::::latest_stable("gossip_beacon_aggregate_and_proof").run(); } #[test] fn gossip_bls_to_execution_change() { - GossipValidationHandler::::latest_stable("gossip_bls_to_execution_change") - .run(); - GossipValidationHandler::::latest_stable("gossip_bls_to_execution_change") - .run(); + GossipValidationHandler::::latest_stable("gossip_bls_to_execution_change").run(); } #[test] fn gossip_sync_committee_message() { - GossipValidationHandler::::latest_stable("gossip_sync_committee_message").run(); - GossipValidationHandler::::latest_stable("gossip_sync_committee_message").run(); + GossipValidationHandler::::latest_stable("gossip_sync_committee_message").run(); } #[test] fn gossip_sync_committee_contribution_and_proof() { - GossipValidationHandler::::latest_stable( - "gossip_sync_committee_contribution_and_proof", - ) - .run(); - GossipValidationHandler::::latest_stable( - "gossip_sync_committee_contribution_and_proof", - ) - .run(); + GossipValidationHandler::::latest_stable("gossip_sync_committee_contribution_and_proof") + .run(); } From 3d82387b8f4839d1b77bd48f2b832ea171ed4763 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 7 Oct 2026 13:50:38 +1100 Subject: [PATCH 37/44] Bump PTC SSE queue (#10239) ## Issue Addressed Closes: - https://github.com/sigp/lighthouse/issues/10214 ## Proposed Changes Bump the queue cap to 512 so we can take a whole slot worth of PTC messages in a burst. This does not meaningfully increase memory usage. --- beacon_node/beacon_chain/src/events.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/events.rs b/beacon_node/beacon_chain/src/events.rs index b7b131e5602..9e82992476c 100644 --- a/beacon_node/beacon_chain/src/events.rs +++ b/beacon_node/beacon_chain/src/events.rs @@ -4,7 +4,8 @@ use tokio::sync::broadcast::{Receiver, Sender, error::SendError}; use tracing::trace; use types::EthSpec; -const DEFAULT_CHANNEL_CAPACITY: usize = 16; +// Allow a full slot of payload attestation messages (mainnet PTC_SIZE) to be buffered. +const DEFAULT_CHANNEL_CAPACITY: usize = 512; pub struct ServerSentEventHandler { attestation_tx: Sender>, From 20a74762ca5f2664ac4d0107bf94a06746b6d5e8 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 6 Oct 2026 22:30:57 -0700 Subject: [PATCH 38/44] Fix PTC duties at the Gloas fork boundary (#10193) Closes #9584 At the Gloas fork, the PTC duties endpoint returned 500 `IncorrectStateVariant` while the head state was still Fulu, so the VC missed PTC duties for slot 0 of the fork epoch. The BN now serves fork-epoch duties from the advanced head state at slot 0 and returns 400 before the fork. https://discordapp.com/channels/595666850260713488/1555299785437610066 --- beacon_node/beacon_chain/src/beacon_chain.rs | 14 +- beacon_node/http_api/src/ptc_duties.rs | 70 +++++++--- beacon_node/http_api/tests/fork_tests.rs | 125 +++++++++++++++++- .../validator_services/src/duties_service.rs | 2 +- 4 files changed, 188 insertions(+), 23 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index d9ae09db588..d20dfb8d6eb 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1768,8 +1768,18 @@ impl BeaconChain { let relative_epoch = RelativeEpoch::from_epoch(state.current_epoch(), epoch) .map_err(Error::IncorrectStateForAttestation)?; - let dependent_root = - state.attester_shuffling_decision_root(dependent_block_root, relative_epoch)?; + let dependent_root = if self.spec.gloas_fork_epoch == Some(epoch) { + let decision_slot = epoch + .start_slot(T::EthSpec::slots_per_epoch()) + .saturating_sub(1_u64); + if state.slot() == decision_slot { + dependent_block_root + } else { + *state.get_block_root(decision_slot)? + } + } else { + state.attester_shuffling_decision_root(dependent_block_root, relative_epoch)? + }; let pubkey_cache = self.validator_pubkey_cache.read(); diff --git a/beacon_node/http_api/src/ptc_duties.rs b/beacon_node/http_api/src/ptc_duties.rs index 6d27fa565a2..148ce5ab603 100644 --- a/beacon_node/http_api/src/ptc_duties.rs +++ b/beacon_node/http_api/src/ptc_duties.rs @@ -34,19 +34,32 @@ pub fn ptc_duties( .epoch(T::EthSpec::slots_per_epoch()) }; + if !chain.spec.fork_name_at_epoch(request_epoch).gloas_enabled() { + return Err(warp_utils::reject::custom_bad_request(format!( + "PTC duties are not available before the Gloas fork, request epoch {}", + request_epoch + ))); + } + + if chain.spec.gloas_fork_epoch == Some(request_epoch) && tolerant_current_epoch < request_epoch + { + return Err(warp_utils::reject::custom_bad_request(format!( + "PTC duties for the Gloas fork epoch {} are not available until the fork", + request_epoch + ))); + } + let is_within_clock_tolerance = request_epoch == current_epoch || request_epoch == current_epoch + 1 || request_epoch == tolerant_current_epoch + 1; if is_within_clock_tolerance { - let head_epoch = chain - .canonical_head - .cached_head() - .snapshot - .beacon_state - .current_epoch(); + let cached_head = chain.canonical_head.cached_head(); + let head_state = &cached_head.snapshot.beacon_state; + let head_epoch = head_state.current_epoch(); - let head_can_serve_request = request_epoch == head_epoch || request_epoch == head_epoch + 1; + let head_can_serve_request = head_state.fork_name_unchecked().gloas_enabled() + && (request_epoch == head_epoch || request_epoch == head_epoch + 1); if head_can_serve_request { compute_ptc_duties_from_cached_head(request_epoch, request_indices, chain) @@ -100,8 +113,9 @@ fn compute_ptc_duties_from_state( if head.beacon_state.current_epoch() <= request_epoch { Some(( + head.beacon_block_root, head.beacon_state_root(), - head.beacon_state.clone(), + head.beacon_state.slot(), execution_status.is_optimistic_or_invalid(), )) } else { @@ -109,8 +123,19 @@ fn compute_ptc_duties_from_state( } }; - let (state, execution_optimistic) = - if let Some((state_root, mut state, execution_optimistic)) = state_opt { + let (state, dependent_block_root, execution_optimistic) = + if let Some((block_root, state_root, head_slot, execution_optimistic)) = state_opt { + let max_slot = std::cmp::max( + head_slot, + request_epoch.start_slot(T::EthSpec::slots_per_epoch()), + ); + let (state_root, mut state) = chain + .store + .get_advanced_hot_state(block_root, max_slot, state_root) + .map_err(BeaconChainError::DBError) + .map_err(warp_utils::reject::unhandled_error)? + .ok_or(BeaconChainError::MissingBeaconState(state_root)) + .map_err(warp_utils::reject::unhandled_error)?; ensure_state_knows_ptc_duties_for_epoch( &mut state, state_root, @@ -118,12 +143,12 @@ fn compute_ptc_duties_from_state( chain.builder_onboarding_cache.as_deref(), &chain.spec, )?; - (state, execution_optimistic) + (state, block_root, execution_optimistic) } else { let (state, execution_optimistic, _finalized) = StateId::from_slot(request_epoch.start_slot(T::EthSpec::slots_per_epoch())) .state(chain)?; - (state, execution_optimistic) + (state, chain.genesis_block_root, execution_optimistic) }; if !(state.current_epoch() == request_epoch || state.current_epoch() + 1 == request_epoch) { @@ -135,12 +160,7 @@ fn compute_ptc_duties_from_state( } let (duties, dependent_root) = chain - .compute_ptc_duties( - &state, - request_epoch, - request_indices, - chain.genesis_block_root, - ) + .compute_ptc_duties(&state, request_epoch, request_indices, dependent_block_root) .map_err(warp_utils::reject::unhandled_error)?; convert_to_api_response(duties, dependent_root, execution_optimistic) @@ -159,6 +179,20 @@ fn ensure_state_knows_ptc_duties_for_epoch( state.current_epoch(), target_epoch ))); + } else if !state.fork_name_unchecked().gloas_enabled() { + let fork_epoch = spec.gloas_fork_epoch.unwrap_or(target_epoch); + let target_slot = std::cmp::max(fork_epoch, target_epoch.saturating_sub(1_u64)) + .start_slot(E::slots_per_epoch()); + + partial_state_advance( + state, + Some(state_root), + target_slot, + builder_onboarding_cache, + spec, + ) + .map_err(BeaconChainError::from) + .map_err(warp_utils::reject::unhandled_error)?; } else if state.current_epoch() + 1 < target_epoch { let target_slot = target_epoch .saturating_sub(1_u64) diff --git a/beacon_node/http_api/tests/fork_tests.rs b/beacon_node/http_api/tests/fork_tests.rs index 0ff8ebc452d..d62a6d96664 100644 --- a/beacon_node/http_api/tests/fork_tests.rs +++ b/beacon_node/http_api/tests/fork_tests.rs @@ -5,14 +5,15 @@ use beacon_chain::{ test_utils::{DEFAULT_ETH1_BLOCK_HASH, HARNESS_GENESIS_TIME, RelativeSyncCommittee}, }; use bls::PublicKey; -use eth2::types::{IndexedErrorMessage, StateId, SyncSubcommittee}; +use eth2::types::{IndexedErrorMessage, PtcDuty, StateId, SyncSubcommittee}; use execution_layer::test_utils::generate_genesis_header; use fixed_bytes::FixedBytesExtended; use genesis::{InteropGenesisBuilder, bls_withdrawal_credentials}; use http_api::test_utils::*; +use state_processing::state_advance::complete_state_advance; use std::collections::HashSet; use types::{ - Address, ChainSpec, Epoch, EthSpec, Hash256, MinimalEthSpec, Slot, + Address, BeaconState, ChainSpec, Epoch, EthSpec, ForkName, Hash256, MinimalEthSpec, Slot, test_utils::{generate_deterministic_keypair, generate_deterministic_keypairs}, }; @@ -32,6 +33,12 @@ fn capella_spec(capella_fork_epoch: Epoch) -> ChainSpec { spec } +fn gloas_spec(gloas_fork_epoch: Epoch) -> ChainSpec { + let mut spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); + spec.gloas_fork_epoch = Some(gloas_fork_epoch); + spec +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sync_committee_duties_across_fork() { let validator_count = E::sync_committee_size(); @@ -113,6 +120,120 @@ async fn sync_committee_duties_across_fork() { ); } +fn ptc_duty_slots(duties: &[PtcDuty]) -> Vec<(u64, Slot)> { + let mut slots = duties + .iter() + .map(|duty| (duty.validator_index, duty.slot)) + .collect::>(); + slots.sort(); + slots +} + +fn expected_ptc_duty_slots( + mut state: BeaconState, + epoch: Epoch, + validators: &[u64], + spec: &ChainSpec, +) -> Vec<(u64, Slot)> { + complete_state_advance( + &mut state, + None, + epoch.start_slot(E::slots_per_epoch()), + None, + spec, + ) + .unwrap(); + let mut slots = validators + .iter() + .filter_map(|&validator_index| { + state + .get_ptc_assignment(validator_index as usize, epoch, spec) + .unwrap() + .map(|slot| (validator_index, slot)) + }) + .collect::>(); + slots.sort(); + slots +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn ptc_duties_across_fork() { + let validator_count = E::sync_committee_size(); + let fork_epoch = Epoch::new(2); + let spec = gloas_spec(fork_epoch); + let tester = InteractiveTester::::new(Some(spec.clone()), validator_count).await; + let harness = &tester.harness; + let client = &tester.client; + + let all_validators = harness.get_all_validators(); + let all_validators_u64 = all_validators.iter().map(|x| *x as u64).collect::>(); + + assert_eq!(harness.get_current_slot(), 0); + + for epoch in [Epoch::new(0), fork_epoch - 1] { + assert_eq!( + client + .post_validator_duties_ptc(epoch, &all_validators_u64) + .await + .unwrap_err() + .status() + .unwrap(), + 400 + ); + } + + let fork_slot = fork_epoch.start_slot(E::slots_per_epoch()); + let genesis_state = harness.get_current_state(); + let (pre_fork_block_root, pre_fork_state) = harness + .add_attested_block_at_slot(fork_slot - 1, genesis_state, &all_validators) + .await + .unwrap(); + let pre_fork_block_root = Hash256::from(pre_fork_block_root); + assert_eq!(harness.get_current_slot(), fork_slot - 1); + + let expected_duties = expected_ptc_duty_slots( + pre_fork_state.clone(), + fork_epoch, + &all_validators_u64, + &spec, + ); + assert!(!expected_duties.is_empty()); + + for epoch in [fork_epoch - 1, fork_epoch] { + assert_eq!( + client + .post_validator_duties_ptc(epoch, &all_validators_u64) + .await + .unwrap_err() + .status() + .unwrap(), + 400 + ); + } + + harness.advance_slot(); + assert_eq!(harness.get_current_slot(), fork_slot); + + let fork_slot_response = client + .post_validator_duties_ptc(fork_epoch, &all_validators_u64) + .await + .unwrap(); + assert_eq!(fork_slot_response.dependent_root, pre_fork_block_root); + assert_eq!(ptc_duty_slots(&fork_slot_response.data), expected_duties); + + harness + .add_attested_block_at_slot(fork_slot, pre_fork_state, &all_validators) + .await + .unwrap(); + + let post_fork_response = client + .post_validator_duties_ptc(fork_epoch, &all_validators_u64) + .await + .unwrap(); + assert_eq!(post_fork_response.dependent_root, pre_fork_block_root); + assert_eq!(ptc_duty_slots(&post_fork_response.data), expected_duties); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn attestations_across_fork_with_skip_slots() { let validator_count = E::sync_committee_size(); diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index fd15e0c7241..6439e543488 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -808,7 +808,7 @@ pub fn start_update_service break; }; - if current_epoch + 1 < gloas_fork_epoch { + if current_epoch < gloas_fork_epoch { // Wait until the next slot and check again if let Some(duration) = duties_service.slot_clock.duration_to_next_slot() { sleep(duration).await; From 4939a94e72d3657ea409b102051b873ab3465212 Mon Sep 17 00:00:00 2001 From: Cristian Conache Date: Wed, 7 Oct 2026 15:50:03 +0300 Subject: [PATCH 39/44] Fix blob ordering when filtering blobs by versioned hash (#9944) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Issue Addressed Closes #9941. ## Proposed Changes - `GET /eth/v1/beacon/blobs/{block_id}` now returns blobs in the order of the KZG commitments in the block, regardless of the order of the requested `versioned_hashes` ([spec](https://github.com/ethereum/beacon-APIs/blob/master/apis/beacon/blobs/blobs.yaml#L8-L9)). This is done by sorting the derived blob indices before fetching the blobs - Add a test that requests the versioned hashes in reverse order and asserts that the returned blobs follow the commitment order ## Additional Info - The second point of the issue (response `data` being a list of `{"blob": …}` objects instead of hex strings) does not apply: `BlobWrapper` is `#[serde(transparent)]`, so the `data` field in the response is already a flat array of hex strings. I validated this locally with a throwaway request test against the `http_api` test server (not included in this PR) --- beacon_node/http_api/src/block_id.rs | 70 +++++++++++++++++++++++++++- beacon_node/http_api/tests/tests.rs | 51 ++++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/beacon_node/http_api/src/block_id.rs b/beacon_node/http_api/src/block_id.rs index f8399689f8b..27f9d25b34b 100644 --- a/beacon_node/http_api/src/block_id.rs +++ b/beacon_node/http_api/src/block_id.rs @@ -413,7 +413,7 @@ impl BlockId { })?; let blob_indices_opt = query.versioned_hashes.map(|versioned_hashes| { - versioned_hashes + let mut blob_indices = versioned_hashes .iter() .flat_map(|versioned_hash| { blob_kzg_commitments.iter().position(|commitment| { @@ -422,7 +422,11 @@ impl BlockId { }) }) .map(|index| index as u64) - .collect::>() + .collect::>(); + // Blobs must be returned in the order of their KZG commitments in the block, + // regardless of the order of the requested versioned hashes + blob_indices.sort_unstable(); + blob_indices }); let max_blobs_per_block = chain.spec.max_blobs_per_block(block.epoch()) as usize; @@ -648,6 +652,68 @@ mod tests { ); } + #[tokio::test] + async fn get_blobs_follow_commitment_order_post_gloas() { + let harness = gloas_supernode_harness(); + harness.execution_block_generator().set_min_blob_count(2); + harness.advance_slot(); + + let slot = harness.get_current_slot(); + let (_, (block, _), _) = harness + .add_block_at_slot(slot, harness.get_current_state()) + .await + .expect("Gloas block should import"); + + assert_eq!(block.fork_name_unchecked(), ForkName::Gloas); + assert!(block.num_expected_blobs() >= 2); + + // The unfiltered response is in commitment order + let all_blobs_response = BlockId(CoreBlockId::Head) + .get_blobs_by_versioned_hashes( + BlobsVersionedHashesQuery { + versioned_hashes: None, + }, + &harness.chain, + ) + .expect("Gloas head blobs should be retrievable"); + assert_eq!(all_blobs_response.data.len(), block.num_expected_blobs()); + + // Blobs must be returned in the order of their KZG commitments in the block, + // regardless of the order of the requested versioned hashes + let reversed_versioned_hashes = block + .message() + .blob_kzg_commitments() + .expect("Gloas block should have blob KZG commitments") + .iter() + .rev() + .map(|commitment| commitment.calculate_versioned_hash()) + .collect::>(); + let reversed_request_response = BlockId(CoreBlockId::Head) + .get_blobs_by_versioned_hashes( + BlobsVersionedHashesQuery { + versioned_hashes: Some(reversed_versioned_hashes), + }, + &harness.chain, + ) + .expect("Gloas head blobs should be retrievable with reversed versioned hashes"); + + assert_eq!( + reversed_request_response.data.len(), + all_blobs_response.data.len() + ); + for (index, (returned, expected)) in reversed_request_response + .data + .iter() + .zip(&all_blobs_response.data) + .enumerate() + { + assert_eq!( + returned.blob, expected.blob, + "blob at position {index} does not follow commitment order" + ); + } + } + #[tokio::test] async fn root_uses_early_attester_cache_for_unpersisted_block() { let Some(fork_name) = fork_name_from_env().filter(|fork_name| fork_name.fulu_enabled()) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 26e9a0e0df2..f224a52fad2 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -2333,6 +2333,53 @@ impl ApiTester { self } + pub async fn test_get_blobs_follow_commitment_order(self) -> Self { + let block_id = BlockId(CoreBlockId::Head); + let (block_root, _, _) = block_id.root(&self.chain).unwrap(); + let (block, _, _) = block_id.full_block(&self.chain).await.unwrap(); + + let versioned_hashes: Vec = block + .message() + .blob_kzg_commitments() + .unwrap() + .iter() + .map(|commitment| commitment.calculate_versioned_hash()) + .collect(); + let num_blobs = versioned_hashes.len(); + assert!(num_blobs >= 2, "test block must contain at least two blobs"); + + // The unfiltered response is in commitment order + let all_blobs = self + .client + .get_blobs::(CoreBlockId::Root(block_root), None) + .await + .unwrap() + .unwrap() + .into_data(); + assert_eq!(all_blobs.len(), num_blobs); + + // Every hash in reverse order + let request: Vec = versioned_hashes.iter().rev().copied().collect(); + + let result = self + .client + .get_blobs::(CoreBlockId::Root(block_root), Some(&request)) + .await + .unwrap() + .unwrap() + .into_data(); + + assert_eq!(result.len(), num_blobs); + for (index, (returned, expected)) in result.iter().zip(&all_blobs).enumerate() { + assert_eq!( + returned.blob, expected.blob, + "blob at position {index} does not follow commitment order" + ); + } + + self + } + pub async fn test_get_blobs_post_fulu_full_node(self, versioned_hashes: bool) -> Self { let block_id = BlockId(CoreBlockId::Head); let (block_root, _, _) = block_id.root(&self.chain).unwrap(); @@ -11003,6 +11050,8 @@ async fn get_blob_sidecars() { .test_get_blobs(false) .await .test_get_blobs(true) + .await + .test_get_blobs_follow_commitment_order() .await; } @@ -11029,6 +11078,8 @@ async fn get_blobs_post_fulu_supernode() { .test_get_blobs(false) .await .test_get_blobs(true) + .await + .test_get_blobs_follow_commitment_order() .await; } From 322d34d9e4a56ae88b43806a4b63c23742db128e Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 7 Oct 2026 22:51:46 +0800 Subject: [PATCH 40/44] Align the EIP-8025 proof types and gossip rules with the spec (#10236) ## Issue Addressed This PR represents the final state of EIP-8025 spec https://github.com/ethereum/consensus-specs/pull/5639 ## Proposed Changes Update types, verifier API and gossip rules --- .../gossip_verified_execution_proof.rs | 95 +++++++++++++++++-- .../src/execution_proof_verification/mod.rs | 4 + .../observed_execution_proofs.rs | 2 +- .../lighthouse_network/src/types/pubsub.rs | 6 +- .../gossip_methods.rs | 7 +- .../src/network_beacon_processor/mod.rs | 2 +- beacon_node/proof_engine/src/lib.rs | 10 +- .../types/src/execution/execution_proof.rs | 42 ++++---- consensus/types/src/execution/mod.rs | 6 +- consensus/types/src/execution/payload.rs | 22 ++++- 10 files changed, 154 insertions(+), 42 deletions(-) diff --git a/beacon_node/beacon_chain/src/execution_proof_verification/gossip_verified_execution_proof.rs b/beacon_node/beacon_chain/src/execution_proof_verification/gossip_verified_execution_proof.rs index 6348f264f16..158ec24f6a9 100644 --- a/beacon_node/beacon_chain/src/execution_proof_verification/gossip_verified_execution_proof.rs +++ b/beacon_node/beacon_chain/src/execution_proof_verification/gossip_verified_execution_proof.rs @@ -1,21 +1,27 @@ use super::Error; use crate::beacon_chain::BeaconStore; use crate::canonical_head::CanonicalHead; +use crate::data_column_verification::load_gloas_payload_bid; use crate::execution_proof_verification::observed_execution_proofs::{ ObservedExecutionProofs, ProofObservation, }; use crate::shuffling_cache::{ShufflingCache, with_cached_shuffling}; use crate::validator_pubkey_cache::ValidatorPubkeyCache; -use crate::{BeaconChain, BeaconChainTypes}; +use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; use parking_lot::RwLock; use proof_engine::{ProofEngine, ProofVerificationOutcome}; use state_processing::builder_deposits_cache::OnboardBuildersCache; +use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; use std::sync::Arc; use tree_hash::TreeHash; -use types::execution::SignedExecutionProof; +use types::execution::{ + ExecutionProof, ExecutionProofEnvelope, NewPayloadRequest, PublicInput, + STATELESS_INPUT_SCHEMA_ID, SignedExecutionProofEnvelope, VersionedHashes, +}; use types::{ChainSpec, Domain, EthSpec, Hash256, SignedRoot, Slot}; pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { + pub chain: &'a Arc>, pub canonical_head: &'a CanonicalHead, pub observed_execution_proofs: &'a RwLock, pub validator_pubkey_cache: &'a RwLock>, @@ -27,15 +33,15 @@ pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { pub genesis_validators_root: Hash256, } -/// A `SignedExecutionProof` that has been verified for propagation on the gossip network. +/// A `SignedExecutionProofEnvelope` that has been verified for propagation on the gossip network. pub struct GossipVerifiedExecutionProof { - pub proof: Arc, + pub proof: Arc, pub block_slot: Slot, } impl GossipVerifiedExecutionProof { pub async fn new( - proof: Arc, + proof: Arc, ctx: &GossipVerificationContext<'_, T>, ) -> Result { // [REJECT] `proof.proof_data` is non-empty. The `MAX_PROOF_SIZE` upper bound is enforced @@ -44,6 +50,8 @@ impl GossipVerifiedExecutionProof { return Err(Error::EmptyProofData); } + // [DEVIATION] The sidecar decides which proof types are supported. + let proof_root = proof.message.tree_hash_root(); let block_root = proof.beacon_block_root(); let proof_type = proof.proof_type(); @@ -124,6 +132,19 @@ impl GossipVerifiedExecutionProof { } } + // [IGNORE] The payload this proof is about is available. Reads the store and merkleizes a + // payload, so it runs on the blocking pool. + let chain = ctx.chain.clone(); + let signed_proof = proof.clone(); + let execution_proof = ctx + .chain + .spawn_blocking_handle( + move || get_execution_proof(&chain, &signed_proof.message, block_root), + "get_execution_proof", + ) + .await + .map_err(|e| Error::BeaconChainError(Box::new(e)))??; + // Only record the validator's attempt after the signature binds `validator_index`; // recording earlier would let unauthenticated messages suppress honest provers. if !ctx @@ -149,7 +170,7 @@ impl GossipVerifiedExecutionProof { // significantly. let proof_engine = ctx.proof_engine.as_ref().ok_or(Error::ProofEngineMissing)?; match proof_engine - .verify_execution_proof(&proof.message) + .verify_execution_proof(&execution_proof) .await .map_err(Error::ProofEngine)? { @@ -165,8 +186,11 @@ impl GossipVerifiedExecutionProof { } impl BeaconChain { - pub fn execution_proof_gossip_verification_context(&self) -> GossipVerificationContext<'_, T> { + pub fn execution_proof_gossip_verification_context( + self: &Arc, + ) -> GossipVerificationContext<'_, T> { GossipVerificationContext { + chain: self, canonical_head: &self.canonical_head, observed_execution_proofs: &self.observed_execution_proofs, validator_pubkey_cache: &self.validator_pubkey_cache, @@ -180,8 +204,8 @@ impl BeaconChain { } pub async fn verify_execution_proof_for_gossip( - &self, - proof: Arc, + self: &Arc, + proof: Arc, ) -> Result { GossipVerifiedExecutionProof::new( proof, @@ -190,3 +214,56 @@ impl BeaconChain { .await } } + +/// Spec helper `get_execution_proof`. +/// +/// TODO(EIP8025): add cache of result of the hash. The root is the same for every proof of a +/// block, so it is recomputed on each gossip hop. +fn get_execution_proof( + chain: &BeaconChain, + proof_envelope: &ExecutionProofEnvelope, + block_root: Hash256, +) -> Result { + let payload_envelope = chain + .store + .get_signed_payload_envelope(&block_root) + .map_err(|e| Error::BeaconChainError(Box::new(e.into())))? + .ok_or(Error::PayloadUnavailable { + beacon_block_root: block_root, + })? + .message; + + // Commitments live on the bid, not the envelope. + let bid = load_gloas_payload_bid(block_root, chain) + .map_err(|e| Error::BeaconChainError(Box::new(e)))? + .ok_or(Error::UnknownBlockRoot { + beacon_block_root: block_root, + })?; + + let versioned_hashes = VersionedHashes::::new( + bid.message + .blob_kzg_commitments + .iter() + .map(kzg_commitment_to_versioned_hash) + .collect(), + ) + .map_err(|e| Error::BeaconChainError(Box::new(BeaconChainError::SszTypesError(e))))?; + + let new_payload_request = NewPayloadRequest:: { + execution_payload: payload_envelope.payload, + versioned_hashes, + parent_beacon_block_root: payload_envelope.parent_beacon_block_root, + execution_requests: payload_envelope.execution_requests, + }; + + Ok(ExecutionProof { + proof_data: proof_envelope.proof_data.clone(), + proof_type: proof_envelope.proof_type, + public_input: PublicInput { + new_payload_request_root: new_payload_request.tree_hash_root(), + successful_validation: true, + chain_id: chain.spec.deposit_chain_id, + schema_id: STATELESS_INPUT_SCHEMA_ID, + }, + }) +} diff --git a/beacon_node/beacon_chain/src/execution_proof_verification/mod.rs b/beacon_node/beacon_chain/src/execution_proof_verification/mod.rs index ce206a43d93..06a3e9bbbdd 100644 --- a/beacon_node/beacon_chain/src/execution_proof_verification/mod.rs +++ b/beacon_node/beacon_chain/src/execution_proof_verification/mod.rs @@ -33,6 +33,10 @@ pub enum Error { slot: Slot, finalized_slot: Slot, }, + /// The payload this proof is about has not been seen (IGNORE). + PayloadUnavailable { + beacon_block_root: Hash256, + }, /// `proof_data` is empty (REJECT). EmptyProofData, /// The validator index does not exist (REJECT). diff --git a/beacon_node/beacon_chain/src/execution_proof_verification/observed_execution_proofs.rs b/beacon_node/beacon_chain/src/execution_proof_verification/observed_execution_proofs.rs index 314b6b76a1d..2fc4614ae7d 100644 --- a/beacon_node/beacon_chain/src/execution_proof_verification/observed_execution_proofs.rs +++ b/beacon_node/beacon_chain/src/execution_proof_verification/observed_execution_proofs.rs @@ -1,4 +1,4 @@ -//! Provides the `ObservedExecutionProofs` struct which allows for ignoring `SignedExecutionProof`s +//! Provides the `ObservedExecutionProofs` struct which allows for ignoring `SignedExecutionProofEnvelope`s //! that we have already seen over the gossip network. //! Only proofs that have completed signature verification can be added to this cache to reduce //! DoS risks. diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index 6a716526d87..462e3ccf3cf 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -18,7 +18,7 @@ use types::{ SignedBeaconBlock, SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedInclusionList, SignedProposerPreferences, SignedVoluntaryExit, SingleAttestation, SubnetId, - SyncCommitteeMessage, SyncSubnetId, execution::SignedExecutionProof, + SyncCommitteeMessage, SyncSubnetId, execution::SignedExecutionProofEnvelope, }; #[derive(Debug, Clone, PartialEq)] @@ -52,7 +52,7 @@ pub enum PubsubMessage { /// Gossipsub message providing notification of signed proposer preferences. ProposerPreferences(Arc), /// Gossipsub message providing notification of an EIP-8025 execution proof. - ExecutionProof(Arc), + ExecutionProof(Arc), /// Gossipsub message providing notification of a signed inclusion list. InclusionList(Box), /// Gossipsub message providing notification of a light client finality update. @@ -416,7 +416,7 @@ impl PubsubMessage { ))) } GossipKind::ExecutionProof => { - let execution_proof = SignedExecutionProof::from_ssz_bytes(data) + let execution_proof = SignedExecutionProofEnvelope::from_ssz_bytes(data) .map_err(|e| format!("{:?}", e))?; Ok(PubsubMessage::ExecutionProof(Arc::new(execution_proof))) } 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 b52c9919fa0..61d12f2c6fa 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -56,7 +56,7 @@ use types::{ SignedContributionAndProof, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedInclusionList, SignedProposerPreferences, SignedVoluntaryExit, SingleAttestation, Slot, SubnetId, SyncCommitteeMessage, SyncSubnetId, block::BlockImportSource, data::CellBitmap, - execution::SignedExecutionProof, + execution::SignedExecutionProofEnvelope, }; use beacon_processor::work_reprocessing_queue::QueuedColumnReconstruction; @@ -4153,7 +4153,7 @@ impl NetworkBeaconProcessor { self: Arc, message_id: MessageId, peer_id: PeerId, - execution_proof: Arc, + execution_proof: Arc, ) { let beacon_block_root = execution_proof.beacon_block_root(); let proof_type = execution_proof.proof_type(); @@ -4180,7 +4180,8 @@ impl NetworkBeaconProcessor { | ExecutionProofError::ValidProofAlreadyKnown | ExecutionProofError::DuplicateFromValidator { .. } | ExecutionProofError::UnknownBlockRoot { .. } - | ExecutionProofError::PastFinalizedSlot { .. } => { + | ExecutionProofError::PastFinalizedSlot { .. } + | ExecutionProofError::PayloadUnavailable { .. } => { (MessageAcceptance::Ignore, None) } // REJECT: the proof is invalid. diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 0fcbb1a5972..f5b75a2fb06 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -468,7 +468,7 @@ impl NetworkBeaconProcessor { self: &Arc, message_id: MessageId, peer_id: PeerId, - execution_proof: Arc, + execution_proof: Arc, ) -> Result<(), Error> { let processor = self.clone(); let process_fn = async move { diff --git a/beacon_node/proof_engine/src/lib.rs b/beacon_node/proof_engine/src/lib.rs index 91ceeae1f7e..07f0d6dc5d0 100644 --- a/beacon_node/proof_engine/src/lib.rs +++ b/beacon_node/proof_engine/src/lib.rs @@ -66,16 +66,20 @@ impl ProofEngine { let response: VerifyResponse = self .client .post(url) + // [DEVIATION] The spec public input is `hash_tree_root(public_input)`; the sidecar + // rebuilds it from the fields. .query(&[ ( "new_payload_request_root", format!("{:?}", proof.public_input.new_payload_request_root), ), - ("proof_type", proof.proof_type.to_string()), ( - "beacon_block_root", - format!("{:?}", proof.beacon_block_root), + "successful_validation", + proof.public_input.successful_validation.to_string(), ), + ("chain_id", proof.public_input.chain_id.to_string()), + ("schema_id", proof.public_input.schema_id.to_string()), + ("proof_type", proof.proof_type.to_string()), ]) .header("content-type", "application/octet-stream") .body(proof.proof_data.to_vec()) diff --git a/consensus/types/src/execution/execution_proof.rs b/consensus/types/src/execution/execution_proof.rs index 2cc34160f32..c951eb67366 100644 --- a/consensus/types/src/execution/execution_proof.rs +++ b/consensus/types/src/execution/execution_proof.rs @@ -12,28 +12,29 @@ pub const MAX_PROOF_SIZE: usize = 4_194_304; /// SSZ bound for `proof_data`. pub type MaxProofSize = typenum::U4194304; -/// Opaque proof bytes. -/// -/// The EIP-8025 spec defines this as `ProgressiveByteList` (EIP-7916), which is not yet -/// supported by `ssz_types`. A `VariableList` serializes identically but merkleizes -/// differently, so signing roots are not interoperable with spec-conformant clients. +/// Spec type `ProofData`. pub type ProofData = VariableList; -/// Identifier for the proof system that produced a proof (EIP-8025 `ProofType`). +/// Spec type `ProofType`. pub type ProofType = u8; +/// Spec constant `STATELESS_INPUT_SCHEMA_ID`: Amsterdam fork (`0x15`), schema revision (`0x01`). +pub const STATELESS_INPUT_SCHEMA_ID: u16 = 0x1501; + +/// Spec type `PublicInput`. #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Encode, Decode, TreeHash)] #[context_deserialize(ForkName)] +#[tree_hash(struct_behaviour = "progressive_container", active_fields(1, 1, 1, 1))] pub struct PublicInput { pub new_payload_request_root: Hash256, + pub successful_validation: bool, + #[serde(with = "serde_utils::quoted_u64")] + pub chain_id: u64, + pub schema_id: u16, } -/// An execution proof attesting to the validity of an execution payload (EIP-8025). -/// -/// Deviation from the spec: `beacon_block_root` binds the proof to the beacon block whose -/// envelope committed the payload, allowing the proof to be resolved without an index from -/// `new_payload_request_root` to block root. +/// Spec type `ExecutionProof`. #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Encode, Decode, TreeHash)] #[context_deserialize(ForkName)] @@ -41,22 +42,31 @@ pub struct ExecutionProof { pub proof_data: ProofData, pub proof_type: ProofType, pub public_input: PublicInput, +} + +/// Spec type `ExecutionProofEnvelope`. +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Encode, Decode, TreeHash)] +#[context_deserialize(ForkName)] +pub struct ExecutionProofEnvelope { + pub proof_data: ProofData, + pub proof_type: ProofType, pub beacon_block_root: Hash256, } -impl SignedRoot for ExecutionProof {} +impl SignedRoot for ExecutionProofEnvelope {} #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Encode, Decode, TreeHash)] #[context_deserialize(ForkName)] -pub struct SignedExecutionProof { - pub message: ExecutionProof, +pub struct SignedExecutionProofEnvelope { + pub message: ExecutionProofEnvelope, #[serde(with = "serde_utils::quoted_u64")] pub validator_index: u64, pub signature: Signature, } -impl SignedExecutionProof { +impl SignedExecutionProofEnvelope { pub fn beacon_block_root(&self) -> Hash256 { self.message.beacon_block_root } @@ -70,5 +80,5 @@ impl SignedExecutionProof { mod tests { use super::*; - ssz_and_tree_hash_tests!(SignedExecutionProof); + ssz_and_tree_hash_tests!(SignedExecutionProofEnvelope); } diff --git a/consensus/types/src/execution/mod.rs b/consensus/types/src/execution/mod.rs index cae31eda45a..a41c68ee23c 100644 --- a/consensus/types/src/execution/mod.rs +++ b/consensus/types/src/execution/mod.rs @@ -34,8 +34,8 @@ pub use execution_payload_header::{ ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, }; pub use execution_proof::{ - ExecutionProof, MAX_PROOF_SIZE, MaxProofSize, ProofData, ProofType, PublicInput, - SignedExecutionProof, + ExecutionProof, ExecutionProofEnvelope, MAX_PROOF_SIZE, MaxProofSize, ProofData, ProofType, + PublicInput, STATELESS_INPUT_SCHEMA_ID, SignedExecutionProofEnvelope, }; pub use execution_requests::{ BuilderDepositRequests, BuilderExitRequests, ConsolidationRequests, DepositRequests, @@ -48,7 +48,7 @@ pub use payload::{ BlindedPayloadDeneb, BlindedPayloadElectra, BlindedPayloadFulu, BlindedPayloadRef, BlockProductionVersion, BlockType, ExecPayload, FullPayload, FullPayloadBellatrix, FullPayloadCapella, FullPayloadDeneb, FullPayloadElectra, FullPayloadFulu, FullPayloadRef, - OwnedExecPayload, + NewPayloadRequest, OwnedExecPayload, VersionedHashes, }; pub use signed_bls_to_execution_change::SignedBlsToExecutionChange; pub use signed_execution_payload_bid::SignedExecutionPayloadBid; diff --git a/consensus/types/src/execution/payload.rs b/consensus/types/src/execution/payload.rs index 86376c9b9ec..45706f8a9b6 100644 --- a/consensus/types/src/execution/payload.rs +++ b/consensus/types/src/execution/payload.rs @@ -1,3 +1,4 @@ +use context_deserialize::context_deserialize; use educe::Educe; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -14,9 +15,9 @@ use crate::{ execution::{ ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadFulu, - ExecutionPayloadHeader, ExecutionPayloadHeaderBellatrix, ExecutionPayloadHeaderCapella, - ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderFulu, - ExecutionPayloadRef, Transactions, + ExecutionPayloadGloas, ExecutionPayloadHeader, ExecutionPayloadHeaderBellatrix, + ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, + ExecutionPayloadHeaderFulu, ExecutionPayloadRef, ExecutionRequestsGloas, Transactions, }, fork::ForkName, map_execution_payload_into_blinded_payload, map_execution_payload_into_full_payload, @@ -1110,3 +1111,18 @@ pub enum BlockProductionVersion { BlindedV2, FullV2, } + +/// Spec type `VersionedHashes`. +pub type VersionedHashes = VariableList::MaxBlobCommitmentsPerBlock>; + +/// Spec type `NewPayloadRequest`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TreeHash)] +#[serde(bound = "E: EthSpec")] +#[context_deserialize(ForkName)] +#[tree_hash(struct_behaviour = "progressive_container", active_fields(1, 1, 1, 1))] +pub struct NewPayloadRequest { + pub execution_payload: ExecutionPayloadGloas, + pub versioned_hashes: VersionedHashes, + pub parent_beacon_block_root: Hash256, + pub execution_requests: ExecutionRequestsGloas, +} From 95206a0248980d4530f290fa1aac105481c59c2c Mon Sep 17 00:00:00 2001 From: Cristian Conache Date: Wed, 7 Oct 2026 19:24:57 +0300 Subject: [PATCH 41/44] Add inclusion_list_bits to ExecutionPayloadBid (#9729) This PR is a follow-up on [the base FOCIL types & constants PR](https://github.com/sigp/lighthouse/pull/9708). ## Proposed Changes - Add `inclusion_list_bits` to the Heze `ExecutionPayloadBid` - Convert `ExecutionPayloadBid` and `SignedExecutionPayloadBid` to `superstruct(Gloas, Heze)`. Most of the changes in the PR are cascading from this decision, because fields are migrated to getters as part of this change. - Later edit: post [#9714](https://github.com/sigp/lighthouse/pull/9714) merge, local block production also builds Heze bids ## Additional Info - `inclusion_list_bits` defaults to empty everywhere, so we're not computing or reading it yet. The main goal is to land this early so that we freeze the Heze bid and state layout before downstream FOCIL work builds on it. - The Gloas variant is byte-identical to the old struct's version, so the `ssz_static` vectors for the bid and `BeaconState` pass unchanged. - This PR was tested locally by running the following commands: - `make cargo-fmt` - `make lint` - `make test` - `make test-ef` Co-authored-by: Eitan Seri- Levi --- .../beacon_chain/src/beacon_block_reward.rs | 10 +- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- .../src/block_production/bid_selection.rs | 40 +-- .../src/block_production/gloas.rs | 263 ++++++++++++------ .../beacon_chain/src/block_verification.rs | 4 +- .../src/block_verification_types.rs | 4 +- .../beacon_chain/src/canonical_head.rs | 2 +- .../beacon_chain/src/custody_context.rs | 4 +- .../src/data_availability_checker.rs | 2 +- .../src/data_column_verification.rs | 32 +-- .../gossip_verified_execution_proof.rs | 4 +- .../beacon_chain/src/fetch_blobs/mod.rs | 14 +- .../beacon_chain/src/fetch_blobs/tests.rs | 2 +- .../beacon_chain/src/historical_blocks.rs | 6 +- beacon_node/beacon_chain/src/kzg_utils.rs | 2 +- .../src/observed_execution_payloads.rs | 6 +- .../direct_verified_bid.rs | 150 +++++++--- .../gossip_verified_bid.rs | 115 ++++---- .../src/payload_bid_verification/mod.rs | 4 +- .../payload_bid_cache.rs | 70 +++-- .../src/payload_bid_verification/tests.rs | 257 +++++++++++------ .../gossip_verified_envelope.rs | 79 ++++-- .../payload_envelope_verification/import.rs | 2 +- .../src/payload_envelope_verification/mod.rs | 12 +- .../payload_notifier.rs | 6 +- .../src/pending_payload_cache/mod.rs | 16 +- .../pending_components.rs | 18 +- beacon_node/beacon_chain/src/test_utils.rs | 98 ++++--- .../beacon_chain/tests/block_verification.rs | 1 - .../beacon_chain/tests/column_verification.rs | 2 +- .../tests/envelope_verification.rs | 4 +- beacon_node/beacon_chain/tests/events.rs | 13 +- .../tests/payload_invalidation.rs | 4 +- .../beacon_chain/tests/prepare_payload.rs | 14 +- beacon_node/beacon_chain/tests/store_tests.rs | 22 +- .../builder_client/src/builder_http_client.rs | 56 +++- beacon_node/builder_client/src/builders.rs | 18 +- beacon_node/builder_client/src/error.rs | 10 +- .../src/beacon/execution_payload_bids.rs | 36 ++- .../src/beacon/execution_payload_envelopes.rs | 2 +- beacon_node/http_api/src/block_id.rs | 4 +- beacon_node/http_api/src/lib.rs | 4 +- beacon_node/http_api/src/utils.rs | 1 + .../http_api/tests/gloas_reorg_tests.rs | 8 +- beacon_node/http_api/tests/tests.rs | 64 +++-- .../lighthouse_network/src/service/mod.rs | 31 ++- .../lighthouse_network/src/types/pubsub.rs | 113 ++++++-- .../gossip_methods.rs | 5 +- .../src/network_beacon_processor/tests.rs | 2 +- .../sync/block_lookups/single_block_lookup.rs | 2 +- .../src/sync/block_sidecar_coupling.rs | 12 +- beacon_node/operation_pool/src/attestation.rs | 2 +- beacon_node/operation_pool/src/lib.rs | 5 +- consensus/fork_choice/src/fork_choice.rs | 12 +- .../src/envelope_processing.rs | 20 +- consensus/state_processing/src/genesis.rs | 39 ++- .../src/per_block_processing.rs | 88 +++--- .../per_block_processing/signature_sets.rs | 14 +- .../src/per_block_processing/tests.rs | 12 +- .../src/per_block_processing/withdrawals.rs | 2 +- .../state_processing/src/upgrade/gloas.rs | 6 +- .../state_processing/src/upgrade/heze.rs | 27 +- consensus/types/presets/gnosis/heze.yaml | 2 + consensus/types/presets/mainnet/heze.yaml | 2 + consensus/types/presets/minimal/heze.yaml | 2 + consensus/types/src/block/beacon_block.rs | 6 +- .../types/src/block/beacon_block_body.rs | 41 ++- .../types/src/block/signed_beacon_block.rs | 8 +- consensus/types/src/core/eth_spec.rs | 15 + consensus/types/src/core/preset.rs | 4 + .../src/execution/execution_payload_bid.rs | 195 ++++++++++++- consensus/types/src/execution/mod.rs | 10 +- .../execution/signed_execution_payload_bid.rs | 176 ++++++++++-- consensus/types/src/state/beacon_state.rs | 58 +++- testing/ef_tests/src/cases/operations.rs | 18 +- testing/ef_tests/src/type_name.rs | 4 + testing/ef_tests/tests/tests.rs | 15 +- .../validator_services/src/block_service.rs | 8 +- 78 files changed, 1718 insertions(+), 725 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_block_reward.rs b/beacon_node/beacon_chain/src/beacon_block_reward.rs index fdd145b4234..4c48a8a0291 100644 --- a/beacon_node/beacon_chain/src/beacon_block_reward.rs +++ b/beacon_node/beacon_chain/src/beacon_block_reward.rs @@ -45,11 +45,11 @@ impl BeaconChain { let bid_parent_block_hash = block .body() .signed_execution_payload_bid()? - .message - .parent_block_hash; - if bid_parent_block_hash == parent_bid.block_hash { + .message() + .parent_block_hash(); + if bid_parent_block_hash == parent_bid.block_hash() { let availability_index = parent_bid - .slot + .slot() .as_usize() .safe_rem(T::EthSpec::slots_per_historical_root())?; state @@ -291,7 +291,7 @@ impl BeaconChain { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); for attestation in block.body().attestations() { let data = attestation.data(); diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index d20dfb8d6eb..31746fcabaf 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3935,7 +3935,7 @@ impl BeaconChain { .message() .body() .signed_execution_payload_bid()? - .clone(), + .clone_as_signed_execution_payload_bid(), ); chain.pending_payload_cache.insert_bid(block_root, bid); } diff --git a/beacon_node/beacon_chain/src/block_production/bid_selection.rs b/beacon_node/beacon_chain/src/block_production/bid_selection.rs index 01f756d71d9..ca61944ec22 100644 --- a/beacon_node/beacon_chain/src/block_production/bid_selection.rs +++ b/beacon_node/beacon_chain/src/block_production/bid_selection.rs @@ -153,15 +153,15 @@ impl BidCandidate { /// terms are far below `Uint256::MAX`, so it is exact (a u64 gwei sum could saturate under a /// hostile `execution_payment` with the cap disabled). fn trusted_value(&self) -> Uint256 { - let bid = &self.signed_bid.message; + let bid = self.signed_bid.message(); match &self.source { BidSource::Local { block_value, .. } => *block_value, - BidSource::Gossip => gwei_to_wei(bid.value), // gossip `execution_payment` is zero + BidSource::Gossip => gwei_to_wei(bid.value()), // gossip `execution_payment` is zero BidSource::Direct { max_execution_payment, .. - } => gwei_to_wei(bid.value).saturating_add(gwei_to_wei( - bid.execution_payment.min(*max_execution_payment), + } => gwei_to_wei(bid.value()).saturating_add(gwei_to_wei( + bid.execution_payment().min(*max_execution_payment), )), } } @@ -197,10 +197,10 @@ impl BidCandidate { /// or the **unclamped** `value + execution_payment` (the proposer's real revenue; the clamp is a /// ranking-only trust bound). pub fn payload_value(&self) -> Uint256 { - let bid = &self.signed_bid.message; + let bid = self.signed_bid.message(); match &self.source { BidSource::Local { block_value, .. } => *block_value, - _ => gwei_to_wei(bid.value.saturating_add(bid.execution_payment)), + _ => gwei_to_wei(bid.value().saturating_add(bid.execution_payment())), } } @@ -252,7 +252,7 @@ mod tests { use super::*; use bls::Signature; use ssz_types::VariableList; - use types::{ExecutionPayloadBid, MainnetEthSpec}; + use types::{ExecutionPayloadBidGloas, MainnetEthSpec, SignedExecutionPayloadBidGloas}; type TestSpec = MainnetEthSpec; @@ -277,15 +277,17 @@ mod tests { value_gwei: u64, payment_gwei: u64, ) -> Arc> { - Arc::new(SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - builder_index, - value: value_gwei, - execution_payment: payment_gwei, - ..Default::default() + Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + builder_index, + value: value_gwei, + execution_payment: payment_gwei, + ..Default::default() + }, + signature: Signature::empty(), }, - signature: Signature::empty(), - }) + )) } fn gossip(value_gwei: u64, boost: u64) -> BidCandidate { @@ -327,13 +329,13 @@ mod tests { fn local(block_value_gwei: u64, should_override_builder: bool) -> BidCandidate { BidCandidate::local( - SignedExecutionPayloadBid { - message: ExecutionPayloadBid { + SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { builder_index: LOCAL_BUILDER, ..Default::default() }, signature: Signature::empty(), - }, + }), ExecutionPayloadData { payload: ExecutionPayloadGloas::default(), execution_requests: ExecutionRequestsGloas::default(), @@ -354,7 +356,7 @@ mod tests { BidSource::Direct { .. } => "direct", }; ( - win.signed_bid.message.builder_index, + win.signed_bid.message().builder_index(), win.is_local(), win.payload_value(), source, diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index a58f13947b6..1a74e735893 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -9,7 +9,7 @@ use execution_layer::{ BlockProposalContentsGloas, BuilderParams, PayloadAttributes, PayloadParameters, }; use operation_pool::CompactAttestationRef; -use ssz::{Encode, ProgressiveBitList, TryFromIter}; +use ssz::{BitVector, Encode, ProgressiveBitList, TryFromIter}; use ssz_types::ProgressiveVariableList; use state_processing::common::{get_attesting_indices_from_state, get_indexed_payload_attestation}; use state_processing::envelope_processing::verify_execution_payload_envelope; @@ -31,10 +31,11 @@ use types::{ Address, Attestation, AttestationGloas, AttesterSlashing, AttesterSlashingGloas, BeaconBlock, BeaconBlockBodyGloas, BeaconBlockBodyHeze, BeaconBlockGloas, BeaconBlockHeze, BeaconState, BeaconStateError, BlobsList, BuilderIndex, Deposit, Eth1Data, EthSpec, ExecutionBlockHash, - ExecutionPayloadBid, ExecutionPayloadEnvelope, ExecutionRequestsGloas, FullPayload, Graffiti, - Hash256, IndexedAttestation, KzgProofs, PayloadAttestation, ProgressiveTransactions, - ProposerSlashing, RelativeEpoch, SignedBeaconBlock, SignedBlsToExecutionChange, - SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, + ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadEnvelope, + ExecutionRequestsGloas, FullPayload, Graffiti, Hash256, IndexedAttestation, KzgProofs, + PayloadAttestation, ProgressiveTransactions, ProposerSlashing, RelativeEpoch, + SignedBeaconBlock, SignedBlsToExecutionChange, SignedExecutionPayloadBid, + SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, SignedExecutionPayloadEnvelope, SignedExecutionPayloadEnvelopeSummary, SignedProposerPreferences, SignedVoluntaryExit, Slot, SyncAggregate, Uint256, Withdrawal, Withdrawals, }; @@ -252,9 +253,9 @@ impl BeaconChain { // Distinct from the beacon-chain parent (`parent_root`); on the wire this becomes the // bid's `parent_block_hash` and the builder request's `parent_hash` path parameter. let executed_ancestor_hash = if should_build_on_full || parent_is_pre_gloas { - parent_bid.block_hash + parent_bid.block_hash() } else { - parent_bid.parent_block_hash + parent_bid.parent_block_hash() }; // The per-proposal context addressing each `getExecutionPayloadBid`. @@ -451,7 +452,7 @@ impl BeaconChain { // building on the parent's payload) so that attestation packing scores head votes like // block processing does. if should_build_on_full { - let parent_slot = state.latest_execution_payload_bid()?.slot; + let parent_slot = state.latest_execution_payload_bid()?.slot(); let availability_index = parent_slot.as_usize() % T::EthSpec::slots_per_historical_root(); state @@ -722,74 +723,104 @@ impl BeaconChain { "Cannot construct a block pre-Gloas".to_owned(), )); } - BeaconState::Gloas(_) => BeaconBlock::Gloas(BeaconBlockGloas { - slot, - proposer_index, - parent_root, - state_root: Hash256::ZERO, - body: BeaconBlockBodyGloas { - randao_reveal, - eth1_data, - graffiti, - // The operation list lengths are bounded by the op pool packing limits above. - proposer_slashings: ProgressiveVariableList::try_from_iter(proposer_slashings) - .map_err(BlockProductionError::SszTypesError)?, - attester_slashings: ProgressiveVariableList::try_from_iter(attester_slashings) - .map_err(BlockProductionError::SszTypesError)?, - attestations: ProgressiveVariableList::try_from_iter(attestations) + BeaconState::Gloas(_) => { + let SignedExecutionPayloadBid::Gloas(signed_execution_payload_bid) = + signed_execution_payload_bid + else { + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a Gloas block with a non-Gloas execution payload bid" + .to_string(), + )); + }; + + BeaconBlock::Gloas(BeaconBlockGloas { + slot, + proposer_index, + parent_root, + state_root: Hash256::ZERO, + body: BeaconBlockBodyGloas { + randao_reveal, + eth1_data, + graffiti, + // The operation list lengths are bounded by the op pool packing limits above. + proposer_slashings: ProgressiveVariableList::try_from_iter( + proposer_slashings, + ) .map_err(BlockProductionError::SszTypesError)?, - deposits: ProgressiveVariableList::try_from_iter(deposits) + attester_slashings: ProgressiveVariableList::try_from_iter( + attester_slashings, + ) .map_err(BlockProductionError::SszTypesError)?, - voluntary_exits: ProgressiveVariableList::try_from_iter(voluntary_exits) + attestations: ProgressiveVariableList::try_from_iter(attestations) + .map_err(BlockProductionError::SszTypesError)?, + deposits: ProgressiveVariableList::try_from_iter(deposits) + .map_err(BlockProductionError::SszTypesError)?, + voluntary_exits: ProgressiveVariableList::try_from_iter(voluntary_exits) + .map_err(BlockProductionError::SszTypesError)?, + sync_aggregate, + bls_to_execution_changes: ProgressiveVariableList::try_from_iter( + bls_to_execution_changes, + ) .map_err(BlockProductionError::SszTypesError)?, - sync_aggregate, - bls_to_execution_changes: ProgressiveVariableList::try_from_iter( - bls_to_execution_changes, - ) - .map_err(BlockProductionError::SszTypesError)?, - parent_execution_requests, - signed_execution_payload_bid, - payload_attestations: ProgressiveVariableList::try_from_iter( - payload_attestations, - ) - .map_err(BlockProductionError::SszTypesError)?, - _phantom: PhantomData::>, - }, - }), - BeaconState::Heze(_) => BeaconBlock::Heze(BeaconBlockHeze { - slot, - proposer_index, - parent_root, - state_root: Hash256::ZERO, - body: BeaconBlockBodyHeze { - randao_reveal, - eth1_data, - graffiti, - // The operation list lengths are bounded by the op pool packing limits above. - proposer_slashings: ProgressiveVariableList::try_from_iter(proposer_slashings) + parent_execution_requests, + signed_execution_payload_bid, + payload_attestations: ProgressiveVariableList::try_from_iter( + payload_attestations, + ) .map_err(BlockProductionError::SszTypesError)?, - attester_slashings: ProgressiveVariableList::try_from_iter(attester_slashings) + _phantom: PhantomData::>, + }, + }) + } + BeaconState::Heze(_) => { + let SignedExecutionPayloadBid::Heze(signed_execution_payload_bid) = + signed_execution_payload_bid + else { + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a Heze block with a non-Heze execution payload bid" + .to_string(), + )); + }; + + BeaconBlock::Heze(BeaconBlockHeze { + slot, + proposer_index, + parent_root, + state_root: Hash256::ZERO, + body: BeaconBlockBodyHeze { + randao_reveal, + eth1_data, + graffiti, + // The operation list lengths are bounded by the op pool packing limits above. + proposer_slashings: ProgressiveVariableList::try_from_iter( + proposer_slashings, + ) .map_err(BlockProductionError::SszTypesError)?, - attestations: ProgressiveVariableList::try_from_iter(attestations) + attester_slashings: ProgressiveVariableList::try_from_iter( + attester_slashings, + ) .map_err(BlockProductionError::SszTypesError)?, - deposits: ProgressiveVariableList::try_from_iter(deposits) + attestations: ProgressiveVariableList::try_from_iter(attestations) + .map_err(BlockProductionError::SszTypesError)?, + deposits: ProgressiveVariableList::try_from_iter(deposits) + .map_err(BlockProductionError::SszTypesError)?, + voluntary_exits: ProgressiveVariableList::try_from_iter(voluntary_exits) + .map_err(BlockProductionError::SszTypesError)?, + sync_aggregate, + bls_to_execution_changes: ProgressiveVariableList::try_from_iter( + bls_to_execution_changes, + ) .map_err(BlockProductionError::SszTypesError)?, - voluntary_exits: ProgressiveVariableList::try_from_iter(voluntary_exits) + parent_execution_requests, + signed_execution_payload_bid, + payload_attestations: ProgressiveVariableList::try_from_iter( + payload_attestations, + ) .map_err(BlockProductionError::SszTypesError)?, - sync_aggregate, - bls_to_execution_changes: ProgressiveVariableList::try_from_iter( - bls_to_execution_changes, - ) - .map_err(BlockProductionError::SszTypesError)?, - parent_execution_requests, - signed_execution_payload_bid, - payload_attestations: ProgressiveVariableList::try_from_iter( - payload_attestations, - ) - .map_err(BlockProductionError::SszTypesError)?, - _phantom: PhantomData::>, - }, - }), + _phantom: PhantomData::>, + }, + }) + } }; let signed_beacon_block = SignedBeaconBlock::from_block( @@ -996,21 +1027,63 @@ impl BeaconChain { should_override_builder, } = block_proposal_contents; - // TODO(gloas) since we are defaulting to local building, execution payment is 0 - // execution payment should only be set to > 0 for trusted building. - let bid = ExecutionPayloadBid:: { - parent_block_hash: executed_ancestor_hash, - parent_block_root: parent_root, - block_hash: payload.block_hash, - prev_randao: payload.prev_randao, - fee_recipient: Address::ZERO, - gas_limit: payload.gas_limit, - builder_index, - slot: produce_at_slot, - value: bid_value, - execution_payment: EXECUTION_PAYMENT_TRUSTLESS_BUILD, - blob_kzg_commitments, - execution_requests_root: execution_requests.tree_hash_root(), + let signature = Signature::infinity().map_err(BlockProductionError::BlsError)?; + let signed_execution_payload_bid = match state { + BeaconState::Base(_) + | BeaconState::Altair(_) + | BeaconState::Bellatrix(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) + | BeaconState::Electra(_) + | BeaconState::Fulu(_) => { + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot produce an execution payload bid pre-Gloas".to_owned(), + )); + } + BeaconState::Gloas(_) => { + let bid = ExecutionPayloadBidGloas:: { + parent_block_hash: executed_ancestor_hash, + parent_block_root: parent_root, + block_hash: payload.block_hash, + prev_randao: payload.prev_randao, + fee_recipient: Address::ZERO, + gas_limit: payload.gas_limit, + builder_index, + slot: produce_at_slot, + value: bid_value, + execution_payment: EXECUTION_PAYMENT_TRUSTLESS_BUILD, + blob_kzg_commitments, + execution_requests_root: execution_requests.tree_hash_root(), + }; + + SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas { + message: bid, + signature, + }) + } + BeaconState::Heze(_) => { + let bid = ExecutionPayloadBidHeze:: { + parent_block_hash: executed_ancestor_hash, + parent_block_root: parent_root, + block_hash: payload.block_hash, + prev_randao: payload.prev_randao, + fee_recipient: Address::ZERO, + gas_limit: payload.gas_limit, + builder_index, + slot: produce_at_slot, + value: bid_value, + execution_payment: EXECUTION_PAYMENT_TRUSTLESS_BUILD, + blob_kzg_commitments, + execution_requests_root: execution_requests.tree_hash_root(), + // TODO(heze): set the bits based on the IL committee members whose + // inclusion lists were considered when building the payload + inclusion_list_bits: BitVector::new(), + }; + SignedExecutionPayloadBid::Heze(SignedExecutionPayloadBidHeze { + message: bid, + signature, + }) + } }; // Store payload data for envelope construction after block is created @@ -1023,10 +1096,7 @@ impl BeaconChain { }; Ok(( - SignedExecutionPayloadBid { - message: bid, - signature: Signature::infinity().map_err(BlockProductionError::BlsError)?, - }, + signed_execution_payload_bid, LocalBuildResult { payload_data, payload_value, @@ -1092,7 +1162,7 @@ impl BeaconChain { // eligibility or coverage can go stale before production. Re-check against the production // state and drop it if it would now fail `per_block_processing`, so a stale gossip bid // can't outrank a viable candidate and sink the whole proposal. - match verify_bid_state_conditions(&gossip_bid.message, state, &self.spec) { + match verify_bid_state_conditions(gossip_bid.message(), state, &self.spec) { Ok(_) => { externals.push(BidCandidate::gossip( gossip_bid, @@ -1109,10 +1179,23 @@ impl BeaconChain { } } + // Filter out any bid whose variant doesn't match the fork at its slot, making sure + // that we don't fail block production + externals.retain(|candidate| { + if let Err(error) = candidate.signed_bid.fork_name(&self.spec) { + warn!( + ?error, + "Skipping bid with a variant inconsistent with its fork" + ); + return false; + } + true + }); + // The parent's exit requests apply to the state before this block's bid is processed, so a // bid from a builder the parent payload exits fails `process_execution_payload_bid`. externals.retain(|candidate| { - let builder_index = candidate.signed_bid.message.builder_index; + let builder_index = candidate.signed_bid.message().builder_index(); let exit_requested = state .get_builder(builder_index) .is_ok_and(|builder| builder_exit_requested(builder, parent_execution_requests)); @@ -1266,7 +1349,7 @@ fn get_execution_payload_gloas( let random = *state.get_randao_mix(current_epoch)?; let parent_bid = state.latest_execution_payload_bid()?; - let is_parent_block_full = parent_block_hash == parent_bid.block_hash; + let is_parent_block_full = parent_block_hash == parent_bid.block_hash(); let target_gas_limit = preferred_gas_limit.unwrap_or_else(|| spec.default_gas_limit(current_epoch)); diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 698e156722e..5f78a9785e6 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -953,10 +953,10 @@ impl GossipVerifiedBlock { // [New in Gloas]: Verify bid.parent_block_root matches block.parent_root. if let Ok(bid) = block.message().body().signed_execution_payload_bid() - && bid.message.parent_block_root != block.message().parent_root() + && bid.message().parent_block_root() != block.message().parent_root() { return Err(BlockError::BidParentRootMismatch { - bid_parent_root: bid.message.parent_block_root, + bid_parent_root: bid.message().parent_block_root(), block_parent_root: block.message().parent_root(), }); } diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 99172c199e0..624430af247 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -144,12 +144,12 @@ impl RangeSyncBlock { envelope: Option>, ) -> Result { if let Some(envelope) = envelope.as_ref() { - let execution_bid = &block + let execution_bid = block .message() .body() .signed_execution_payload_bid() .map_err(|e| format!("missing signed_execution_payload_bid: {e:?}"))? - .message; + .message(); // Skip the finalized-slot check; range sync imports historical (finalized) blocks. let latest_finalized_slot = Slot::new(0); verify_envelope_consistency( diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index 23ef67c2731..7a773313d41 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -365,7 +365,7 @@ impl CachedHead { self.snapshot .beacon_state .latest_execution_payload_bid() - .map(|bid| bid.prev_randao) + .map(|bid| bid.prev_randao()) } else { block.body().execution_payload().map(|p| p.prev_randao()) } diff --git a/beacon_node/beacon_chain/src/custody_context.rs b/beacon_node/beacon_chain/src/custody_context.rs index 4a1dfdbe711..7664c4b6b3e 100644 --- a/beacon_node/beacon_chain/src/custody_context.rs +++ b/beacon_node/beacon_chain/src/custody_context.rs @@ -11,7 +11,7 @@ use std::{ }; use tracing::{debug, warn}; use types::{ - ChainSpec, ColumnIndex, Epoch, EthSpec, SignedBeaconBlock, SignedExecutionPayloadBid, Slot, + ChainSpec, ColumnIndex, Epoch, EthSpec, SignedBeaconBlock, SignedExecutionPayloadBidRef, Slot, }; /// A delay before making the CGC change effective to the data availability checker. @@ -590,7 +590,7 @@ impl CustodyContext { pub fn data_columns_required_for_bid( &self, - bid: &SignedExecutionPayloadBid, + bid: SignedExecutionPayloadBidRef, ) -> bool { bid.num_blobs_expected() > 0 && self.data_columns_required_for_epoch(bid.epoch()) } diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 54598e37add..7224c9ea4f9 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -647,7 +647,7 @@ pub fn verify_columns_against_block( .message() .body() .signed_execution_payload_bid() - .map(|bid| bid.message.blob_kzg_commitments.to_vec()) + .map(|bid| bid.message().blob_kzg_commitments().to_vec()) .map_err(|_| { AvailabilityCheckError::Unexpected( "Gloas block missing signed_execution_payload_bid".to_string(), diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index acc6e95aa21..50c7c55fb69 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -413,7 +413,7 @@ impl GossipVerifiedDataColumn )?; verify_data_column_sidecar_with_commitments_len( &column_sidecar, - bid.message.blob_kzg_commitments.len(), + bid.message().blob_kzg_commitments().len(), &chain.spec, )?; } @@ -1236,13 +1236,13 @@ pub fn validate_data_column_sidecar_for_gossip_gloas< slot: column_slot, }, )?; - if bid.message.slot != column_slot { + if bid.message().slot() != column_slot { return Err(GossipDataColumnError::BlockSlotMismatch { - block_slot: bid.message.slot, + block_slot: bid.message().slot(), data_column_slot: column_slot, }); } - let kzg_commitments = &bid.message.blob_kzg_commitments; + let kzg_commitments = bid.message().blob_kzg_commitments(); verify_data_column_sidecar_with_commitments_len( &data_column, kzg_commitments.len(), @@ -1388,10 +1388,10 @@ fn validate_partial_data_column_sidecar_for_gossip_gloas( } }; - if slot != bid.message.slot { + if slot != bid.slot() { return PartialColumnVerificationResult::Err(GossipPartialDataColumnError::IncorrectSlot { group_id_slot: slot, - block_slot: bid.message.slot, + block_slot: bid.slot(), }); } @@ -1402,7 +1402,7 @@ fn validate_partial_data_column_sidecar_for_gossip_gloas( match validate_partial_data_column_common( PartialDataColumnRef::Gloas(&column), - bid.message.blob_kzg_commitments.as_ref(), + bid.message().blob_kzg_commitments().as_ref(), chain, ) { Ok(()) => PartialColumnVerificationResult::Ok(GossipVerifiedPartialDataColumn::PostGloas { @@ -1478,7 +1478,7 @@ impl GossipVerifiedPartialDataColumn { pub fn slot(&self) -> Slot { match self { Self::PreGloas { header, .. } => header.as_header().slot(), - Self::PostGloas { bid, .. } => bid.message.slot, + Self::PostGloas { bid, .. } => bid.slot(), } } @@ -1568,7 +1568,7 @@ pub(crate) fn load_gloas_payload_bid( .body() .signed_execution_payload_bid() .map_err(BeaconChainError::BeaconStateError)? - .clone(), + .clone_as_signed_execution_payload_bid(), ) } else { match chain @@ -1582,7 +1582,7 @@ pub(crate) fn load_gloas_payload_bid( .body() .signed_execution_payload_bid() .map_err(BeaconChainError::BeaconStateError)? - .clone(), + .clone_as_signed_execution_payload_bid(), ), Some(DatabaseBlock::Blinded(block)) => Arc::new( block @@ -1590,7 +1590,7 @@ pub(crate) fn load_gloas_payload_bid( .body() .signed_execution_payload_bid() .map_err(BeaconChainError::BeaconStateError)? - .clone(), + .clone_as_signed_execution_payload_bid(), ), None => { return Ok(None); @@ -1895,8 +1895,8 @@ mod test { Cell, CellBitmap, DataColumnSidecar, DataColumnSidecarFulu, DataColumnSubnetId, EthSpec, ForkName, Hash256, MainnetEthSpec, PartialDataColumn, PartialDataColumnFulu, PartialDataColumnGloas, PartialDataColumnHeader, PartialDataColumnSidecarFulu, - PartialDataColumnSidecarGloas, SignedExecutionPayloadBid, Slot, - test_utils::test_unstructured, + PartialDataColumnSidecarGloas, SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, + Slot, test_utils::test_unstructured, }; type E = MainnetEthSpec; @@ -1973,7 +1973,7 @@ mod test { .body() .signed_execution_payload_bid() .unwrap() - .clone(); + .clone_as_signed_execution_payload_bid(); // Put the block on disk only, so the bid can only be found via the store fallback. harness.chain.store.put_block(&block_root, block).unwrap(); @@ -2410,12 +2410,12 @@ mod test { block_root: Hash256, slot: Slot, ) { - let mut bid = SignedExecutionPayloadBid::::empty(); + let mut bid = SignedExecutionPayloadBidGloas::::empty(); bid.message.slot = slot; harness .chain .pending_payload_cache - .insert_bid(block_root, Arc::new(bid)); + .insert_bid(block_root, Arc::new(SignedExecutionPayloadBid::Gloas(bid))); } async fn gloas_partial_slot_mismatch_returns_error( diff --git a/beacon_node/beacon_chain/src/execution_proof_verification/gossip_verified_execution_proof.rs b/beacon_node/beacon_chain/src/execution_proof_verification/gossip_verified_execution_proof.rs index 158ec24f6a9..256c979ee3b 100644 --- a/beacon_node/beacon_chain/src/execution_proof_verification/gossip_verified_execution_proof.rs +++ b/beacon_node/beacon_chain/src/execution_proof_verification/gossip_verified_execution_proof.rs @@ -241,8 +241,8 @@ fn get_execution_proof( })?; let versioned_hashes = VersionedHashes::::new( - bid.message - .blob_kzg_commitments + bid.message() + .blob_kzg_commitments() .iter() .map(kzg_commitment_to_versioned_hash) .collect(), diff --git a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs index d1f45254aa8..1852df457a1 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs @@ -65,7 +65,9 @@ impl PartialHeaderOrBid { block: &SignedBeaconBlock, ) -> Option { if let Ok(bid) = block.message().body().signed_execution_payload_bid() { - Some(PartialHeaderOrBid::Bid(Arc::new(bid.clone()))) + Some(PartialHeaderOrBid::Bid(Arc::new( + bid.clone_as_signed_execution_payload_bid(), + ))) } else { PartialDataColumnHeader::try_from(block) .ok() @@ -76,14 +78,16 @@ impl PartialHeaderOrBid { pub fn kzg_commitments(&self) -> ListRef<'_, KzgCommitment, E::MaxBlobCommitmentsPerBlock> { match self { PartialHeaderOrBid::PartialHeader(header) => ListRef::Basic(&header.kzg_commitments), - PartialHeaderOrBid::Bid(bid) => ListRef::Progressive(&bid.message.blob_kzg_commitments), + PartialHeaderOrBid::Bid(bid) => { + ListRef::Progressive(bid.message().blob_kzg_commitments()) + } } } pub fn slot(&self) -> Slot { match self { PartialHeaderOrBid::PartialHeader(header) => header.slot(), - PartialHeaderOrBid::Bid(bid) => bid.message.slot, + PartialHeaderOrBid::Bid(bid) => bid.slot(), } } } @@ -632,7 +636,7 @@ async fn build_partial_columns_from_v4_response( header.slot(), ), PartialHeaderOrBid::Bid(bid) => { - ObservationKey::new_block_root_key(block_root, bid.message.slot) + ObservationKey::new_block_root_key(block_root, bid.message().slot()) } }; if let Some(observed_columns) = @@ -709,7 +713,7 @@ async fn compute_custody_columns_to_import( header.slot(), ), PartialHeaderOrBid::Bid(bid) => { - ObservationKey::new_block_root_key(block_root, bid.message.slot) + ObservationKey::new_block_root_key(block_root, bid.slot()) } }; diff --git a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs index d8dcdf86931..cf160c17f4a 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs @@ -525,7 +525,7 @@ mod get_blobs_v4 { .body() .signed_execution_payload_bid() .expect("gloas block has a bid") - .clone(), + .clone_as_signed_execution_payload_bid(), ); // Real pending payload cache: the Gloas path inserts the bid and merges partial columns into diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index e6b097ec345..aff2e387c37 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -146,7 +146,7 @@ impl BeaconChain { .body() .signed_execution_payload_bid() .ok() - .map(|bid| bid.message.parent_block_hash) + .map(|bid| bid.message().parent_block_hash()) }); let mut blob_batch = Vec::::new(); @@ -240,7 +240,7 @@ impl BeaconChain { .body() .signed_execution_payload_bid() .ok() - .map(|bid| bid.message.block_hash) + .map(|bid| bid.message().block_hash()) .and_then(|bid_hash| { child_bid_parent_hash.map(|child_parent_hash| bid_hash == child_parent_hash) }); @@ -334,7 +334,7 @@ impl BeaconChain { .body() .signed_execution_payload_bid() .ok() - .map(|bid| bid.message.parent_block_hash); + .map(|bid| bid.message().parent_block_hash()); signed_blocks.push(block); // If we've reached genesis, add the genesis block root to the batch for all slots diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index f6dab3c9a7e..a224af6cdd7 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -465,7 +465,7 @@ pub fn blobs_to_partial_data_columns( } PartialHeaderOrBid::Bid(bid) => build_partial_data_columns_gloas( block_root, - bid.message.slot, + bid.slot(), blob_cells_and_proofs_vec, spec, ) diff --git a/beacon_node/beacon_chain/src/observed_execution_payloads.rs b/beacon_node/beacon_chain/src/observed_execution_payloads.rs index 29086222528..7b40ed3cb11 100644 --- a/beacon_node/beacon_chain/src/observed_execution_payloads.rs +++ b/beacon_node/beacon_chain/src/observed_execution_payloads.rs @@ -51,13 +51,13 @@ impl BeaconChain { .insert(payload.block_hash(), payload.gas_limit()); } } else if block.slot() == self.spec.genesis_slot { - let bid = &block + let bid = block .body() .signed_execution_payload_bid() .map_err(BeaconChainError::BeaconStateError)? - .message; + .message(); self.observed_execution_payloads - .insert(bid.parent_block_hash, bid.gas_limit); + .insert(bid.parent_block_hash(), bid.gas_limit()); } else if let Some(envelope) = head.snapshot.execution_envelope.as_ref() { let payload_header = &envelope.payload_header; self.observed_execution_payloads diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs b/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs index f57d5628695..a063b9c3b0e 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs @@ -45,37 +45,44 @@ pub fn verify_direct_bid( state: &BeaconState, spec: &ChainSpec, ) -> Result<(), PayloadBidError> { - let bid = &signed_bid.message; + let bid = signed_bid.message(); + + // Ensure the bid is the correct structure for the fork at `bid.slot()`. + signed_bid + .fork_name(spec) + .map_err(PayloadBidError::InconsistentFork)?; // The bid must be for exactly the slot being produced. - if bid.slot != proposal_slot { - return Err(PayloadBidError::InvalidBidSlot { bid_slot: bid.slot }); + if bid.slot() != proposal_slot { + return Err(PayloadBidError::InvalidBidSlot { + bid_slot: bid.slot(), + }); } // The bid must build on the executed ancestor the producer selected (FULL or EMPTY view). - if bid.parent_block_hash != executed_ancestor_hash { + if bid.parent_block_hash() != executed_ancestor_hash { return Err(PayloadBidError::InvalidParentBlockHash { - bid: bid.parent_block_hash, + bid: bid.parent_block_hash(), expected: executed_ancestor_hash, }); } - if bid.parent_block_root != parent_block_root { + if bid.parent_block_root() != parent_block_root { return Err(PayloadBidError::InvalidParentBlockRoot { - bid: bid.parent_block_root, + bid: bid.parent_block_root(), expected: parent_block_root, }); } // `prev_randao` must be the RANDAO mix from the production state. let expected_prev_randao = *state.get_randao_mix(proposal_slot.epoch(E::slots_per_epoch()))?; - if bid.prev_randao != expected_prev_randao { - return Err(PayloadBidError::InvalidPrevRandao { slot: bid.slot }); + if bid.prev_randao() != expected_prev_randao { + return Err(PayloadBidError::InvalidPrevRandao { slot: bid.slot() }); } // The gas limit must be compatible with the parent payload's, given the proposer's target. if !is_gas_limit_target_compatible( executed_ancestor_gas_limit, - bid.gas_limit, + bid.gas_limit(), proposer_preferences.message.target_gas_limit, )? { return Err(PayloadBidError::InvalidGasLimit); @@ -89,14 +96,14 @@ pub fn verify_direct_bid( // response filter from beacon-APIs #630; an empty list accepts any builder). if !expected_builder_pubkeys.is_empty() { let actual = state - .get_builder(bid.builder_index) + .get_builder(bid.builder_index()) .map_err(|_| PayloadBidError::InvalidBuilder { - builder_index: bid.builder_index, + builder_index: bid.builder_index(), })? .pubkey; if !expected_builder_pubkeys.contains(&actual) { return Err(PayloadBidError::UnexpectedBuilder { - builder_index: bid.builder_index, + builder_index: bid.builder_index(), }); } } @@ -105,7 +112,7 @@ pub fn verify_direct_bid( execution_payload_bid_signature_set( state, |i| get_builder_pubkey_from_state(state, i), - signed_bid, + signed_bid.to_ref(), spec, ) .map_err(|_| PayloadBidError::BadSignature)? @@ -121,7 +128,11 @@ pub fn verify_direct_bid( mod tests { use super::*; use bls::Signature; - use types::{Address, ExecutionPayloadBid, MinimalEthSpec, ProposerPreferences}; + use types::{ + Address, Epoch, ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ForkName, + MinimalEthSpec, ProposerPreferences, SignedExecutionPayloadBidGloas, + SignedExecutionPayloadBidHeze, + }; type E = MinimalEthSpec; @@ -129,7 +140,7 @@ mod tests { const EXECUTED_ANCESTOR_GAS_LIMIT: u64 = 30_000_000; fn state_and_spec() -> (BeaconState, ChainSpec) { - let spec = E::default_spec(); + let spec = ForkName::Gloas.make_genesis_spec(E::default_spec()); let state = BeaconState::new(0, <_>::default(), &spec); (state, spec) } @@ -145,22 +156,40 @@ mod tests { } } + fn bid_message( + slot: Slot, + parent_block_hash: ExecutionBlockHash, + parent_block_root: Hash256, + prev_randao: Hash256, + ) -> ExecutionPayloadBidGloas { + ExecutionPayloadBidGloas { + slot, + parent_block_hash, + parent_block_root, + prev_randao, + ..ExecutionPayloadBidGloas::default() + } + } + + fn sign(message: ExecutionPayloadBidGloas) -> SignedExecutionPayloadBid { + SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas { + message, + signature: Signature::empty(), + }) + } + fn signed_bid( slot: Slot, parent_block_hash: ExecutionBlockHash, parent_block_root: Hash256, prev_randao: Hash256, ) -> SignedExecutionPayloadBid { - SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - slot, - parent_block_hash, - parent_block_root, - prev_randao, - ..ExecutionPayloadBid::default() - }, - signature: Signature::empty(), - } + sign(bid_message( + slot, + parent_block_hash, + parent_block_root, + prev_randao, + )) } #[test] @@ -275,14 +304,15 @@ mod tests { // claims a `block_hash` equal to its `parent_block_hash` — the consensus assert from // `process_execution_payload_bid` that must be front-run before selection. let executed_ancestor = ExecutionBlockHash::repeat_byte(7); - let mut bid = signed_bid( + let mut message = bid_message( Slot::new(1), executed_ancestor, Hash256::ZERO, Hash256::ZERO, ); - bid.message.block_hash = executed_ancestor; - bid.message.gas_limit = EXECUTED_ANCESTOR_GAS_LIMIT; + message.block_hash = executed_ancestor; + message.gas_limit = EXECUTED_ANCESTOR_GAS_LIMIT; + let bid = sign(message); let result = verify_direct_bid( &bid, Slot::new(1), @@ -305,13 +335,14 @@ mod tests { let (state, spec) = state_and_spec(); // Passes the slot, parent and RANDAO checks (a fresh state's mix is zero), then asks for // double the parent's gas limit — far outside the per-block adjustment bound. - let mut bid = signed_bid( + let mut message = bid_message( Slot::new(1), ExecutionBlockHash::zero(), Hash256::ZERO, Hash256::ZERO, ); - bid.message.gas_limit = EXECUTED_ANCESTOR_GAS_LIMIT * 2; + message.gas_limit = EXECUTED_ANCESTOR_GAS_LIMIT * 2; + let bid = sign(message); let result = verify_direct_bid( &bid, Slot::new(1), @@ -331,16 +362,17 @@ mod tests { let (state, spec) = state_and_spec(); // Same as above but holding the parent's gas limit, which is always compatible: the bid // must get past the gas check and fail later, on the fresh state's missing builder. - let mut bid = signed_bid( + let mut message = bid_message( Slot::new(1), ExecutionBlockHash::zero(), Hash256::ZERO, Hash256::ZERO, ); - bid.message.gas_limit = EXECUTED_ANCESTOR_GAS_LIMIT; + message.gas_limit = EXECUTED_ANCESTOR_GAS_LIMIT; // A default (zero) `block_hash` would equal the zero parent hash and trip the // block-hash-equals-parent rejection before the checks this test targets. - bid.message.block_hash = ExecutionBlockHash::repeat_byte(1); + message.block_hash = ExecutionBlockHash::repeat_byte(1); + let bid = sign(message); let result = verify_direct_bid( &bid, Slot::new(1), @@ -357,4 +389,54 @@ mod tests { "expected to fail after the gas check, got {result:?}" ); } + #[test] + fn rejects_gloas_bid_at_heze_slot() { + let (state, mut spec) = state_and_spec(); + let heze_fork_epoch = Epoch::new(1); + spec.heze_fork_epoch = Some(heze_fork_epoch); + let heze_slot = heze_fork_epoch.start_slot(E::slots_per_epoch()); + let bid = signed_bid( + heze_slot, + ExecutionBlockHash::zero(), + Hash256::ZERO, + Hash256::ZERO, + ); + let result = verify_direct_bid( + &bid, + heze_slot, + ExecutionBlockHash::zero(), + Hash256::ZERO, + EXECUTED_ANCESTOR_GAS_LIMIT, + &BuilderPubkeys::default(), + &preferences(), + &state, + &spec, + ); + assert!(matches!(result, Err(PayloadBidError::InconsistentFork(_)))); + } + + #[test] + fn rejects_heze_bid_at_gloas_slot() { + let (state, mut spec) = state_and_spec(); + spec.heze_fork_epoch = Some(Epoch::new(1)); + let bid = SignedExecutionPayloadBid::Heze(SignedExecutionPayloadBidHeze { + message: ExecutionPayloadBidHeze { + slot: Slot::new(1), + ..ExecutionPayloadBidHeze::default() + }, + signature: Signature::empty(), + }); + let result = verify_direct_bid( + &bid, + Slot::new(1), + ExecutionBlockHash::zero(), + Hash256::ZERO, + EXECUTED_ANCESTOR_GAS_LIMIT, + &BuilderPubkeys::default(), + &preferences(), + &state, + &spec, + ); + assert!(matches!(result, Err(PayloadBidError::InconsistentFork(_)))); + } } diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs b/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs index c8e6476f673..74cfb08c79c 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs @@ -19,7 +19,7 @@ use state_processing::signature_sets::{ }; use tracing::debug; use types::{ - BeaconState, Builder, ChainSpec, EthSpec, ExecutionPayloadBid, ExecutionRequestsGloas, + BeaconState, Builder, ChainSpec, EthSpec, ExecutionPayloadBidRef, ExecutionRequestsGloas, SignedExecutionPayloadBid, SignedProposerPreferences, Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, }; @@ -33,14 +33,14 @@ pub(crate) fn verify_bid_slot(bid_slot: Slot, current_slot: Slot) -> Result<(), } fn verify_bid_payment_and_blobs( - bid: &ExecutionPayloadBid, + bid: ExecutionPayloadBidRef, spec: &ChainSpec, ) -> Result<(), PayloadBidError> { // Execution payments are used by off protocol builders. In protocol bids // should always have this value set to zero. - if bid.execution_payment != 0 { + if bid.execution_payment() != 0 { return Err(PayloadBidError::ExecutionPaymentNonZero { - execution_payment: bid.execution_payment, + execution_payment: bid.execution_payment(), }); } @@ -55,28 +55,28 @@ fn verify_bid_payment_and_blobs( /// gossip *and* direct (builder-API) — must front-run it: a bid that fails only at block /// processing has already won selection and costs the proposer the slot. pub(crate) fn verify_bid_block_hash_not_parent( - bid: &ExecutionPayloadBid, + bid: ExecutionPayloadBidRef, ) -> Result<(), PayloadBidError> { - if bid.block_hash == bid.parent_block_hash { + if bid.block_hash() == bid.parent_block_hash() { return Err(PayloadBidError::BlockHashEqualsParentBlockHash { - slot: bid.slot, - block_hash: bid.block_hash, + slot: bid.slot(), + block_hash: bid.block_hash(), }); } Ok(()) } fn verify_bid_blobs( - bid: &ExecutionPayloadBid, + bid: ExecutionPayloadBidRef, spec: &ChainSpec, ) -> Result<(), PayloadBidError> { let max_blobs_per_block = - spec.max_blobs_per_block(bid.slot.epoch(E::slots_per_epoch())) as usize; + spec.max_blobs_per_block(bid.slot().epoch(E::slots_per_epoch())) as usize; - if bid.blob_kzg_commitments.len() > max_blobs_per_block { + if bid.blob_kzg_commitments().len() > max_blobs_per_block { return Err(PayloadBidError::InvalidBlobKzgCommitments { max_blobs_per_block, - blob_kzg_commitments_len: bid.blob_kzg_commitments.len(), + blob_kzg_commitments_len: bid.blob_kzg_commitments().len(), }); } @@ -94,15 +94,15 @@ fn verify_bid_blobs( /// rules). A check that must cover both intakes belongs in one of those shared helpers — adding /// it only here leaves gossip uncovered. pub(crate) fn verify_direct_bid_consistency( - bid: &ExecutionPayloadBid, + bid: ExecutionPayloadBidRef, current_slot: Slot, proposer_preferences: &SignedProposerPreferences, head_state: &BeaconState, spec: &ChainSpec, ) -> Result<(), PayloadBidError> { - verify_bid_slot(bid.slot, current_slot)?; + verify_bid_slot(bid.slot(), current_slot)?; - if bid.fee_recipient != proposer_preferences.message.fee_recipient { + if bid.fee_recipient() != proposer_preferences.message.fee_recipient { return Err(PayloadBidError::InvalidFeeRecipient); } @@ -123,11 +123,11 @@ pub(crate) fn verify_direct_bid_consistency( /// dropping). Re-running them against the production state lets bid selection drop a gossip bid that /// has since become invalid, rather than committing to it and failing the whole block. pub(crate) fn verify_bid_state_conditions( - bid: &ExecutionPayloadBid, + bid: ExecutionPayloadBidRef, head_state: &BeaconState, spec: &ChainSpec, ) -> Result<(), PayloadBidError> { - let builder_index = bid.builder_index; + let builder_index = bid.builder_index(); let builder_version = head_state .get_builder(builder_index) .map_err(|_| PayloadBidError::InvalidBuilder { builder_index })? @@ -147,10 +147,10 @@ pub(crate) fn verify_bid_state_conditions( return Err(PayloadBidError::InvalidBuilder { builder_index }); } - if !head_state.can_builder_cover_bid(builder_index, bid.value, spec)? { + if !head_state.can_builder_cover_bid(builder_index, bid.value(), spec)? { return Err(PayloadBidError::BuilderCantCoverBid { builder_index, - builder_bid: bid.value, + builder_bid: bid.value(), }); } @@ -160,24 +160,24 @@ pub(crate) fn verify_bid_state_conditions( /// Returns `true` if the bid builds on the parent's full payload and that payload carries an exit /// request for the bid's builder. pub(crate) fn parent_payload_exits_builder( - bid: &ExecutionPayloadBid, + bid: ExecutionPayloadBidRef, parent_block: &ProtoBlock, head_state: &BeaconState, store: &BeaconStore, ) -> Result { - if parent_block.execution_payload_block_hash != Some(bid.parent_block_hash) { + if parent_block.execution_payload_block_hash != Some(bid.parent_block_hash()) { return Ok(false); } - let builder = head_state.get_builder(bid.builder_index)?; + let builder = head_state.get_builder(bid.builder_index())?; // Only the execution requests are needed, and the summary survives payload body pruning. let parent_summary = store - .get_payload_envelope_summary(&bid.parent_block_root) + .get_payload_envelope_summary(&bid.parent_block_root()) .map_err(|e| { PayloadBidError::InternalError(format!("failed to load parent payload summary: {e:?}")) })? .ok_or(PayloadBidError::ParentExecutionPayloadUnknown { - parent_block_hash: bid.parent_block_hash, + parent_block_hash: bid.parent_block_hash(), })?; Ok(builder_exit_requested( @@ -199,7 +199,7 @@ pub(crate) fn builder_exit_requested( pub(crate) fn is_bid_compatible_with_head( cached_head: &CachedHead, fork_choice_read: &ForkChoiceReadGuard<'_, T>, - bid: &ExecutionPayloadBid, + bid: ExecutionPayloadBidRef<'_, T::EthSpec>, spec: &ChainSpec, ) -> Result { let head_block_root = cached_head.head_block_root(); @@ -217,28 +217,28 @@ pub(crate) fn is_bid_compatible_with_head( .fork_name_at_slot::(head_block.slot) .gloas_enabled(); - let builds_on_parent_block = Some(bid.parent_block_root) == head_block.parent_root; + let builds_on_parent_block = Some(bid.parent_block_root()) == head_block.parent_root; let builds_on_parent_payload = if head_is_pre_gloas { head_block .parent_root .and_then(|parent_root| fork_choice_read.get_block(&parent_root)) .is_some_and(|parent| { - parent.block_hash() == PayloadBlockHash::Hash(bid.parent_block_hash) + parent.block_hash() == PayloadBlockHash::Hash(bid.parent_block_hash()) }) } else { - head_block.execution_payload_parent_hash == Some(bid.parent_block_hash) + head_block.execution_payload_parent_hash == Some(bid.parent_block_hash()) }; if builds_on_parent_block && builds_on_parent_payload { return Ok(true); } - if bid.parent_block_root != head_block.root { + if bid.parent_block_root() != head_block.root { return Ok(false); } let builds_on_head_payload = - head_block.block_hash() == PayloadBlockHash::Hash(bid.parent_block_hash); + head_block.block_hash() == PayloadBlockHash::Hash(bid.parent_block_hash()); if head_is_pre_gloas { return Ok(builds_on_head_payload); @@ -248,7 +248,7 @@ pub(crate) fn is_bid_compatible_with_head( .should_build_on_full( &head_block_root, cached_head.head_payload_status(), - bid.slot, + bid.slot(), ) .map_err(|e| { PayloadBidError::InternalError(format!("should_build_on_full failed: {e:?}")) @@ -286,10 +286,15 @@ impl GossipVerifiedPayloadBid { where T: BeaconChainTypes, { - let bid_slot = signed_bid.message.slot; - let bid_parent = BidParent::from_bid(&signed_bid.message); - let bid_parent_block_root = signed_bid.message.parent_block_root; - let bid_value = signed_bid.message.value; + // Ensure the bid is the correct structure for the fork at `bid.slot()`. + signed_bid + .fork_name(ctx.spec) + .map_err(PayloadBidError::InconsistentFork)?; + + let bid_slot = signed_bid.message().slot(); + let bid_parent = BidParent::from_bid(signed_bid.message()); + let bid_parent_block_root = signed_bid.message().parent_block_root(); + let bid_value = signed_bid.message().value(); let current_slot = ctx .slot_clock .now() @@ -298,10 +303,14 @@ impl GossipVerifiedPayloadBid { if ctx .gossip_verified_payload_bid_cache - .seen_builder_bid_for_parent(&bid_slot, bid_parent, signed_bid.message.builder_index) + .seen_builder_bid_for_parent( + &bid_slot, + bid_parent, + signed_bid.message().builder_index(), + ) { return Err(PayloadBidError::BuilderAlreadySeen { - builder_index: signed_bid.message.builder_index, + builder_index: signed_bid.message().builder_index(), slot: bid_slot, }); } @@ -311,10 +320,10 @@ impl GossipVerifiedPayloadBid { if let Some(cached_bid) = ctx .gossip_verified_payload_bid_cache .get_highest_bid(bid_slot, bid_parent) - && bid_value <= cached_bid.message.value + && bid_value <= cached_bid.message().value() { return Err(PayloadBidError::BidValueBelowCached { - cached_value: cached_bid.message.value, + cached_value: cached_bid.message().value(), incoming_value: bid_value, }); } @@ -334,7 +343,7 @@ impl GossipVerifiedPayloadBid { }); } - verify_bid_payment_and_blobs(&signed_bid.message, ctx.spec)?; + verify_bid_payment_and_blobs(signed_bid.message(), ctx.spec)?; let parent_block = parent_block.ok_or(PayloadBidError::ParentBlockRootUnknown { parent_block_root: bid_parent_block_root, @@ -395,19 +404,19 @@ impl GossipVerifiedPayloadBid { return Err(PayloadBidError::NoProposerPreferences { slot: bid_slot }); }; - if signed_bid.message.fee_recipient != proposer_preferences.message.fee_recipient { + if signed_bid.message().fee_recipient() != proposer_preferences.message.fee_recipient { return Err(PayloadBidError::InvalidFeeRecipient); } let parent_gas_limit = ctx .observed_execution_payloads - .get_gas_limit(signed_bid.message.parent_block_hash) + .get_gas_limit(signed_bid.message().parent_block_hash()) .ok_or(PayloadBidError::ParentExecutionPayloadUnknown { - parent_block_hash: signed_bid.message.parent_block_hash, + parent_block_hash: signed_bid.message().parent_block_hash(), })?; if !is_gas_limit_target_compatible( parent_gas_limit, - signed_bid.message.gas_limit, + signed_bid.message().gas_limit(), proposer_preferences.message.target_gas_limit, )? { return Err(PayloadBidError::InvalidGasLimit); @@ -416,7 +425,7 @@ impl GossipVerifiedPayloadBid { let fork_choice = ctx.canonical_head.fork_choice_read_lock(); // TODO(gloas) should we reprocess a dropped bid when the head changes to its parent? - if !is_bid_compatible_with_head(&cached_head, &fork_choice, &signed_bid.message, ctx.spec)? + if !is_bid_compatible_with_head(&cached_head, &fork_choice, signed_bid.message(), ctx.spec)? { return Err(PayloadBidError::BidNotCompatibleWithHead { parent_block_root: bid_parent_block_root, @@ -433,27 +442,27 @@ impl GossipVerifiedPayloadBid { // Head compatibility also permits a bid on the head's parent. cached_head.parent_random()? }; - if signed_bid.message.prev_randao != expected_randao { + if signed_bid.message().prev_randao() != expected_randao { return Err(PayloadBidError::InvalidPrevRandao { slot: bid_slot }); } - verify_bid_state_conditions(&signed_bid.message, head_state, ctx.spec)?; + verify_bid_state_conditions(signed_bid.message(), head_state, ctx.spec)?; if parent_payload_exits_builder::( - &signed_bid.message, + signed_bid.message(), &parent_block, head_state, ctx.store, )? { return Err(PayloadBidError::BuilderExitPending { - builder_index: signed_bid.message.builder_index, + builder_index: signed_bid.message().builder_index(), }); } execution_payload_bid_signature_set( head_state, |i| get_builder_pubkey_from_state(head_state, i), - &signed_bid, + signed_bid.to_ref(), ctx.spec, ) .map_err(|_| PayloadBidError::BadSignature)? @@ -496,9 +505,9 @@ impl BeaconChain { &self, bid: Arc>, ) -> Result, PayloadBidError> { - let slot = bid.message.slot; - let parent_block_root = bid.message.parent_block_root; - let parent_block_hash = bid.message.parent_block_hash; + let slot = bid.message().slot(); + let parent_block_root = bid.message().parent_block_root(); + let parent_block_hash = bid.message().parent_block_hash(); let ctx = self.payload_bid_gossip_verification_context(); match GossipVerifiedPayloadBid::new(bid, &ctx) { diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs index 98ad661adf8..935db42a15b 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs @@ -10,7 +10,7 @@ //! GossipVerifiedPayloadBid -------> Insert into GossipVerifiedPayloadBidCache //! ``` -use types::{BeaconStateError, ExecutionBlockHash, Hash256, Slot}; +use types::{BeaconStateError, ExecutionBlockHash, Hash256, InconsistentFork, Slot}; pub mod direct_verified_bid; pub mod gossip_verified_bid; @@ -94,6 +94,8 @@ pub enum PayloadBidError { InvalidPrevRandao { slot: Slot }, /// Some Beacon State error BeaconStateError(BeaconStateError), + /// The bid's variant does not match the fork at its slot. + InconsistentFork(InconsistentFork), /// Internal error InternalError(String), } diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/payload_bid_cache.rs b/beacon_node/beacon_chain/src/payload_bid_verification/payload_bid_cache.rs index 27f09d9be69..01d9c878d26 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/payload_bid_cache.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/payload_bid_cache.rs @@ -7,7 +7,7 @@ use std::{ sync::Arc, }; use types::{ - BuilderIndex, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, Hash256, + BuilderIndex, EthSpec, ExecutionBlockHash, ExecutionPayloadBidRef, Hash256, SignedExecutionPayloadBid, Slot, }; @@ -19,10 +19,10 @@ pub struct BidParent { } impl BidParent { - pub fn from_bid(bid: &ExecutionPayloadBid) -> Self { + pub fn from_bid(bid: ExecutionPayloadBidRef<'_, E>) -> Self { Self { - parent_block_hash: bid.parent_block_hash, - parent_block_root: bid.parent_block_root, + parent_block_hash: bid.parent_block_hash(), + parent_block_root: bid.parent_block_root(), } } } @@ -90,14 +90,14 @@ impl GossipVerifiedPayloadBidCache { /// Returns `true` if the bid became the new highest bid for its parent, or `false` if an /// existing cached bid had an equal or greater value and was therefore retained. pub fn observe_bid(&self, bid: GossipVerifiedPayloadBid) -> bool { - let slot = bid.signed_bid.message.slot; - let key = BidParent::from_bid(&bid.signed_bid.message); + let slot = bid.signed_bid.message().slot(); + let key = BidParent::from_bid(bid.signed_bid.message()); let mut inner = self.inner.write(); inner .seen_builder_bids .entry(slot) .or_default() - .insert((key, bid.signed_bid.message.builder_index)); + .insert((key, bid.signed_bid.message().builder_index())); match inner.highest_bid.entry(slot).or_default().entry(key) { hash_map::Entry::Vacant(entry) => { @@ -105,7 +105,7 @@ impl GossipVerifiedPayloadBidCache { true } hash_map::Entry::Occupied(mut entry) => { - if entry.get().signed_bid.message.value >= bid.signed_bid.message.value { + if entry.get().signed_bid.message().value() >= bid.signed_bid.message().value() { return false; } entry.insert(bid); @@ -145,8 +145,8 @@ mod tests { use bls::Signature; use types::{ - ExecutionBlockHash, ExecutionPayloadBid, Hash256, MinimalEthSpec, - SignedExecutionPayloadBid, Slot, + ExecutionBlockHash, ExecutionPayloadBidGloas, Hash256, MinimalEthSpec, + SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, Slot, }; use super::{BidParent, GossipVerifiedPayloadBidCache}; @@ -162,17 +162,19 @@ mod tests { value: u64, ) -> GossipVerifiedPayloadBid { GossipVerifiedPayloadBid { - signed_bid: Arc::new(SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - slot, - builder_index, - parent_block_hash, - parent_block_root, - value, - ..ExecutionPayloadBid::default() + signed_bid: Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + slot, + builder_index, + parent_block_hash, + parent_block_root, + value, + ..ExecutionPayloadBidGloas::default() + }, + signature: Signature::empty(), }, - signature: Signature::empty(), - }), + )), } } @@ -227,11 +229,19 @@ mod tests { // Each parent tuple keeps its own highest bid. assert_eq!( - cache.get_highest_bid(slot, parent_a).unwrap().message.value, + cache + .get_highest_bid(slot, parent_a) + .unwrap() + .message() + .value(), 100 ); assert_eq!( - cache.get_highest_bid(slot, parent_b).unwrap().message.value, + cache + .get_highest_bid(slot, parent_b) + .unwrap() + .message() + .value(), 50 ); @@ -239,19 +249,27 @@ mod tests { // not touch the other tuple. assert!(!cache.observe_bid(make_gossip_verified(slot, 2, hash_a, root_a, 60))); assert_eq!( - cache.get_highest_bid(slot, parent_a).unwrap().message.value, + cache + .get_highest_bid(slot, parent_a) + .unwrap() + .message() + .value(), 100 ); assert_eq!( - cache.get_highest_bid(slot, parent_b).unwrap().message.value, + cache + .get_highest_bid(slot, parent_b) + .unwrap() + .message() + .value(), 50 ); // A higher bid replaces the cached bid for its tuple. assert!(cache.observe_bid(make_gossip_verified(slot, 3, hash_b, root_b, 70))); let highest_b = cache.get_highest_bid(slot, parent_b).unwrap(); - assert_eq!(highest_b.message.value, 70); - assert_eq!(highest_b.message.builder_index, 3); + assert_eq!(highest_b.message().value(), 70); + assert_eq!(highest_b.message().builder_index(), 3); } #[test] diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs index f861760c26c..a11ba912b15 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs @@ -13,10 +13,11 @@ use state_processing::genesis::genesis_block; use store::{HotColdDB, StoreConfig, StoreOp}; use types::{ Address, BuilderExitRequest, ChainSpec, Checkpoint, Domain, Epoch, EthSpec, ExecutionBlockHash, - ExecutionPayloadBid, ExecutionPayloadEnvelope, ExecutionPayloadHeader, - ExecutionPayloadHeaderFulu, Hash256, MinimalEthSpec, ProposerPreferences, SignedBeaconBlock, - SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedProposerPreferences, - SignedRoot, Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, + ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadBidRef, + ExecutionPayloadEnvelope, ExecutionPayloadHeader, ExecutionPayloadHeaderFulu, ForkName, + Hash256, MinimalEthSpec, ProposerPreferences, SignedBeaconBlock, SignedExecutionPayloadBid, + SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, SignedExecutionPayloadEnvelope, + SignedProposerPreferences, SignedRoot, Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, }; use proto_array::{Block as ProtoBlock, ExecutionStatus}; @@ -177,7 +178,8 @@ impl TestContext { let genesis_bid = state .latest_execution_payload_bid() .expect("should have a Gloas payload bid"); - observed_execution_payloads.insert(genesis_bid.parent_block_hash, genesis_bid.gas_limit); + observed_execution_payloads + .insert(genesis_bid.parent_block_hash(), genesis_bid.gas_limit()); let slot_clock = TestingSlotClock::new( Slot::new(0), @@ -199,7 +201,7 @@ impl TestContext { } } - fn sign_bid(&self, bid: ExecutionPayloadBid) -> Arc> { + fn sign_bid(&self, bid: ExecutionPayloadBidGloas) -> Arc> { let head = self.canonical_head.cached_head(); let state = &head.snapshot.beacon_state; let domain = self.spec.get_domain( @@ -208,12 +210,14 @@ impl TestContext { &state.fork(), state.genesis_validators_root(), ); - let message = bid.signing_root(domain); + let message = ExecutionPayloadBidRef::Gloas(&bid).signing_root(domain); let signature = self.keypairs[bid.builder_index as usize].sk.sign(message); - Arc::new(SignedExecutionPayloadBid { - message: bid, - signature, - }) + Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: bid, + signature, + }, + )) } fn gossip_ctx(&self) -> GossipVerificationContext<'_, T> { @@ -256,20 +260,22 @@ impl TestContext { value: u64, parent_block_root: Hash256, ) -> Arc> { - Arc::new(SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - slot, - builder_index, - fee_recipient, - gas_limit, - value, - parent_block_root, - parent_block_hash: self.execution_parent_hash(), - prev_randao: self.expected_prev_randao(), - ..ExecutionPayloadBid::default() + Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + slot, + builder_index, + fee_recipient, + gas_limit, + value, + parent_block_root, + parent_block_hash: self.execution_parent_hash(), + prev_randao: self.expected_prev_randao(), + ..ExecutionPayloadBidGloas::default() + }, + signature: Signature::empty(), }, - signature: Signature::empty(), - }) + )) } fn slot_1_proto_block( @@ -447,7 +453,7 @@ fn same_builder_new_parent_tuple_not_blocked() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = ctx.sign_bid(ExecutionPayloadBid { + let bid = ctx.sign_bid(ExecutionPayloadBidGloas { slot, builder_index: 0, fee_recipient: Address::ZERO, @@ -456,7 +462,7 @@ fn same_builder_new_parent_tuple_not_blocked() { parent_block_root: ctx.genesis_block_root, parent_block_hash: ctx.execution_parent_hash(), prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!( @@ -526,6 +532,52 @@ fn invalid_bid_slot() { )); } +#[test] +fn inconsistent_fork() { + // A Heze bid is only inconsistent with the slot's fork before Heze. + if fork_name_from_env() != Some(ForkName::Gloas) { + return; + } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + + let bid = Arc::new(SignedExecutionPayloadBid::Heze( + SignedExecutionPayloadBidHeze { + message: ExecutionPayloadBidHeze { + slot: Slot::new(1), + ..ExecutionPayloadBidHeze::default() + }, + signature: Signature::empty(), + }, + )); + let result = GossipVerifiedPayloadBid::new(bid, &gossip); + assert!(matches!(result, Err(PayloadBidError::InconsistentFork(_)))); +} + +#[test] +fn inconsistent_fork_at_heze_boundary() { + // A Gloas bid for the first Heze slot, + // possible from a peer still on the Gloas topic. + if fork_name_from_env() != Some(ForkName::Gloas) { + return; + } + let mut ctx = TestContext::new(); + let heze_fork_epoch = Epoch::new(1); + ctx.spec.heze_fork_epoch = Some(heze_fork_epoch); + let gossip = ctx.gossip_ctx(); + + let bid = ctx.make_signed_bid( + heze_fork_epoch.start_slot(E::slots_per_epoch()), + 0, + Address::ZERO, + 30_000_000, + 100, + ctx.genesis_block_root, + ); + let result = GossipVerifiedPayloadBid::new(bid, &gossip); + assert!(matches!(result, Err(PayloadBidError::InconsistentFork(_)))); +} + #[test] fn fee_recipient_mismatch() { if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { @@ -558,7 +610,7 @@ fn gas_limit_mismatch() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = ctx.sign_bid(ExecutionPayloadBid { + let bid = ctx.sign_bid(ExecutionPayloadBidGloas { slot, builder_index: 0, fee_recipient: Address::ZERO, @@ -567,7 +619,7 @@ fn gas_limit_mismatch() { parent_block_root: ctx.genesis_block_root, parent_block_hash: ctx.execution_parent_hash(), prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!(matches!(result, Err(PayloadBidError::InvalidGasLimit))); @@ -616,18 +668,20 @@ fn block_hash_equals_parent_block_hash() { seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); let parent_block_hash = ctx.execution_parent_hash(); - let bid = Arc::new(SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - slot, - gas_limit: 30_000_000, - parent_block_root: ctx.genesis_block_root, - parent_block_hash, - block_hash: parent_block_hash, - prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + let bid = Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + slot, + gas_limit: 30_000_000, + parent_block_root: ctx.genesis_block_root, + parent_block_hash, + block_hash: parent_block_hash, + prev_randao: ctx.expected_prev_randao(), + ..ExecutionPayloadBidGloas::default() + }, + signature: Signature::empty(), }, - signature: Signature::empty(), - }); + )); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!(matches!( result, @@ -643,12 +697,12 @@ fn exit_test_parent_payload_hash() -> ExecutionBlockHash { ExecutionBlockHash::repeat_byte(0xab) } -fn exit_test_bid(parent_block_hash: ExecutionBlockHash) -> ExecutionPayloadBid { - ExecutionPayloadBid { +fn exit_test_bid(parent_block_hash: ExecutionBlockHash) -> ExecutionPayloadBidGloas { + ExecutionPayloadBidGloas { builder_index: 0, parent_block_root: exit_test_parent_root(), parent_block_hash, - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() } } @@ -672,7 +726,12 @@ fn parent_payload_exits_builder_on_matching_exit_request() { ); let bid = exit_test_bid(exit_test_parent_payload_hash()); - let result = parent_payload_exits_builder::(&bid, &parent_block, head_state, &ctx.store); + let result = parent_payload_exits_builder::( + ExecutionPayloadBidRef::Gloas(&bid), + &parent_block, + head_state, + &ctx.store, + ); assert!(matches!(result, Ok(true)), "got: {result:?}"); } @@ -696,7 +755,12 @@ fn parent_payload_exit_from_other_address_does_not_exit_builder() { ); let bid = exit_test_bid(exit_test_parent_payload_hash()); - let result = parent_payload_exits_builder::(&bid, &parent_block, head_state, &ctx.store); + let result = parent_payload_exits_builder::( + ExecutionPayloadBidRef::Gloas(&bid), + &parent_block, + head_state, + &ctx.store, + ); assert!(matches!(result, Ok(false)), "got: {result:?}"); } @@ -712,7 +776,12 @@ fn parent_payload_exit_check_skipped_when_bid_builds_on_empty_parent() { ctx.slot_1_proto_block(exit_test_parent_root(), exit_test_parent_payload_hash()); let bid = exit_test_bid(ExecutionBlockHash::repeat_byte(0x11)); - let result = parent_payload_exits_builder::(&bid, &parent_block, head_state, &ctx.store); + let result = parent_payload_exits_builder::( + ExecutionPayloadBidRef::Gloas(&bid), + &parent_block, + head_state, + &ctx.store, + ); assert!(matches!(result, Ok(false)), "got: {result:?}"); } @@ -741,7 +810,12 @@ fn parent_payload_exit_check_uses_summary_after_body_pruning() { .expect("should prune payload body"); let bid = exit_test_bid(exit_test_parent_payload_hash()); - let result = parent_payload_exits_builder::(&bid, &parent_block, head_state, &ctx.store); + let result = parent_payload_exits_builder::( + ExecutionPayloadBidRef::Gloas(&bid), + &parent_block, + head_state, + &ctx.store, + ); assert!(matches!(result, Ok(true)), "got: {result:?}"); } @@ -757,7 +831,12 @@ fn parent_payload_exit_check_needs_parent_envelope() { ctx.slot_1_proto_block(exit_test_parent_root(), exit_test_parent_payload_hash()); let bid = exit_test_bid(exit_test_parent_payload_hash()); - let result = parent_payload_exits_builder::(&bid, &parent_block, head_state, &ctx.store); + let result = parent_payload_exits_builder::( + ExecutionPayloadBidRef::Gloas(&bid), + &parent_block, + head_state, + &ctx.store, + ); assert!( matches!( result, @@ -777,18 +856,20 @@ fn execution_payment_nonzero() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = Arc::new(SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - slot, - gas_limit: 30_000_000, - execution_payment: 42, - parent_block_root: ctx.genesis_block_root, - parent_block_hash: ctx.execution_parent_hash(), - prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + let bid = Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + slot, + gas_limit: 30_000_000, + execution_payment: 42, + parent_block_root: ctx.genesis_block_root, + parent_block_hash: ctx.execution_parent_hash(), + prev_randao: ctx.expected_prev_randao(), + ..ExecutionPayloadBidGloas::default() + }, + signature: Signature::empty(), }, - signature: Signature::empty(), - }); + )); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!(matches!( result, @@ -897,7 +978,7 @@ fn bid_state_conditions_reject_uncoverable_bid() { 100, ctx.genesis_block_root, ); - assert!(verify_bid_state_conditions(&coverable.message, state, &ctx.spec).is_ok()); + assert!(verify_bid_state_conditions(coverable.message(), state, &ctx.spec).is_ok()); let uncoverable = ctx.make_signed_bid( slot, @@ -908,7 +989,7 @@ fn bid_state_conditions_reject_uncoverable_bid() { ctx.genesis_block_root, ); assert!(matches!( - verify_bid_state_conditions(&uncoverable.message, state, &ctx.spec), + verify_bid_state_conditions(uncoverable.message(), state, &ctx.spec), Err(PayloadBidError::BuilderCantCoverBid { .. }) )); } @@ -931,7 +1012,7 @@ fn bid_state_conditions_reject_inactive_builder_before_coverage() { ctx.genesis_block_root, ); assert!(matches!( - verify_bid_state_conditions(&bid.message, state, &ctx.spec), + verify_bid_state_conditions(bid.message(), state, &ctx.spec), Err(PayloadBidError::InvalidBuilder { .. }) )); } @@ -966,7 +1047,7 @@ fn bid_state_conditions_reject_wrong_version_before_coverage() { ctx.genesis_block_root, ); assert!(matches!( - verify_bid_state_conditions(&bid.message, &state, &ctx.spec), + verify_bid_state_conditions(bid.message(), &state, &ctx.spec), Err(PayloadBidError::InvalidBuilderVersion { .. }) )); } @@ -1064,21 +1145,23 @@ fn invalid_blob_kzg_commitments() { .map(|_| KzgCommitment::empty_for_testing()) .collect(); - let bid = Arc::new(SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - slot, - builder_index: 0, - fee_recipient: Address::ZERO, - gas_limit: 30_000_000, - value: 0, - parent_block_root: ctx.genesis_block_root, - parent_block_hash: ctx.execution_parent_hash(), - prev_randao: ctx.expected_prev_randao(), - blob_kzg_commitments: ProgressiveVariableList::new(commitments).unwrap(), - ..ExecutionPayloadBid::default() + let bid = Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + slot, + builder_index: 0, + fee_recipient: Address::ZERO, + gas_limit: 30_000_000, + value: 0, + parent_block_root: ctx.genesis_block_root, + parent_block_hash: ctx.execution_parent_hash(), + prev_randao: ctx.expected_prev_randao(), + blob_kzg_commitments: ProgressiveVariableList::new(commitments).unwrap(), + ..ExecutionPayloadBidGloas::default() + }, + signature: Signature::empty(), }, - signature: Signature::empty(), - }); + )); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!(matches!( result, @@ -1105,7 +1188,7 @@ fn bad_signature() { 0, ctx.genesis_block_root, ); - let bid_parent = BidParent::from_bid(&bid.message); + let bid_parent = BidParent::from_bid(bid.message()); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!(matches!(result, Err(PayloadBidError::BadSignature))); assert!( @@ -1125,7 +1208,7 @@ fn valid_bid_after_empty_genesis_uses_parent_payload_gas_limit() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = ctx.sign_bid(ExecutionPayloadBid { + let bid = ctx.sign_bid(ExecutionPayloadBidGloas { slot, builder_index: 0, fee_recipient: Address::ZERO, @@ -1134,7 +1217,7 @@ fn valid_bid_after_empty_genesis_uses_parent_payload_gas_limit() { parent_block_root: ctx.genesis_block_root, parent_block_hash: ctx.execution_parent_hash(), prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!( @@ -1160,7 +1243,7 @@ fn valid_bid_with_parent_in_previous_epoch() { let slot = Slot::new(bid_slot); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); let parent_state = &ctx.canonical_head.cached_head().snapshot.beacon_state; - let bid = ctx.sign_bid(ExecutionPayloadBid { + let bid = ctx.sign_bid(ExecutionPayloadBidGloas { slot, builder_index: 0, fee_recipient: Address::ZERO, @@ -1170,7 +1253,7 @@ fn valid_bid_with_parent_in_previous_epoch() { prev_randao: *parent_state .get_randao_mix(parent_state.current_epoch()) .unwrap(), - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }); let result = GossipVerifiedPayloadBid::new(bid, &ctx.gossip_ctx()); assert!( @@ -1190,7 +1273,7 @@ fn two_builders_coexist_in_cache() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid_0 = ctx.sign_bid(ExecutionPayloadBid { + let bid_0 = ctx.sign_bid(ExecutionPayloadBidGloas { slot, builder_index: 0, fee_recipient: Address::ZERO, @@ -1199,7 +1282,7 @@ fn two_builders_coexist_in_cache() { parent_block_root: ctx.genesis_block_root, parent_block_hash: ctx.execution_parent_hash(), prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }); let result_0 = GossipVerifiedPayloadBid::new(bid_0, &gossip); assert!( @@ -1209,7 +1292,7 @@ fn two_builders_coexist_in_cache() { ); // Builder 1 must bid strictly higher than builder 0's cached value. - let bid_1 = ctx.sign_bid(ExecutionPayloadBid { + let bid_1 = ctx.sign_bid(ExecutionPayloadBidGloas { slot, builder_index: 1, fee_recipient: Address::ZERO, @@ -1218,7 +1301,7 @@ fn two_builders_coexist_in_cache() { parent_block_root: ctx.genesis_block_root, parent_block_hash: ctx.execution_parent_hash(), prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }); let result_1 = GossipVerifiedPayloadBid::new(bid_1, &gossip); assert!( @@ -1245,8 +1328,8 @@ fn two_builders_coexist_in_cache() { .bid_cache .get_highest_bid(slot, bid_parent) .expect("should have highest bid"); - assert_eq!(highest.message.value, 1); - assert_eq!(highest.message.builder_index, 1); + assert_eq!(highest.message().value(), 1); + assert_eq!(highest.message().builder_index(), 1); } #[test] diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs index f3b1faae2d3..fcfa5d8c595 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs @@ -7,8 +7,9 @@ use store::DatabaseBlock; use tracing::debug; use tree_hash::TreeHash; use types::{ - ChainSpec, EthSpec, ExecutionPayloadBid, ExecutionPayloadEnvelope, Hash256, SignedBeaconBlock, - SignedExecutionPayloadEnvelope, Slot, consts::gloas::BUILDER_INDEX_SELF_BUILD, + ChainSpec, EthSpec, ExecutionPayloadBidRef, ExecutionPayloadEnvelope, Hash256, + SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, + consts::gloas::BUILDER_INDEX_SELF_BUILD, }; use crate::payload_envelope_verification::observed_payload_envelopes::ObservedPayloadEnvelopes; @@ -45,7 +46,7 @@ pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { pub(crate) fn verify_envelope_consistency( envelope: &ExecutionPayloadEnvelope, block: &SignedBeaconBlock, - execution_bid: &ExecutionPayloadBid, + execution_bid: ExecutionPayloadBidRef, latest_finalized_slot: Slot, ) -> Result<(), EnvelopeError> { // Check that the envelope's slot isn't from a slot prior @@ -66,17 +67,17 @@ pub(crate) fn verify_envelope_consistency( } // Builder index matches committed bid. - if envelope.builder_index != execution_bid.builder_index { + if envelope.builder_index != execution_bid.builder_index() { return Err(EnvelopeError::BuilderIndexMismatch { - committed_bid: execution_bid.builder_index, + committed_bid: execution_bid.builder_index(), envelope: envelope.builder_index, }); } // The block hash should match the block hash of the execution bid. - if envelope.payload.block_hash != execution_bid.block_hash { + if envelope.payload.block_hash != execution_bid.block_hash() { return Err(EnvelopeError::BlockHashMismatch { - committed_bid: execution_bid.block_hash, + committed_bid: execution_bid.block_hash(), envelope: envelope.payload.block_hash, }); } @@ -121,9 +122,9 @@ pub(crate) fn verify_envelope_consistency( // The SSZ root of the envelope's execution requests must match the committed bid, per // `verify_execution_payload_envelope` in the spec. let execution_requests_root = requests.tree_hash_root(); - if execution_requests_root != execution_bid.execution_requests_root { + if execution_requests_root != execution_bid.execution_requests_root() { return Err(EnvelopeError::ExecutionRequestsRootMismatch { - committed_bid: execution_bid.execution_requests_root, + committed_bid: execution_bid.execution_requests_root(), envelope: execution_requests_root, }); } @@ -200,11 +201,11 @@ impl GossipVerifiedEnvelope { ))); } }; - let execution_bid = &block + let execution_bid = block .message() .body() .signed_execution_payload_bid()? - .message; + .message(); verify_envelope_consistency(envelope, &block, execution_bid, latest_finalized_slot)?; @@ -421,9 +422,10 @@ mod tests { use types::{ Address, BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, BuilderDepositRequest, BuilderExitRequest, ConsolidationRequest, Eth1Data, EthSpec, ExecutionBlockHash, - ExecutionPayloadBid, ExecutionPayloadEnvelope, ExecutionPayloadGloas, - ExecutionRequestsGloas, Graffiti, Hash256, MinimalEthSpec, SignedBeaconBlock, - SignedExecutionPayloadBid, Slot, SyncAggregate, Withdrawal, WithdrawalRequest, + ExecutionPayloadBid, ExecutionPayloadBidGloas, ExecutionPayloadBidRef, + ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequestsGloas, Graffiti, Hash256, + MinimalEthSpec, SignedBeaconBlock, SignedExecutionPayloadBidGloas, Slot, SyncAggregate, + Withdrawal, WithdrawalRequest, }; use super::verify_envelope_consistency; @@ -472,7 +474,7 @@ mod tests { sync_aggregate: SyncAggregate::empty(), bls_to_execution_changes: ProgressiveVariableList::empty(), parent_execution_requests: ExecutionRequestsGloas::default(), - signed_execution_payload_bid: SignedExecutionPayloadBid::empty(), + signed_execution_payload_bid: SignedExecutionPayloadBidGloas::empty(), payload_attestations: ProgressiveVariableList::empty(), _phantom: PhantomData, }, @@ -481,13 +483,13 @@ mod tests { } fn make_bid(builder_index: u64, block_hash: ExecutionBlockHash) -> ExecutionPayloadBid { - ExecutionPayloadBid { + ExecutionPayloadBid::Gloas(ExecutionPayloadBidGloas { builder_index, block_hash, // Commit to the (default) execution requests carried by `make_envelope`. execution_requests_root: ExecutionRequestsGloas::::default().tree_hash_root(), - ..ExecutionPayloadBid::default() - } + ..ExecutionPayloadBidGloas::default() + }) } #[test] @@ -500,7 +502,9 @@ mod tests { let block = make_block(slot); let bid = make_bid(builder_index, block_hash); - assert!(verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)).is_ok()); + assert!( + verify_envelope_consistency::(&envelope, &block, bid.to_ref(), Slot::new(0)).is_ok() + ); } #[test] @@ -514,8 +518,12 @@ mod tests { let bid = make_bid(builder_index, block_hash); let latest_finalized_slot = Slot::new(10); - let result = - verify_envelope_consistency::(&envelope, &block, &bid, latest_finalized_slot); + let result = verify_envelope_consistency::( + &envelope, + &block, + bid.to_ref(), + latest_finalized_slot, + ); assert!(matches!( result, Err(EnvelopeError::PriorToFinalization { .. }) @@ -531,7 +539,8 @@ mod tests { let block = make_block(Slot::new(20)); let bid = make_bid(builder_index, block_hash); - let result = verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)); + let result = + verify_envelope_consistency::(&envelope, &block, bid.to_ref(), Slot::new(0)); assert!(matches!(result, Err(EnvelopeError::SlotMismatch { .. }))); } @@ -544,7 +553,8 @@ mod tests { let block = make_block(slot); let bid = make_bid(2, block_hash); - let result = verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)); + let result = + verify_envelope_consistency::(&envelope, &block, bid.to_ref(), Slot::new(0)); assert!(matches!( result, Err(EnvelopeError::BuilderIndexMismatch { .. }) @@ -566,7 +576,9 @@ mod tests { // Not a gossip condition; the sync-only rejection is inlined in // `RangeSyncBlock` construction. - assert!(verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)).is_ok()); + assert!( + verify_envelope_consistency::(&envelope, &block, bid.to_ref(), Slot::new(0)).is_ok() + ); } #[test] @@ -578,7 +590,8 @@ mod tests { let block = make_block(slot); let bid = make_bid(builder_index, ExecutionBlockHash::repeat_byte(0xff)); - let result = verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)); + let result = + verify_envelope_consistency::(&envelope, &block, bid.to_ref(), Slot::new(0)); assert!(matches!( result, Err(EnvelopeError::BlockHashMismatch { .. }) @@ -604,7 +617,9 @@ mod tests { let max = E::max_withdrawals_per_payload(); envelope.payload.withdrawals = ProgressiveVariableList::new(vec![withdrawal.clone(); max]).unwrap(); - assert!(verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)).is_ok()); + assert!( + verify_envelope_consistency::(&envelope, &block, bid.to_ref(), Slot::new(0)).is_ok() + ); assert!( ProgressiveVariableList::::MaxWithdrawalsPerPayload>::new( @@ -627,14 +642,20 @@ mod tests { let block = make_block(slot); set_len(&mut envelope.execution_requests, max).unwrap(); - let bid = ExecutionPayloadBid { + let bid = ExecutionPayloadBidGloas { builder_index, block_hash, execution_requests_root: envelope.execution_requests.tree_hash_root(), - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }; assert!( - verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)).is_ok(), + verify_envelope_consistency::( + &envelope, + &block, + ExecutionPayloadBidRef::Gloas(&bid), + Slot::new(0) + ) + .is_ok(), "{kind} at max should be accepted" ); diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs index b86e6fd8753..4013bb27525 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -61,7 +61,7 @@ impl BeaconChain { .message() .body() .signed_execution_payload_bid()? - .clone(), + .clone_as_signed_execution_payload_bid(), ); // Set observed time if not already set. Usually this should be set by gossip or RPC, diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs index 885e8a37acc..c66fbdbb731 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -30,7 +30,7 @@ use strum::AsRefStr; use tracing::{instrument, warn}; use types::{ BeaconState, BeaconStateError, BuilderIndex, DataColumnSidecarList, EthSpec, - ExecutionBlockHash, ExecutionPayloadEnvelope, Hash256, SignedExecutionPayloadBid, + ExecutionBlockHash, ExecutionPayloadEnvelope, Hash256, SignedExecutionPayloadBidRef, SignedExecutionPayloadEnvelope, Slot, }; @@ -70,7 +70,7 @@ impl AvailableEnvelope { pub fn new( envelope: Arc>, columns: DataColumnSidecarList, - bid: &SignedExecutionPayloadBid, + bid: SignedExecutionPayloadBidRef, custody_context: &CustodyContext, ) -> Result where @@ -409,10 +409,10 @@ pub fn build_new_payload_request<'a, E: EthSpec>( .body() .signed_execution_payload_bid() .map_err(|e| EnvelopeError::BeaconChainError(Box::new(BeaconChainError::from(e))))? - .message; + .message(); let versioned_hashes = bid - .blob_kzg_commitments + .blob_kzg_commitments() .iter() .map(state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash) .collect(); @@ -452,7 +452,7 @@ mod payload_hash_tests { use types::{ BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, Eth1Data, ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionPayloadRef, ExecutionRequestsGloas, ExecutionRequestsRef, - Graffiti, Hash256, MinimalEthSpec, SignedBeaconBlock, SignedExecutionPayloadBid, + Graffiti, Hash256, MinimalEthSpec, SignedBeaconBlock, SignedExecutionPayloadBidGloas, SignedExecutionPayloadEnvelope, Slot, SyncAggregate, }; @@ -480,7 +480,7 @@ mod payload_hash_tests { sync_aggregate: SyncAggregate::empty(), bls_to_execution_changes: ProgressiveVariableList::empty(), parent_execution_requests: ExecutionRequestsGloas::default(), - signed_execution_payload_bid: SignedExecutionPayloadBid::empty(), + signed_execution_payload_bid: SignedExecutionPayloadBidGloas::empty(), payload_attestations: ProgressiveVariableList::empty(), _phantom: PhantomData, }, diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs index b16de37d331..ce5667fcece 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs @@ -72,15 +72,15 @@ impl PayloadNotifier { envelope: &'a SignedExecutionPayloadEnvelope, block: &'a SignedBeaconBlock, ) -> Result, PayloadVerificationError> { - let bid = &block + let bid = block .message() .body() .signed_execution_payload_bid() .map_err(|e| PayloadVerificationError::BeaconChainError(Box::new(e.into())))? - .message; + .message(); let versioned_hashes = bid - .blob_kzg_commitments + .blob_kzg_commitments() .iter() .map(kzg_commitment_to_versioned_hash) .collect(); diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index c2509eb1d79..09bd2115667 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -282,12 +282,12 @@ impl PendingPayloadCache { ) -> Result, AvailabilityCheckError> { let kzg_verified_columns = KzgVerifiedDataColumn::from_batch_with_scoring_and_commitments( custody_columns, - &bid.message.blob_kzg_commitments, + bid.message().blob_kzg_commitments(), &self.kzg, ) .map_err(AvailabilityCheckError::InvalidColumn)?; - let epoch = bid.message.slot.epoch(T::EthSpec::slots_per_epoch()); + let epoch = bid.message().slot().epoch(T::EthSpec::slots_per_epoch()); let sampling_columns = self.custody_context.sampling_columns_for_epoch(epoch); let verified_custody_columns = kzg_verified_columns .into_iter() @@ -313,7 +313,7 @@ impl PendingPayloadCache { let bid = self .get_bid(&block_root) .ok_or(AvailabilityCheckError::MissingBid(block_root))?; - let epoch = bid.message.slot.epoch(T::EthSpec::slots_per_epoch()); + let epoch = bid.message().slot().epoch(T::EthSpec::slots_per_epoch()); let sampling_columns = self.custody_context.sampling_columns_for_epoch(epoch); let custody_columns = data_columns .into_iter() @@ -446,7 +446,7 @@ impl PendingPayloadCache { let all_data_columns = KzgVerifiedCustodyDataColumn::reconstruct_columns( &self.kzg, verified_data_columns, - &bid.message.blob_kzg_commitments, + bid.message().blob_kzg_commitments(), &self.spec, ) .map_err(|e| { @@ -460,7 +460,7 @@ impl PendingPayloadCache { AvailabilityCheckError::ReconstructColumnsError(e) })?; - let slot = bid.message.slot; + let slot = bid.message().slot(); let columns_to_sample = self .custody_context() .sampling_columns_for_epoch(slot.epoch(T::EthSpec::slots_per_epoch())); @@ -701,11 +701,11 @@ mod data_availability_checker_tests { .body() .signed_execution_payload_bid() .expect("Gloas block has bid") - .clone(), + .clone_as_signed_execution_payload_bid(), ); cache.insert_bid(block_root, bid.clone()); - let epoch = bid.message.slot.epoch(E::slots_per_epoch()); + let epoch = bid.message().slot().epoch(E::slots_per_epoch()); let sampling = cache.custody_context().sampling_columns_for_epoch(epoch); let custody = columns .into_iter() @@ -962,7 +962,7 @@ mod data_availability_checker_tests { async fn merge_partial_columns_completes_column_across_arrivals() { let s = setup_with(NodeCustodyType::Fullnode, NumBlobs::Number(2)); let bid = s.cache.get_bid(&s.block_root).expect("bid"); - let slot = bid.message.slot; + let slot = bid.message().slot(); s.cache.availability_cache.write().remove(&s.block_root); assert!(s.cache.get_bid(&s.block_root).is_none()); diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index e2808b50c56..988566a9185 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -40,7 +40,7 @@ impl PendingComponents { where T: BeaconChainTypes, { - if custody_context.data_columns_required_for_bid(&self.bid) { + if custody_context.data_columns_required_for_bid(self.bid.to_ref()) { custody_context.num_of_data_columns_to_sample(self.bid.epoch()) } else { 0 @@ -49,7 +49,7 @@ impl PendingComponents { /// Returns columns that have all cells present. pub fn get_cached_data_columns(&self) -> Vec>> { - let slot = self.bid.message.slot; + let slot = self.bid.message().slot(); let block_root = self.block_root; self.verified_data_columns .iter() @@ -59,7 +59,7 @@ impl PendingComponents { /// Returns the indices of columns that have all cells present. pub fn get_cached_data_columns_indices(&self) -> Vec { - let slot = self.bid.message.slot; + let slot = self.bid.message().slot(); let block_root = self.block_root; self.verified_data_columns .iter() @@ -76,7 +76,7 @@ impl PendingComponents { &self, ) -> Vec> { let block_root = self.block_root; - let slot = self.bid.message.slot; + let slot = self.bid.slot(); self.verified_data_columns .iter() .filter_map(|(idx, col)| { @@ -162,7 +162,7 @@ impl PendingComponents { outcome: PartialColumnsMergeOutcome, disable_get_blobs: bool, ) -> PartialMergeResult { - let slot = self.bid.message.slot; + let slot = self.bid.slot(); let full_columns = outcome .newly_complete @@ -268,8 +268,12 @@ impl PendingComponents { } }; - let available_envelope = - AvailableEnvelope::new(envelope.clone(), columns, &self.bid, custody_context)?; + let available_envelope = AvailableEnvelope::new( + envelope.clone(), + columns, + self.bid.to_ref(), + custody_context, + )?; Ok(Some(AvailableExecutedEnvelope { envelope: available_envelope, diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 4fd3b4eb018..33651b85fcc 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -2997,30 +2997,28 @@ where .is_ok_and(|c| !c.is_empty()); let is_available = !has_blob_commitments || blob_items.is_some(); let block_hash: SignedBeaconBlockHash = if !is_available { - self.chain - .process_block( - block_root, - LookupBlock::new(block), - NotifyExecutionLayer::Yes, - BlockImportSource::Lookup, - || Ok(()), - ) - .await? - .try_into() - .expect("block blobs are available") + Box::pin(self.chain.process_block( + block_root, + LookupBlock::new(block), + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + )) + .await? + .try_into() + .expect("block blobs are available") } else { let range_sync_block = self.build_range_sync_block_from_blobs(block, blob_items)?; - self.chain - .process_block( - block_root, - range_sync_block, - NotifyExecutionLayer::Yes, - BlockImportSource::RangeSync, - || Ok(()), - ) - .await? - .try_into() - .expect("block blobs are available") + Box::pin(self.chain.process_block( + block_root, + range_sync_block, + NotifyExecutionLayer::Yes, + BlockImportSource::RangeSync, + || Ok(()), + )) + .await? + .try_into() + .expect("block blobs are available") }; self.chain.recompute_head_at_current_slot().await; @@ -3044,29 +3042,27 @@ where let is_available = !has_blob_commitments || blob_items.is_some(); let block_hash: SignedBeaconBlockHash = if is_available { let range_sync_block = self.build_range_sync_block_from_blobs(block, blob_items)?; - self.chain - .process_block( - block_root, - range_sync_block, - NotifyExecutionLayer::Yes, - BlockImportSource::RangeSync, - || Ok(()), - ) - .await? - .try_into() - .expect("block blobs are available") + Box::pin(self.chain.process_block( + block_root, + range_sync_block, + NotifyExecutionLayer::Yes, + BlockImportSource::RangeSync, + || Ok(()), + )) + .await? + .try_into() + .expect("block blobs are available") } else { - self.chain - .process_block( - block_root, - LookupBlock::new(block), - NotifyExecutionLayer::Yes, - BlockImportSource::Lookup, - || Ok(()), - ) - .await? - .try_into() - .expect("block blobs are available") + Box::pin(self.chain.process_block( + block_root, + LookupBlock::new(block), + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + )) + .await? + .try_into() + .expect("block blobs are available") }; self.chain.recompute_head_at_current_slot().await; @@ -3104,15 +3100,15 @@ where .expect("should read block from store") .expect("block should exist in store"); - let bid = &block + let bid = block .message() .body() .signed_execution_payload_bid() .expect("Gloas block should have a payload bid") - .message; + .message(); let versioned_hashes = bid - .blob_kzg_commitments + .blob_kzg_commitments() .iter() .map(kzg_commitment_to_versioned_hash) .collect(); @@ -3421,7 +3417,7 @@ where > { self.set_current_slot(slot); let (block_contents, opt_envelope, new_state) = - self.make_block_with_envelope(state, slot).await; + Box::pin(self.make_block_with_envelope(state, slot)).await; let block_hash = self .process_block( @@ -4218,13 +4214,13 @@ pub fn generate_data_column_sidecars_from_block( // Load the precomputed column sidecar to avoid computing them for every block in the tests. // Then repeat the cells and proofs for every blob if block.fork_name_unchecked().gloas_enabled() { - let kzg_commitments = &block + let kzg_commitments = block .message() .body() .signed_execution_payload_bid() .expect("Gloas block should have a payload bid") - .message - .blob_kzg_commitments; + .message() + .blob_kzg_commitments(); if kzg_commitments.is_empty() { return vec![]; } diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index c3ff55da024..e081c5b0fda 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -3066,7 +3066,6 @@ async fn process_chain_segment_rejects_envelope_with_invalid_signature() { .body() .signed_execution_payload_bid() .unwrap(); - let available_envelope = AvailableEnvelope::new( Arc::new(envelope), columns, diff --git a/beacon_node/beacon_chain/tests/column_verification.rs b/beacon_node/beacon_chain/tests/column_verification.rs index 180e187e900..aebd3c20842 100644 --- a/beacon_node/beacon_chain/tests/column_verification.rs +++ b/beacon_node/beacon_chain/tests/column_verification.rs @@ -158,7 +158,7 @@ async fn gloas_envelope_blobs_produce_valid_columns() { .signed_execution_payload_bid() .expect("Gloas block should have a payload bid"); assert!( - !bid.message.blob_kzg_commitments.is_empty(), + !bid.message().blob_kzg_commitments().is_empty(), "Block should have blob KZG commitments" ); diff --git a/beacon_node/beacon_chain/tests/envelope_verification.rs b/beacon_node/beacon_chain/tests/envelope_verification.rs index 2f5b07f21e8..04fcdd025e4 100644 --- a/beacon_node/beacon_chain/tests/envelope_verification.rs +++ b/beacon_node/beacon_chain/tests/envelope_verification.rs @@ -98,8 +98,8 @@ async fn startup_seeds_gloas_genesis_parent_payload() { harness .chain .observed_execution_payloads - .get_gas_limit(genesis_bid.parent_block_hash), - Some(genesis_bid.gas_limit) + .get_gas_limit(genesis_bid.parent_block_hash()), + Some(genesis_bid.gas_limit()) ); } diff --git a/beacon_node/beacon_chain/tests/events.rs b/beacon_node/beacon_chain/tests/events.rs index bcb378783ea..991360098b6 100644 --- a/beacon_node/beacon_chain/tests/events.rs +++ b/beacon_node/beacon_chain/tests/events.rs @@ -10,7 +10,8 @@ use types::data::FixedBlobSidecarList; use types::{ Address, BlobSidecar, DataColumnSidecar, DataColumnSidecarFulu, DataColumnSidecarGloas, Domain, EthSpec, MinimalEthSpec, PayloadAttestationData, PayloadAttestationMessage, - ProposerPreferences, SignedExecutionPayloadBid, SignedProposerPreferences, SignedRoot, Slot, + ProposerPreferences, SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, + SignedProposerPreferences, SignedRoot, Slot, }; type E = MinimalEthSpec; @@ -50,12 +51,12 @@ async fn data_column_sidecar_event_on_process_gossip_data_column() { .sampling_columns_for_epoch(epoch)[0]; // For gloas, the bid must be known, e.g. in the pending payload cache - let mut bid = SignedExecutionPayloadBid::::empty(); + let mut bid = SignedExecutionPayloadBidGloas::::empty(); bid.message.slot = Slot::new(10); - harness - .chain - .pending_payload_cache - .insert_bid(random_sidecar.beacon_block_root, Arc::new(bid)); + harness.chain.pending_payload_cache.insert_bid( + random_sidecar.beacon_block_root, + Arc::new(SignedExecutionPayloadBid::Gloas(bid)), + ); DataColumnSidecar::Gloas(random_sidecar) } else { diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index c8dfefffca9..20522780cff 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -96,8 +96,8 @@ impl InvalidPayloadRig { .body() .signed_execution_payload_bid() .unwrap() - .message - .block_hash + .message() + .block_hash() } fn execution_status(&self, block_root: Hash256) -> ExecutionStatus { diff --git a/beacon_node/beacon_chain/tests/prepare_payload.rs b/beacon_node/beacon_chain/tests/prepare_payload.rs index 77c2db0e7cd..b2913f48224 100644 --- a/beacon_node/beacon_chain/tests/prepare_payload.rs +++ b/beacon_node/beacon_chain/tests/prepare_payload.rs @@ -141,7 +141,7 @@ async fn gloas_block_production_parent_root_with_unadvanced_state() { let parent_slot = parent_state.slot(); let slot = parent_slot + 1; let parent_bid = parent_state.latest_execution_payload_bid().unwrap(); - assert_ne!(parent_bid.block_hash, parent_bid.parent_block_hash); + assert_ne!(parent_bid.block_hash(), parent_bid.parent_block_hash()); // The head's full branch has attestation weight, but negative PTC votes should make the // next proposer build on empty. Looking up the grandparent (as the buggy code did) instead @@ -214,13 +214,13 @@ async fn gloas_block_production_parent_root_with_unadvanced_state() { .body() .signed_execution_payload_bid() .unwrap() - .message - .parent_block_hash, - parent_bid.parent_block_hash, + .message() + .parent_block_hash(), + parent_bid.parent_block_hash(), ); let (envelope, _, _) = payload_contents.unwrap(); assert_eq!(envelope.parent_beacon_block_root, parent_root); - assert_eq!(envelope.payload.parent_hash, parent_bid.parent_block_hash); + assert_eq!(envelope.payload.parent_hash, parent_bid.parent_block_hash()); } } @@ -551,8 +551,8 @@ async fn prepare_payload_generic( .body() .signed_execution_payload_bid() .unwrap() - .message - .parent_block_hash; + .message() + .parent_block_hash(); assert_eq!( produced_execution_parent_hash, expected_execution_parent_hash, "block production should independently select the {parent_payload_status:?} parent" diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 6bafe4819b0..75fa094a049 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -4377,19 +4377,15 @@ async fn weak_subjectivity_sync_test( info!(block_root = ?full_block_root, ?state_root, %slot, "Importing block from chain dump"); beacon_chain.slot_clock.set_slot(slot.as_u64()); - beacon_chain - .process_block( - full_block_root, - harness.build_range_sync_block_from_store_blobs( - Some(block_root), - Arc::new(full_block), - ), - NotifyExecutionLayer::Yes, - BlockImportSource::Lookup, - || Ok(()), - ) - .await - .unwrap(); + Box::pin(beacon_chain.process_block( + full_block_root, + harness.build_range_sync_block_from_store_blobs(Some(block_root), Arc::new(full_block)), + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + )) + .await + .unwrap(); // Store the envelope, its columns, and apply to fork choice. if let Some(envelope) = &snapshot.execution_envelope { diff --git a/beacon_node/builder_client/src/builder_http_client.rs b/beacon_node/builder_client/src/builder_http_client.rs index 12241036b3f..66322954333 100644 --- a/beacon_node/builder_client/src/builder_http_client.rs +++ b/beacon_node/builder_client/src/builder_http_client.rs @@ -5,8 +5,8 @@ use crate::{ use bls::PublicKeyBytes; use eth2::types::{ BuilderPreferencesRequest, ContentType, EthSpec, ExecutionBlockHash, ForkName, - ForkVersionedResponse, Hash256, SignedBeaconBlock, SignedExecutionPayloadBid, - SignedRequestAuth, Slot, + ForkVersionDecode, ForkVersionedResponse, Hash256, SignedBeaconBlock, + SignedExecutionPayloadBid, SignedRequestAuth, Slot, }; use eth2::{ CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE_HEADER, @@ -15,7 +15,7 @@ use eth2::{ use reqwest::StatusCode; use reqwest::header::{ACCEPT, HeaderMap, HeaderName, HeaderValue}; use sensitive_url::SensitiveUrl; -use ssz::{Decode, Encode}; +use ssz::Encode; use std::time::Duration; use tracing::warn; @@ -182,13 +182,23 @@ impl BuilderHttpClient { match content_type_from_header(&response_headers) { ContentType::Ssz => { - let bid = SignedExecutionPayloadBid::::from_ssz_bytes(&response_bytes) - .map_err(Error::InvalidSsz)?; + let bid = SignedExecutionPayloadBid::::from_ssz_bytes_by_fork( + &response_bytes, + fork_name, + ) + .map_err(Error::InvalidSsz)?; Ok(Some(bid)) } ContentType::Json => { let versioned: ForkVersionedResponse> = serde_json::from_slice(&response_bytes).map_err(Error::InvalidJson)?; + let received = versioned.data.message().fork_name_unchecked(); + if received != fork_name { + return Err(Error::InvalidFork { + expected: fork_name, + received, + }); + } Ok(Some(versioned.data)) } } @@ -311,7 +321,7 @@ mod tests { use super::*; use arbitrary::Arbitrary; use eth2::types::beacon_response::EmptyMetadata; - use eth2::types::{ForkName, MainnetEthSpec}; + use eth2::types::{ForkName, MainnetEthSpec, SignedExecutionPayloadBidGloas}; use mockito::{Matcher, Server, ServerGuard}; use std::str::FromStr; @@ -330,11 +340,15 @@ mod tests { SignedRequestAuth::arbitrary(&mut u).unwrap() } + fn empty_bid() -> SignedExecutionPayloadBid { + SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas::empty()) + } + fn empty_bid_response() -> ForkVersionedResponse> { ForkVersionedResponse { version: ForkName::Gloas, metadata: EmptyMetadata {}, - data: SignedExecutionPayloadBid::empty(), + data: empty_bid(), } } @@ -377,7 +391,7 @@ mod tests { let mut server = Server::new_async().await; mock_bid(&mut server, ContentType::Json); let bid = request_bid(&server).await.expect("should have a bid"); - assert_eq!(bid, SignedExecutionPayloadBid::empty()); + assert_eq!(bid, empty_bid()); } #[tokio::test] @@ -385,7 +399,7 @@ mod tests { let mut server = Server::new_async().await; mock_bid(&mut server, ContentType::Ssz); let bid = request_bid(&server).await.expect("should have a bid"); - assert_eq!(bid, SignedExecutionPayloadBid::empty()); + assert_eq!(bid, empty_bid()); } #[tokio::test] @@ -516,4 +530,28 @@ mod tests { .create(); assert!(request_bid(&server).await.is_none()); } + + #[tokio::test] + async fn get_execution_payload_bid_rejects_invalid_fork() { + let mut server = Server::new_async().await; + mock_bid(&mut server, ContentType::Json); + let result = client_for() + .get_execution_payload_bid::( + &builder_url(&server), + Slot::new(1), + ExecutionBlockHash::repeat_byte(1), + Hash256::repeat_byte(2), + &PublicKeyBytes::empty(), + &signed_request_auth(), + ForkName::Heze, + ) + .await; + assert!(matches!( + result, + Err(Error::InvalidFork { + expected: ForkName::Heze, + received: ForkName::Gloas, + }) + )); + } } diff --git a/beacon_node/builder_client/src/builders.rs b/beacon_node/builder_client/src/builders.rs index 28902118f16..71727699c90 100644 --- a/beacon_node/builder_client/src/builders.rs +++ b/beacon_node/builder_client/src/builders.rs @@ -250,8 +250,9 @@ mod tests { use bls::Signature; use eth2::types::beacon_response::EmptyMetadata; use eth2::types::{ - ExecutionPayloadBid, ForkName, ForkVersionedResponse, MainnetEthSpec, RequestAuth, - RequestAuthData, SignedExecutionPayloadBid, SignedRequestAuth, + ExecutionPayloadBidGloas, ForkName, ForkVersionedResponse, MainnetEthSpec, RequestAuth, + RequestAuthData, SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, + SignedRequestAuth, }; use eth2::{CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE_HEADER}; use mockito::{Matcher, Mock, Server, ServerGuard}; @@ -281,16 +282,16 @@ mod tests { let body = ForkVersionedResponse { version: ForkName::Gloas, metadata: EmptyMetadata {}, - data: SignedExecutionPayloadBid:: { - message: ExecutionPayloadBid { + data: SignedExecutionPayloadBid::::Gloas(SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { slot: Slot::new(1), parent_block_hash: ExecutionBlockHash::zero(), parent_block_root: Hash256::ZERO, value, - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }, signature: Signature::empty(), - }, + }), }; serde_json::to_string(&body).unwrap() } @@ -334,7 +335,10 @@ mod tests { Ok::<(), String>(()) }) .await; - let mut values: Vec = bids.iter().map(|b| b.signed_bid.message.value).collect(); + let mut values: Vec = bids + .iter() + .map(|b| b.signed_bid.message().value()) + .collect(); values.sort_unstable(); assert_eq!(values, vec![100, 200]); } diff --git a/beacon_node/builder_client/src/error.rs b/beacon_node/builder_client/src/error.rs index 1fe4af0fa43..00e89836c62 100644 --- a/beacon_node/builder_client/src/error.rs +++ b/beacon_node/builder_client/src/error.rs @@ -7,7 +7,7 @@ //! 406/415 negotiation) into an opaque status code. The pre-Gloas builder client still uses //! `eth2::Error`. -use eth2::types::{BuilderUrlError, ErrorMessage}; +use eth2::types::{BuilderUrlError, ErrorMessage, ForkName}; use pretty_reqwest_error::PrettyReqwestError; use reqwest::{Response, StatusCode}; use sensitive_url::SensitiveUrl; @@ -32,6 +32,11 @@ pub enum Error { InvalidSsz(ssz::DecodeError), /// Request headers could not be constructed. InvalidHeaders(String), + /// The builder returned a bid for a different fork than the one requested. + InvalidFork { + expected: ForkName, + received: ForkName, + }, } impl From for Error { @@ -59,6 +64,9 @@ impl fmt::Display for Error { Error::InvalidJson(e) => write!(f, "invalid JSON response: {e}"), Error::InvalidSsz(e) => write!(f, "invalid SSZ response: {e:?}"), Error::InvalidHeaders(e) => write!(f, "invalid response headers: {e}"), + Error::InvalidFork { expected, received } => { + write!(f, "expected a {expected} bid, received a {received} bid") + } } } } diff --git a/beacon_node/http_api/src/beacon/execution_payload_bids.rs b/beacon_node/http_api/src/beacon/execution_payload_bids.rs index de469453a29..732f66488b1 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_bids.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_bids.rs @@ -1,17 +1,17 @@ use crate::task_spawner::{Priority, TaskSpawner}; use crate::utils::{ - ChainFilter, EthV1Filter, NetworkTxFilter, ResponseFilter, TaskSpawnerFilter, - publish_pubsub_message, + ChainFilter, ConsensusVersionHeaderFilter, EthV1Filter, NetworkTxFilter, ResponseFilter, + TaskSpawnerFilter, publish_pubsub_message, }; use beacon_chain::{BeaconChain, BeaconChainTypes}; use bytes::Bytes; +use context_deserialize::ContextDeserialize; use lighthouse_network::PubsubMessage; use network::NetworkMessage; -use ssz::Decode; use std::sync::Arc; use tokio::sync::mpsc::UnboundedSender; use tracing::{debug, warn}; -use types::SignedExecutionPayloadBid; +use types::{ForkName, ForkVersionDecode, SignedExecutionPayloadBid}; use warp::{ Filter, Rejection, reply::{Reply, Response}, @@ -20,6 +20,7 @@ use warp::{ // POST /eth/v1/beacon/execution_payload_bids (SSZ) pub(crate) fn post_beacon_execution_payload_bids_ssz( eth_v1: EthV1Filter, + consensus_version_header_filter: ConsensusVersionHeaderFilter, task_spawner_filter: TaskSpawnerFilter, chain_filter: ChainFilter, network_tx_filter: NetworkTxFilter, @@ -29,19 +30,24 @@ pub(crate) fn post_beacon_execution_payload_bids_ssz( .and(warp::path("execution_payload_bids")) .and(warp::path::end()) .and(warp::body::bytes()) + .and(consensus_version_header_filter) .and(task_spawner_filter) .and(chain_filter) .and(network_tx_filter) .then( |body_bytes: Bytes, + consensus_version: ForkName, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>| { task_spawner.blocking_response_task(Priority::P0, move || { - let bid = SignedExecutionPayloadBid::::from_ssz_bytes(&body_bytes) - .map_err(|e| { - warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) - })?; + let bid = SignedExecutionPayloadBid::::from_ssz_bytes_by_fork( + &body_bytes, + consensus_version, + ) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) + })?; publish_execution_payload_bid(bid, &chain, &network_tx) }) }, @@ -52,6 +58,7 @@ pub(crate) fn post_beacon_execution_payload_bids_ssz( // POST /eth/v1/beacon/execution_payload_bids pub(crate) fn post_beacon_execution_payload_bids( eth_v1: EthV1Filter, + consensus_version_header_filter: ConsensusVersionHeaderFilter, task_spawner_filter: TaskSpawnerFilter, chain_filter: ChainFilter, network_tx_filter: NetworkTxFilter, @@ -61,15 +68,24 @@ pub(crate) fn post_beacon_execution_payload_bids( .and(warp::path("execution_payload_bids")) .and(warp::path::end()) .and(warp::body::json()) + .and(consensus_version_header_filter) .and(task_spawner_filter) .and(chain_filter) .and(network_tx_filter) .then( - |bid: SignedExecutionPayloadBid, + |value: serde_json::Value, + consensus_version: ForkName, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>| { task_spawner.blocking_response_task(Priority::P0, move || { + let bid = SignedExecutionPayloadBid::::context_deserialize( + &value, + consensus_version, + ) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid JSON: {e:?}")) + })?; publish_execution_payload_bid(bid, &chain, &network_tx) }) }, @@ -83,7 +99,7 @@ pub fn publish_execution_payload_bid( network_tx: &UnboundedSender>, ) -> Result { let slot = bid.slot(); - let builder_index = bid.message.builder_index; + let builder_index = bid.message().builder_index(); if !chain.spec.is_gloas_scheduled() { return Err(warp_utils::reject::custom_bad_request( diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelopes.rs b/beacon_node/http_api/src/beacon/execution_payload_envelopes.rs index 3be66431165..e34cfa89dc2 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_envelopes.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_envelopes.rs @@ -275,7 +275,7 @@ pub async fn publish_execution_payload_envelope( .message() .body() .signed_execution_payload_bid() - .is_ok_and(|bid| !bid.message.blob_kzg_commitments.is_empty()); + .is_ok_and(|bid| !bid.message().blob_kzg_commitments().is_empty()); if commits_to_blobs { return Err(warp_utils::reject::custom_bad_request( "block commits to blobs but the beacon node has no cached blobs and KZG proofs to attach" diff --git a/beacon_node/http_api/src/block_id.rs b/beacon_node/http_api/src/block_id.rs index 27f9d25b34b..2fbacf5474a 100644 --- a/beacon_node/http_api/src/block_id.rs +++ b/beacon_node/http_api/src/block_id.rs @@ -619,8 +619,8 @@ mod tests { .body() .signed_execution_payload_bid() .expect("Gloas block should contain an execution payload bid") - .message - .blob_kzg_commitments + .message() + .blob_kzg_commitments() .get(1) .expect("test block should contain at least two blob commitments") .calculate_versioned_hash(); diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index de43b60e5bf..0bbaa506ec3 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1069,7 +1069,7 @@ pub async fn serve( .and(warp::query::()) .and(warp::path::end()) .and(warp_utils::json::json()) - .and(consensus_version_header_filter) + .and(consensus_version_header_filter.clone()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) @@ -1574,6 +1574,7 @@ pub async fn serve( // POST beacon/execution_payload_bids let post_beacon_execution_payload_bids = post_beacon_execution_payload_bids( eth_v1.clone(), + consensus_version_header_filter.clone(), task_spawner_filter.clone(), chain_filter.clone(), network_tx_filter.clone(), @@ -1582,6 +1583,7 @@ pub async fn serve( // POST beacon/execution_payload_bids (SSZ) let post_beacon_execution_payload_bids_ssz = post_beacon_execution_payload_bids_ssz( eth_v1.clone(), + consensus_version_header_filter.clone(), task_spawner_filter.clone(), chain_filter.clone(), network_tx_filter.clone(), diff --git a/beacon_node/http_api/src/utils.rs b/beacon_node/http_api/src/utils.rs index f2b859ebe59..3189fe2c506 100644 --- a/beacon_node/http_api/src/utils.rs +++ b/beacon_node/http_api/src/utils.rs @@ -21,6 +21,7 @@ pub type ValidatorSubscriptionTxFilter = BoxedFilter<(Sender = BoxedFilter<(UnboundedSender::EthSpec>>,)>; pub type OptionalConsensusVersionHeaderFilter = BoxedFilter<(Option,)>; +pub type ConsensusVersionHeaderFilter = BoxedFilter<(ForkName,)>; pub fn from_meta_data( meta_data: &RwLock>, diff --git a/beacon_node/http_api/tests/gloas_reorg_tests.rs b/beacon_node/http_api/tests/gloas_reorg_tests.rs index f31746ddf70..ab46599d573 100644 --- a/beacon_node/http_api/tests/gloas_reorg_tests.rs +++ b/beacon_node/http_api/tests/gloas_reorg_tests.rs @@ -751,16 +751,16 @@ pub async fn proposer_boost_re_org_test( .body() .signed_execution_payload_bid() .unwrap() - .message - .block_hash + .message() + .block_hash() }; let exec_parent_hash = |block: BeaconBlockRef| -> ExecutionBlockHash { block .body() .signed_execution_payload_bid() .unwrap() - .message - .parent_block_hash + .message() + .parent_block_hash() }; let block_a_exec_hash = exec_block_hash(block_a.0.message()); diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index f224a52fad2..fb00e2647b6 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -52,9 +52,11 @@ use tokio::time::Duration; use tree_hash::TreeHash; use types::ApplicationDomain; use types::{ - Address, Builder, Domain, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, Hash256, - MainnetEthSpec, ProposerPreferences, RelativeEpoch, SelectionProof, SignedExecutionPayloadBid, - SignedExecutionPayloadEnvelope, SignedProposerPreferences, SignedRoot, SingleAttestation, Slot, + Address, Builder, Domain, EthSpec, ExecutionBlockHash, ExecutionPayloadBidGloas, + ExecutionPayloadBidHeze, Hash256, MainnetEthSpec, ProposerPreferences, RelativeEpoch, + SelectionProof, SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, + SignedExecutionPayloadBidHeze, SignedExecutionPayloadEnvelope, SignedProposerPreferences, + SignedRoot, SingleAttestation, Slot, attestation::AttestationBase, consts::gloas::{BUILDER_INDEX_SELF_BUILD, PAYLOAD_BUILDER_VERSION}, }; @@ -3471,24 +3473,44 @@ impl ApiTester { let slot = self.chain.slot().unwrap(); let fork_name = self.chain.spec.fork_name_at_slot::(slot); - let bid = ExecutionPayloadBid { - parent_block_hash: ExecutionBlockHash::zero(), - parent_block_root: head.head_block_root(), - block_hash: ExecutionBlockHash::zero(), - prev_randao: Hash256::zero(), - fee_recipient: Address::zero(), - gas_limit: 30_000_000, - builder_index: 0, - slot, - value: 100, - execution_payment: 0, - blob_kzg_commitments: Default::default(), - execution_requests_root: Hash256::zero(), - }; - - let signed = SignedExecutionPayloadBid { - message: bid, - signature: bls::Signature::empty(), + let signature = bls::Signature::empty(); + let signed = if fork_name.heze_enabled() { + SignedExecutionPayloadBid::Heze(SignedExecutionPayloadBidHeze { + message: ExecutionPayloadBidHeze { + parent_block_hash: ExecutionBlockHash::zero(), + parent_block_root: head.head_block_root(), + block_hash: ExecutionBlockHash::zero(), + prev_randao: Hash256::zero(), + fee_recipient: Address::zero(), + gas_limit: 30_000_000, + builder_index: 0, + slot, + value: 100, + execution_payment: 0, + blob_kzg_commitments: Default::default(), + execution_requests_root: Hash256::zero(), + inclusion_list_bits: Default::default(), + }, + signature, + }) + } else { + SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + parent_block_hash: ExecutionBlockHash::zero(), + parent_block_root: head.head_block_root(), + block_hash: ExecutionBlockHash::zero(), + prev_randao: Hash256::zero(), + fee_recipient: Address::zero(), + gas_limit: 30_000_000, + builder_index: 0, + slot, + value: 100, + execution_payment: 0, + blob_kzg_commitments: Default::default(), + execution_requests_root: Hash256::zero(), + }, + signature, + }) }; (signed, fork_name) diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 8baf3c20bb0..e640e7d7c70 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -2185,7 +2185,7 @@ impl Network { } /// Returns the fork digest to publish `message` on. Proposer preferences use the digest of their -/// proposal epoch. All other messages use the current digest. +/// proposal epoch, and bids the digest of their epoch. All other messages use the current digest. fn publish_fork_digest( message: &PubsubMessage, fork_context: &ForkContext, @@ -2198,6 +2198,7 @@ fn publish_fork_digest( .proposal_slot .epoch(E::slots_per_epoch()), ), + PubsubMessage::ExecutionPayloadBid(bid) => fork_context.context_bytes(bid.epoch()), _ => current_digest, } } @@ -2207,7 +2208,8 @@ mod tests { use super::*; use bls::Signature; use types::{ - Epoch, Hash256, MinimalEthSpec, ProposerPreferences, SignedProposerPreferences, + Epoch, ExecutionPayloadBidHeze, Hash256, MinimalEthSpec, ProposerPreferences, + SignedExecutionPayloadBid, SignedExecutionPayloadBidHeze, SignedProposerPreferences, SignedVoluntaryExit, VoluntaryExit, }; @@ -2250,4 +2252,29 @@ mod tests { current_digest ); } + + #[test] + fn publish_fork_digest_uses_bid_epoch_for_bids() { + let heze_epoch = Epoch::new(2); + let mut spec = ForkName::Gloas.make_genesis_spec(E::default_spec()); + spec.heze_fork_epoch = Some(heze_epoch); + let fork_context = ForkContext::new::(Slot::new(0), Hash256::ZERO, &spec); + let current_digest = fork_context.current_fork_digest(); + let heze_digest = fork_context.context_bytes(heze_epoch); + + let bid = PubsubMessage::::ExecutionPayloadBid(Box::new( + SignedExecutionPayloadBid::Heze(SignedExecutionPayloadBidHeze { + message: ExecutionPayloadBidHeze { + slot: heze_epoch.start_slot(E::slots_per_epoch()), + ..ExecutionPayloadBidHeze::default() + }, + signature: Signature::empty(), + }), + )); + + assert_eq!( + publish_fork_digest(&bid, &fork_context, current_digest), + heze_digest + ); + } } diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index 462e3ccf3cf..6c956d74a7e 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -388,15 +388,29 @@ impl PubsubMessage { ))) } GossipKind::ExecutionPayloadBid => { - if data.len() > E::max_signed_execution_payload_bid_size() { + let fork_name = *fork_context + .get_fork_from_context_bytes(gossip_topic.fork_digest) + .ok_or_else(|| { + format!( + "Unknown gossipsub fork digest: {:?}", + gossip_topic.fork_digest + ) + })?; + let max_size = if fork_name.heze_enabled() { + E::max_signed_execution_payload_bid_size_heze() + } else { + E::max_signed_execution_payload_bid_size() + }; + if data.len() > max_size { return Err(format!( "SignedExecutionPayloadBid size {} exceeds MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE {}", data.len(), - E::max_signed_execution_payload_bid_size() + max_size )); } - let execution_payload_bid = SignedExecutionPayloadBid::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?; + let execution_payload_bid = + SignedExecutionPayloadBid::from_ssz_bytes_by_fork(data, fork_name) + .map_err(|e| format!("{:?}", e))?; Ok(PubsubMessage::ExecutionPayloadBid(Box::new( execution_payload_bid, ))) @@ -644,7 +658,8 @@ impl std::fmt::Display for PubsubMessage { write!( f, "Execution payload bid: slot: {:?} value: {:?}", - data.message.slot, data.message.value + data.message().slot(), + data.message().value() ) } PubsubMessage::ProposerPreferences(data) => { @@ -686,7 +701,8 @@ mod tests { use libp2p::gossipsub::partial_messages::Partial; use types::data::{CellBitmap, PartialDataColumnSidecarGloas}; use types::{ - BeaconBlock, Epoch, EthSpec, ForkName, MainnetEthSpec, Slot, data::DataColumnSubnetId, + BeaconBlock, ChainSpec, Epoch, EthSpec, ForkName, MainnetEthSpec, Slot, + data::DataColumnSubnetId, }; type E = MainnetEthSpec; @@ -713,7 +729,8 @@ mod tests { } } - fn gloas_fork_context() -> ForkContext { + /// A spec with every fork up to Fulu scheduled at genesis. + fn pre_gloas_spec() -> ChainSpec { let mut spec = E::default_spec(); spec.altair_fork_epoch = Some(Epoch::new(0)); spec.bellatrix_fork_epoch = Some(Epoch::new(0)); @@ -721,10 +738,22 @@ mod tests { spec.deneb_fork_epoch = Some(Epoch::new(0)); spec.electra_fork_epoch = Some(Epoch::new(0)); spec.fulu_fork_epoch = Some(Epoch::new(0)); + spec + } + + fn gloas_fork_context() -> ForkContext { + let mut spec = pre_gloas_spec(); spec.gloas_fork_epoch = Some(Epoch::new(0)); ForkContext::new::(Slot::new(0), Hash256::ZERO, &spec) } + fn heze_fork_context() -> ForkContext { + let mut spec = pre_gloas_spec(); + spec.gloas_fork_epoch = Some(Epoch::new(0)); + spec.heze_fork_epoch = Some(Epoch::new(0)); + ForkContext::new::(Slot::new(0), Hash256::ZERO, &spec) + } + #[test] fn gloas_group_id_round_trips_through_decode_partial() { let fork_context = gloas_fork_context(); @@ -764,8 +793,11 @@ mod tests { assert_eq!(decoded.index, column.index); } - fn decode_oversized(kind: GossipKind, size: usize) -> Result, String> { - let fork_context = gloas_fork_context(); + fn decode_oversized( + fork_context: &ForkContext, + kind: GossipKind, + size: usize, + ) -> Result, String> { let topic = GossipTopic::new( kind, GossipEncoding::default(), @@ -773,15 +805,25 @@ mod tests { ); let topic_hash = TopicHash::from_raw(String::from(topic)); let data = vec![0u8; size]; - PubsubMessage::decode(&topic_hash, &data, &fork_context) + PubsubMessage::decode(&topic_hash, &data, fork_context) } #[test] fn gloas_aggregate_and_proof_size_bound() { let max = E::max_signed_aggregate_and_proof_size(); - let err = decode_oversized(GossipKind::BeaconAggregateAndProof, max + 1).unwrap_err(); + let err = decode_oversized( + &gloas_fork_context(), + GossipKind::BeaconAggregateAndProof, + max + 1, + ) + .unwrap_err(); assert!(err.contains("MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE"), "{err}"); - let err = decode_oversized(GossipKind::BeaconAggregateAndProof, max).unwrap_err(); + let err = decode_oversized( + &gloas_fork_context(), + GossipKind::BeaconAggregateAndProof, + max, + ) + .unwrap_err(); assert!( !err.contains("MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE"), "{err}" @@ -791,9 +833,11 @@ mod tests { #[test] fn gloas_attester_slashing_size_bound() { let max = E::max_attester_slashing_size(); - let err = decode_oversized(GossipKind::AttesterSlashing, max + 1).unwrap_err(); + let err = decode_oversized(&gloas_fork_context(), GossipKind::AttesterSlashing, max + 1) + .unwrap_err(); assert!(err.contains("MAX_ATTESTER_SLASHING_SIZE"), "{err}"); - let err = decode_oversized(GossipKind::AttesterSlashing, max).unwrap_err(); + let err = + decode_oversized(&gloas_fork_context(), GossipKind::AttesterSlashing, max).unwrap_err(); assert!(!err.contains("MAX_ATTESTER_SLASHING_SIZE"), "{err}"); } @@ -804,9 +848,9 @@ mod tests { .spec .compute_max_data_column_sidecar_size_gloas::(); let kind = GossipKind::DataColumnSidecar(DataColumnSubnetId::new(0)); - let err = decode_oversized(kind.clone(), max + 1).unwrap_err(); + let err = decode_oversized(&gloas_fork_context(), kind.clone(), max + 1).unwrap_err(); assert!(err.contains("MAX_DATA_COLUMN_SIDECAR_SIZE"), "{err}"); - let err = decode_oversized(kind, max).unwrap_err(); + let err = decode_oversized(&gloas_fork_context(), kind, max).unwrap_err(); assert!(!err.contains("MAX_DATA_COLUMN_SIDECAR_SIZE"), "{err}"); } @@ -845,12 +889,39 @@ mod tests { #[test] fn gloas_execution_payload_bid_size_bound() { let max = E::max_signed_execution_payload_bid_size(); - let err = decode_oversized(GossipKind::ExecutionPayloadBid, max + 1).unwrap_err(); + let err = decode_oversized( + &gloas_fork_context(), + GossipKind::ExecutionPayloadBid, + max + 1, + ) + .unwrap_err(); + assert!( + err.contains("MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE"), + "{err}" + ); + let err = decode_oversized(&gloas_fork_context(), GossipKind::ExecutionPayloadBid, max) + .unwrap_err(); + assert!( + !err.contains("MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE"), + "{err}" + ); + } + + #[test] + fn heze_execution_payload_bid_size_bound() { + let fork_context = heze_fork_context(); + let gloas_max = E::max_signed_execution_payload_bid_size(); + let heze_max = E::max_signed_execution_payload_bid_size_heze(); + assert!(heze_max > gloas_max); + + let err = decode_oversized(&fork_context, GossipKind::ExecutionPayloadBid, heze_max + 1) + .unwrap_err(); assert!( err.contains("MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE"), "{err}" ); - let err = decode_oversized(GossipKind::ExecutionPayloadBid, max).unwrap_err(); + let err = + decode_oversized(&fork_context, GossipKind::ExecutionPayloadBid, heze_max).unwrap_err(); assert!( !err.contains("MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE"), "{err}" @@ -860,9 +931,11 @@ mod tests { #[test] fn heze_inclusion_list_size_bound() { let max = E::max_signed_inclusion_list_size(); - let err = decode_oversized(GossipKind::InclusionList, max + 1).unwrap_err(); + let err = decode_oversized(&gloas_fork_context(), GossipKind::InclusionList, max + 1) + .unwrap_err(); assert!(err.contains("MAX_SIGNED_INCLUSION_LIST_SIZE"), "{err}"); - let err = decode_oversized(GossipKind::InclusionList, max).unwrap_err(); + let err = + decode_oversized(&gloas_fork_context(), GossipKind::InclusionList, max).unwrap_err(); assert!(!err.contains("MAX_SIGNED_INCLUSION_LIST_SIZE"), "{err}"); } } 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 61d12f2c6fa..f2d580c7dfa 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4212,7 +4212,7 @@ impl NetworkBeaconProcessor { parent = None, level = "debug", skip_all, - fields(parent_block_hash = ?bid.message.parent_block_hash, parent_block_root = ?bid.message.parent_block_root), + fields(parent_block_hash = ?bid.message().parent_block_hash(), parent_block_root = ?bid.message().parent_block_root()), )] pub fn process_gossip_execution_payload_bid( self: &Arc, @@ -4234,7 +4234,8 @@ impl NetworkBeaconProcessor { | PayloadBidError::BlockHashEqualsParentBlockHash { .. } | PayloadBidError::InvalidBlobKzgCommitments { .. } | PayloadBidError::BidNotDescendantOfParent { .. } - | PayloadBidError::InvalidPrevRandao { .. }, + | PayloadBidError::InvalidPrevRandao { .. } + | PayloadBidError::InconsistentFork(_), ) => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index c06e360084f..7f1afac2b08 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -1857,7 +1857,7 @@ async fn rpc_columns_notify_after_deferred_envelope_import() { bundle .commitments .iter() - .eq(bid.message.blob_kzg_commitments.iter()) + .eq(bid.message().blob_kzg_commitments().iter()) }) .expect("blobs for next block") .blobs diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs index 6962e4bc1e9..05d02500640 100644 --- a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs +++ b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs @@ -262,7 +262,7 @@ impl SingleBlockLookup { .body() .signed_execution_payload_bid() .ok() - .map(|bid| bid.message.block_hash) + .map(|bid| bid.message().block_hash()) }) } diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 837b11e1c06..fd81b450d1f 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -608,22 +608,22 @@ mod tests { } fn matching_envelope(block: &SignedBeaconBlock) -> Arc> { - let bid = &block + let bid = block .message() .body() .signed_execution_payload_bid() .expect("Gloas block should have payload bid") - .message; + .message(); let mut envelope = SignedExecutionPayloadEnvelope { message: ExecutionPayloadEnvelope::empty(), signature: Signature::empty(), }; envelope.message.beacon_block_root = block.canonical_root(); envelope.message.parent_beacon_block_root = block.parent_root(); - envelope.message.builder_index = bid.builder_index; + envelope.message.builder_index = bid.builder_index(); envelope.message.payload.slot_number = block.slot(); - envelope.message.payload.parent_hash = bid.parent_block_hash; - envelope.message.payload.block_hash = bid.block_hash; + envelope.message.payload.parent_hash = bid.parent_block_hash(); + envelope.message.payload.block_hash = bid.block_hash(); Arc::new(envelope) } @@ -651,7 +651,7 @@ mod tests { if let Ok(bid) = block .message_mut() .body_mut() - .signed_execution_payload_bid_mut() + .signed_execution_payload_bid_gloas_mut() { bid.message.execution_requests_root = ExecutionRequestsGloas::::default().tree_hash_root(); diff --git a/beacon_node/operation_pool/src/attestation.rs b/beacon_node/operation_pool/src/attestation.rs index 60bd2f2df9d..f9a1372aeaf 100644 --- a/beacon_node/operation_pool/src/attestation.rs +++ b/beacon_node/operation_pool/src/attestation.rs @@ -84,7 +84,7 @@ impl<'a, E: EthSpec> AttMaxCover<'a, E> { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let att_participation_flags = get_attestation_participation_flag_indices( state, diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 46d8fb29a5a..611c5079fa4 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -2723,7 +2723,10 @@ mod release_tests { .unwrap(); // Set the parent slot payload availability - let parent_slot = advanced_state.latest_execution_payload_bid().unwrap().slot; + let parent_slot = advanced_state + .latest_execution_payload_bid() + .unwrap() + .slot(); let availability_index = parent_slot.as_usize() % MinimalEthSpec::slots_per_historical_root(); advanced_state diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index b1fea8e1fd1..818e9bc0461 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -435,9 +435,9 @@ where if let Ok(signed_bid) = anchor_block.message().body().signed_execution_payload_bid() { // Gloas: checkpoint sync fetches the anchor's payload later. ( - ExecutionStatus::NotYetRevealed(signed_bid.message.block_hash), - Some(signed_bid.message.parent_block_hash), - Some(signed_bid.message.block_hash), + ExecutionStatus::NotYetRevealed(signed_bid.message().block_hash()), + Some(signed_bid.message().parent_block_hash()), + Some(signed_bid.message().block_hash()), ) } else if let Ok(execution_payload) = anchor_block.message().execution_payload() { // Pre-Gloas forks: do not set payload hashes, they are only used post-Gloas. @@ -839,7 +839,7 @@ where let builds_on_full = block .body() .signed_execution_payload_bid() - .is_ok_and(|bid| bid.message.parent_block_hash == parent_block_hash); + .is_ok_and(|bid| bid.message().parent_block_hash() == parent_block_hash); if builds_on_full && !self.is_payload_received(&block.parent_root()) { return Err(Error::InvalidBlock(InvalidBlock::ParentPayloadNotVerified( block.parent_root(), @@ -1065,8 +1065,8 @@ where let (execution_payload_parent_hash, execution_payload_block_hash) = if let Ok(signed_bid) = block.body().signed_execution_payload_bid() { ( - Some(signed_bid.message.parent_block_hash), - Some(signed_bid.message.block_hash), + Some(signed_bid.message().parent_block_hash()), + Some(signed_bid.message().block_hash()), ) } else { (None, None) diff --git a/consensus/state_processing/src/envelope_processing.rs b/consensus/state_processing/src/envelope_processing.rs index ff3060c7798..c6d0fd32e9d 100644 --- a/consensus/state_processing/src/envelope_processing.rs +++ b/consensus/state_processing/src/envelope_processing.rs @@ -150,16 +150,16 @@ pub fn verify_execution_payload_envelope( // Verify consistency with the committed bid let committed_bid = state.latest_execution_payload_bid()?; envelope_verify!( - envelope.builder_index == committed_bid.builder_index, + envelope.builder_index == committed_bid.builder_index(), EnvelopeProcessingError::BuilderIndexMismatch { - committed_bid: committed_bid.builder_index, + committed_bid: committed_bid.builder_index(), envelope: envelope.builder_index, } ); envelope_verify!( - committed_bid.prev_randao == payload.prev_randao, + committed_bid.prev_randao() == payload.prev_randao, EnvelopeProcessingError::PrevRandaoMismatch { - committed_bid: committed_bid.prev_randao, + committed_bid: committed_bid.prev_randao(), envelope: payload.prev_randao, } ); @@ -183,18 +183,18 @@ pub fn verify_execution_payload_envelope( // Verify the gas limit envelope_verify!( - committed_bid.gas_limit == payload.gas_limit, + committed_bid.gas_limit() == payload.gas_limit, EnvelopeProcessingError::GasLimitMismatch { - committed_bid: committed_bid.gas_limit, + committed_bid: committed_bid.gas_limit(), envelope: payload.gas_limit, } ); // Verify the block hash envelope_verify!( - committed_bid.block_hash == payload.block_hash, + committed_bid.block_hash() == payload.block_hash, EnvelopeProcessingError::BlockHashMismatch { - committed_bid: committed_bid.block_hash, + committed_bid: committed_bid.block_hash(), envelope: payload.block_hash, } ); @@ -221,9 +221,9 @@ pub fn verify_execution_payload_envelope( // Verify execution requests root matches committed bid let execution_requests_root = envelope.execution_requests.tree_hash_root(); envelope_verify!( - execution_requests_root == committed_bid.execution_requests_root, + execution_requests_root == committed_bid.execution_requests_root(), EnvelopeProcessingError::ExecutionRequestsRootMismatch { - committed_bid: committed_bid.execution_requests_root, + committed_bid: committed_bid.execution_requests_root(), envelope: execution_requests_root, } ); diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index a6914b61f1f..725942b4817 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -170,17 +170,12 @@ pub fn initialize_beacon_state_from_eth1( // The genesis block's bid must have block_hash = 0x00 per spec (empty payload). // Retain the EL genesis hash in latest_block_hash and parent_block_hash so the - // first post-genesis proposer can build on the correct EL head. - let el_genesis_hash = state.latest_execution_payload_bid()?.block_hash; - let bid = state.latest_execution_payload_bid_mut()?; + // first post-genesis proposer can build on the correct EL head. This runs on the + // Gloas genesis state; a later Heze upgrade carries these fields forward. + let bid = state.latest_execution_payload_bid_gloas_mut()?; + let el_genesis_hash = bid.block_hash; bid.parent_block_hash = el_genesis_hash; bid.block_hash = ExecutionBlockHash::default(); - - // Update the `latest_block_header.body_root` so that it matches the body of the - // Gloas genesis block, which embeds `state.latest_execution_payload_bid` in its - // `signed_execution_payload_bid` field (see `genesis_block`). - let genesis_body_root = genesis_block(&state, spec)?.body_root(); - state.latest_block_header_mut().body_root = genesis_body_root; } // Upgrade to heze if configured from genesis. @@ -194,6 +189,14 @@ pub fn initialize_beacon_state_from_eth1( state.fork_mut().previous_version = spec.heze_fork_version; } + // From Gloas onwards the genesis block body embeds `state.latest_execution_payload_bid` + // (see `genesis_block`), so `latest_block_header.body_root` must be computed against the + // final genesis fork, after every upgrade has run + if state.fork_name_unchecked().gloas_enabled() { + let genesis_body_root = genesis_block(&state, spec)?.body_root(); + state.latest_block_header_mut().body_root = genesis_body_root; + } + // Now that we have our validators, initialize the caches (including the committees) state.build_caches(spec)?; @@ -217,8 +220,22 @@ pub fn genesis_block( spec: &ChainSpec, ) -> Result, BeaconStateError> { let mut block = BeaconBlock::empty(spec); - if let Ok(signed_bid) = block.body_mut().signed_execution_payload_bid_mut() { - signed_bid.message = state.latest_execution_payload_bid()?.clone(); + match &mut block { + BeaconBlock::Base(_) + | BeaconBlock::Altair(_) + | BeaconBlock::Bellatrix(_) + | BeaconBlock::Capella(_) + | BeaconBlock::Deneb(_) + | BeaconBlock::Electra(_) + | BeaconBlock::Fulu(_) => {} + BeaconBlock::Gloas(gloas_block) => { + gloas_block.body.signed_execution_payload_bid.message = + state.latest_execution_payload_bid_gloas()?.clone(); + } + BeaconBlock::Heze(heze_block) => { + heze_block.body.signed_execution_payload_bid.message = + state.latest_execution_payload_bid_heze()?.clone(); + } } Ok(block) } diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 37bf4e16ffa..acc4d2791d9 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -195,7 +195,7 @@ pub fn per_block_processing>( if state.fork_name_unchecked().gloas_enabled() { withdrawals::gloas::process_withdrawals::(state, spec)?; let signed_bid = block.body().signed_execution_payload_bid()?; - parent_slot = Some(state.latest_execution_payload_bid()?.slot); + parent_slot = Some(state.latest_execution_payload_bid()?.slot()); process_execution_payload_bid(state, signed_bid, verify_signatures, spec)?; } else { if state.fork_name_unchecked().capella_enabled() { @@ -567,12 +567,12 @@ pub fn process_parent_execution_payload( requests: &ExecutionRequestsGloas, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - let parent_bid = state.latest_execution_payload_bid()?.clone(); - let parent_slot = parent_bid.slot; + let parent_bid = state.latest_execution_payload_bid()?; + let parent_slot = parent_bid.slot(); + let parent_value = parent_bid.value(); + let parent_fee_recipient = parent_bid.fee_recipient(); + let parent_builder_index = parent_bid.builder_index(); + let parent_block_hash = parent_bid.block_hash(); let parent_epoch = parent_slot.epoch(E::slots_per_epoch()); verify_execution_request_list_lengths(requests)?; @@ -627,16 +631,16 @@ pub fn apply_parent_execution_payload( } else if parent_epoch == state.previous_epoch() { let payment_index = parent_slot.as_u64().safe_rem(E::slots_per_epoch())? as usize; settle_builder_payment(state, payment_index)?; - } else if parent_bid.value > 0 { + } else if parent_value > 0 { // Parent is older than previous epoch -- payment entry has already been // settled or evicted by process_builder_pending_payments at epoch boundaries. // Append the withdrawal directly from the bid. state .builder_pending_withdrawals_mut()? .push(BuilderPendingWithdrawal { - fee_recipient: parent_bid.fee_recipient, - amount: parent_bid.value, - builder_index: parent_bid.builder_index, + fee_recipient: parent_fee_recipient, + amount: parent_value, + builder_index: parent_builder_index, }) .map_err(|e| BlockProcessingError::BeaconStateError(e.into()))?; } @@ -651,7 +655,7 @@ pub fn apply_parent_execution_payload( .map_err(BlockProcessingError::BitfieldError)?; // Update latest_block_hash to the parent bid's block_hash - *state.latest_block_hash_mut()? = parent_bid.block_hash; + *state.latest_block_hash_mut()? = parent_block_hash; Ok(()) } @@ -722,14 +726,14 @@ pub fn settle_builder_payment( pub fn process_execution_payload_bid( state: &mut BeaconState, - signed_bid: &SignedExecutionPayloadBid, + signed_bid: SignedExecutionPayloadBidRef, verify_signatures: VerifySignatures, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { // Verify the bid signature - let bid = &signed_bid.message; - let amount = bid.value; - let builder_index = bid.builder_index; + let bid = signed_bid.message(); + let amount = bid.value(); + let builder_index = bid.builder_index(); // For self-builds, amount must be zero regardless of withdrawal credential prefix if builder_index == BUILDER_INDEX_SELF_BUILD { @@ -738,7 +742,7 @@ pub fn process_execution_payload_bid( ExecutionPayloadBidInvalid::SelfBuildNonZeroAmount.into() ); block_verify!( - signed_bid.signature.is_infinity(), + signed_bid.signature().is_infinity(), ExecutionPayloadBidInvalid::BadSignature.into() ); } else { @@ -791,19 +795,19 @@ pub fn process_execution_payload_bid( // Verify commitments are under limit let max_blobs_per_block = spec.max_blobs_per_block(state.current_epoch()) as usize; block_verify!( - bid.blob_kzg_commitments.len() <= max_blobs_per_block, + bid.blob_kzg_commitments().len() <= max_blobs_per_block, ExecutionPayloadBidInvalid::ExcessBlobCommitments { max: max_blobs_per_block, - bid: bid.blob_kzg_commitments.len(), + bid: bid.blob_kzg_commitments().len(), } .into() ); // Verify that the bid is for the current slot block_verify!( - bid.slot == state.slot(), + bid.slot() == state.slot(), ExecutionPayloadBidInvalid::SlotMismatch { - bid_slot: bid.slot, + bid_slot: bid.slot(), state_slot: state.slot(), } .into() @@ -812,39 +816,39 @@ pub fn process_execution_payload_bid( // Verify that the bid is for the right parent block let latest_block_hash = state.latest_block_hash()?; block_verify!( - bid.parent_block_hash == *latest_block_hash, + bid.parent_block_hash() == *latest_block_hash, ExecutionPayloadBidInvalid::ParentBlockHashMismatch { state_block_hash: *latest_block_hash, - bid_parent_hash: bid.parent_block_hash, + bid_parent_hash: bid.parent_block_hash(), } .into() ); // Verify that the bid's block hash differs from its parent block hash block_verify!( - bid.block_hash != bid.parent_block_hash, + bid.block_hash() != bid.parent_block_hash(), ExecutionPayloadBidInvalid::BlockHashEqualsParentBlockHash { - block_hash: bid.block_hash, + block_hash: bid.block_hash(), } .into() ); let expected_parent_root = *state.get_block_root(state.slot().safe_sub(1)?)?; block_verify!( - bid.parent_block_root == expected_parent_root, + bid.parent_block_root() == expected_parent_root, ExecutionPayloadBidInvalid::ParentBlockRootMismatch { block_parent_root: expected_parent_root, - bid_parent_root: bid.parent_block_root, + bid_parent_root: bid.parent_block_root(), } .into() ); let expected_randao = *state.get_randao_mix(state.current_epoch())?; block_verify!( - bid.prev_randao == expected_randao, + bid.prev_randao() == expected_randao, ExecutionPayloadBidInvalid::PrevRandaoMismatch { expected: expected_randao, - bid: bid.prev_randao, + bid: bid.prev_randao(), } .into() ); @@ -855,15 +859,18 @@ pub fn process_execution_payload_bid( let pending_payment = BuilderPendingPayment { weight: 0, withdrawal: BuilderPendingWithdrawal { - fee_recipient: bid.fee_recipient, + fee_recipient: bid.fee_recipient(), amount, builder_index, }, proposer_index, }; - let payment_index = E::SlotsPerEpoch::to_usize() - .safe_add(bid.slot.as_usize().safe_rem(E::SlotsPerEpoch::to_usize())?)?; + let payment_index = E::SlotsPerEpoch::to_usize().safe_add( + bid.slot() + .as_usize() + .safe_rem(E::SlotsPerEpoch::to_usize())?, + )?; *state .builder_pending_payments_mut()? @@ -873,8 +880,19 @@ pub fn process_execution_payload_bid( ))? = pending_payment; } - // Cache the execution bid - *state.latest_execution_payload_bid_mut()? = bid.clone(); + // Cache the execution bid (the bid's fork must match the state's fork) + match (state.latest_execution_payload_bid_mut()?, bid) { + (ExecutionPayloadBidRefMut::Gloas(bid_mut), ExecutionPayloadBidRef::Gloas(cached)) => { + *bid_mut = cached.clone(); + } + (ExecutionPayloadBidRefMut::Heze(bid_mut), ExecutionPayloadBidRef::Heze(cached)) => { + *bid_mut = cached.clone(); + } + (ExecutionPayloadBidRefMut::Gloas(_), ExecutionPayloadBidRef::Heze(_)) + | (ExecutionPayloadBidRefMut::Heze(_), ExecutionPayloadBidRef::Gloas(_)) => { + return Err(BlockProcessingError::IncorrectStateType); + } + } Ok(()) } diff --git a/consensus/state_processing/src/per_block_processing/signature_sets.rs b/consensus/state_processing/src/per_block_processing/signature_sets.rs index f084e259f06..7ba71ffd750 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -12,7 +12,7 @@ use types::{ BuilderIndex, ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork, IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation, ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockHeader, - SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid, + SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBidRef, SignedInclusionList, SignedProposerPreferences, SignedRoot, SignedVoluntaryExit, SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData, consts::gloas::BUILDER_INDEX_SELF_BUILD, }; @@ -439,15 +439,15 @@ where pub fn execution_payload_bid_signature_set<'a, E, F>( state: &'a BeaconState, get_builder_pubkey: F, - signed_execution_payload_bid: &'a SignedExecutionPayloadBid, + signed_execution_payload_bid: SignedExecutionPayloadBidRef<'a, E>, spec: &'a ChainSpec, ) -> Result>> where E: EthSpec, F: Fn(BuilderIndex) -> Option>, { - let execution_payload_bid = &signed_execution_payload_bid.message; - let builder_index = execution_payload_bid.builder_index; + let execution_payload_bid = signed_execution_payload_bid.message(); + let builder_index = execution_payload_bid.builder_index(); if builder_index == BUILDER_INDEX_SELF_BUILD { // No signatures to verify in case of a self-build, but consensus code MUST check that // the signature is the point at infinity. @@ -456,8 +456,8 @@ where } let bid_epoch = signed_execution_payload_bid - .message - .slot + .message() + .slot() .epoch(E::slots_per_epoch()); let bid_fork = spec.fork_at_epoch(bid_epoch); let domain = spec.get_domain( @@ -471,7 +471,7 @@ where let message = execution_payload_bid.signing_root(domain); Ok(Some(SignatureSet::single_pubkey( - &signed_execution_payload_bid.signature, + signed_execution_payload_bid.signature(), pubkey, message, ))) diff --git a/consensus/state_processing/src/per_block_processing/tests.rs b/consensus/state_processing/src/per_block_processing/tests.rs index ea6c83ffbc7..a6047990c80 100644 --- a/consensus/state_processing/src/per_block_processing/tests.rs +++ b/consensus/state_processing/src/per_block_processing/tests.rs @@ -444,7 +444,7 @@ async fn invalid_attestation_no_committee_for_index() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), @@ -499,7 +499,7 @@ async fn invalid_attestation_wrong_justified_checkpoint() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), @@ -558,7 +558,7 @@ async fn invalid_attestation_bad_aggregation_bitfield_len() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), @@ -603,7 +603,7 @@ async fn invalid_attestation_bad_signature() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), @@ -652,7 +652,7 @@ async fn invalid_attestation_included_too_early() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), @@ -708,7 +708,7 @@ async fn invalid_attestation_target_epoch_slot_mismatch() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), diff --git a/consensus/state_processing/src/per_block_processing/withdrawals.rs b/consensus/state_processing/src/per_block_processing/withdrawals.rs index 1324f7fda6e..aa29f98905f 100644 --- a/consensus/state_processing/src/per_block_processing/withdrawals.rs +++ b/consensus/state_processing/src/per_block_processing/withdrawals.rs @@ -495,7 +495,7 @@ pub mod gloas { spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { // Return early if the parent block is empty. - if *state.latest_block_hash()? != state.latest_execution_payload_bid()?.block_hash { + if *state.latest_block_hash()? != state.latest_execution_payload_bid()?.block_hash() { return Ok(()); } diff --git a/consensus/state_processing/src/upgrade/gloas.rs b/consensus/state_processing/src/upgrade/gloas.rs index fc1719e36a4..2d81576850d 100644 --- a/consensus/state_processing/src/upgrade/gloas.rs +++ b/consensus/state_processing/src/upgrade/gloas.rs @@ -12,8 +12,8 @@ use tree_hash::TreeHash; use typenum::Unsigned; use types::{ Address, BeaconState, BeaconStateError as Error, BeaconStateGloas, Builder, - BuilderPendingPayment, ChainSpec, EthSpec, ExecutionPayloadBid, ExecutionRequestsGloas, Fork, - PendingDeposit, ProgressiveKzgCommitments, + BuilderPendingPayment, ChainSpec, EthSpec, ExecutionPayloadBidGloas, ExecutionRequestsGloas, + Fork, PendingDeposit, ProgressiveKzgCommitments, consts::gloas::{BUILDER_INDEX_SELF_BUILD, PAYLOAD_BUILDER_VERSION}, is_builder_withdrawal_credential, }; @@ -120,7 +120,7 @@ pub fn upgrade_state_to_gloas( current_sync_committee: pre.current_sync_committee.clone(), next_sync_committee: pre.next_sync_committee.clone(), // Execution Bid - latest_execution_payload_bid: ExecutionPayloadBid { + latest_execution_payload_bid: ExecutionPayloadBidGloas { parent_block_hash: pre.latest_execution_payload_header.parent_hash, parent_block_root: pre.latest_block_header.parent_root, block_hash: pre.latest_execution_payload_header.block_hash, diff --git a/consensus/state_processing/src/upgrade/heze.rs b/consensus/state_processing/src/upgrade/heze.rs index e9156418c1e..dd03dcd02fc 100644 --- a/consensus/state_processing/src/upgrade/heze.rs +++ b/consensus/state_processing/src/upgrade/heze.rs @@ -1,5 +1,9 @@ +use ssz_types::BitVector; use std::mem; -use types::{BeaconState, BeaconStateError as Error, BeaconStateHeze, ChainSpec, EthSpec, Fork}; +use types::{ + BeaconState, BeaconStateError as Error, BeaconStateHeze, ChainSpec, EthSpec, + ExecutionPayloadBidHeze, Fork, +}; /// Transform a `Gloas` state into a `Heze` state. pub fn upgrade_to_heze( @@ -64,7 +68,26 @@ pub fn upgrade_state_to_heze( current_sync_committee: pre.current_sync_committee.clone(), next_sync_committee: pre.next_sync_committee.clone(), // Execution Bid - latest_execution_payload_bid: pre.latest_execution_payload_bid.clone(), + // [New in Heze:EIP7805] the bid gains `inclusion_list_bits`; carry the shared Gloas + // fields forward and default the new field to empty (no bits computed at upgrade). + latest_execution_payload_bid: { + let pre_bid = &pre.latest_execution_payload_bid; + ExecutionPayloadBidHeze { + parent_block_hash: pre_bid.parent_block_hash, + parent_block_root: pre_bid.parent_block_root, + block_hash: pre_bid.block_hash, + prev_randao: pre_bid.prev_randao, + fee_recipient: pre_bid.fee_recipient, + gas_limit: pre_bid.gas_limit, + builder_index: pre_bid.builder_index, + slot: pre_bid.slot, + value: pre_bid.value, + execution_payment: pre_bid.execution_payment, + blob_kzg_commitments: pre_bid.blob_kzg_commitments.clone(), + execution_requests_root: pre_bid.execution_requests_root, + inclusion_list_bits: BitVector::new(), + } + }, // Capella next_withdrawal_index: pre.next_withdrawal_index, next_withdrawal_validator_index: pre.next_withdrawal_validator_index, diff --git a/consensus/types/presets/gnosis/heze.yaml b/consensus/types/presets/gnosis/heze.yaml index eb4f5c75ebb..e89d35392ab 100644 --- a/consensus/types/presets/gnosis/heze.yaml +++ b/consensus/types/presets/gnosis/heze.yaml @@ -7,5 +7,7 @@ INCLUSION_LIST_COMMITTEE_SIZE: 16 # Type-specific SSZ bounds # --------------------------------------------------------------- +# 196,934 bytes, ~192 KiB +MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE_HEZE: 196934 # 41,112 bytes, ~40 KiB MAX_SIGNED_INCLUSION_LIST_SIZE: 41112 diff --git a/consensus/types/presets/mainnet/heze.yaml b/consensus/types/presets/mainnet/heze.yaml index c0d9a4dd382..a685eebc35d 100644 --- a/consensus/types/presets/mainnet/heze.yaml +++ b/consensus/types/presets/mainnet/heze.yaml @@ -7,5 +7,7 @@ INCLUSION_LIST_COMMITTEE_SIZE: 16 # Type-specific SSZ bounds # --------------------------------------------------------------- +# 196,934 bytes, ~192 KiB +MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE_HEZE: 196934 # 41,112 bytes, ~40 KiB MAX_SIGNED_INCLUSION_LIST_SIZE: 41112 diff --git a/consensus/types/presets/minimal/heze.yaml b/consensus/types/presets/minimal/heze.yaml index d31fa7f3a29..e987c2e1fa0 100644 --- a/consensus/types/presets/minimal/heze.yaml +++ b/consensus/types/presets/minimal/heze.yaml @@ -7,5 +7,7 @@ INCLUSION_LIST_COMMITTEE_SIZE: 16 # Type-specific SSZ bounds # --------------------------------------------------------------- +# 196,934 bytes, ~192 KiB +MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE_HEZE: 196934 # 41,112 bytes, ~40 KiB MAX_SIGNED_INCLUSION_LIST_SIZE: 41112 diff --git a/consensus/types/src/block/beacon_block.rs b/consensus/types/src/block/beacon_block.rs index 368dc2e3002..f4a667acc0d 100644 --- a/consensus/types/src/block/beacon_block.rs +++ b/consensus/types/src/block/beacon_block.rs @@ -14,7 +14,7 @@ use tree_hash_derive::TreeHash; use typenum::Unsigned; use crate::{ - KzgCommitment, SignedExecutionPayloadBid, + KzgCommitment, SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, attestation::{AttestationBase, AttestationData, IndexedAttestationBase}, block::{ BeaconBlockBodyAltair, BeaconBlockBodyBase, BeaconBlockBodyBellatrix, @@ -731,7 +731,7 @@ impl> EmptyBlock for BeaconBlockGloa sync_aggregate: SyncAggregate::empty(), bls_to_execution_changes: ProgressiveVariableList::empty(), parent_execution_requests: ExecutionRequestsGloas::default(), - signed_execution_payload_bid: SignedExecutionPayloadBid::empty(), + signed_execution_payload_bid: SignedExecutionPayloadBidGloas::empty(), payload_attestations: ProgressiveVariableList::empty(), _phantom: PhantomData, }, @@ -763,7 +763,7 @@ impl> EmptyBlock for BeaconBlockHeze sync_aggregate: SyncAggregate::empty(), bls_to_execution_changes: ProgressiveVariableList::empty(), parent_execution_requests: ExecutionRequestsGloas::default(), - signed_execution_payload_bid: SignedExecutionPayloadBid::empty(), + signed_execution_payload_bid: SignedExecutionPayloadBidHeze::empty(), payload_attestations: ProgressiveVariableList::empty(), _phantom: PhantomData, }, diff --git a/consensus/types/src/block/beacon_block_body.rs b/consensus/types/src/block/beacon_block_body.rs index 8350a7f8fe0..e6f79ad2e40 100644 --- a/consensus/types/src/block/beacon_block_body.rs +++ b/consensus/types/src/block/beacon_block_body.rs @@ -13,7 +13,8 @@ use tree_hash::TreeHash; use tree_hash_derive::TreeHash; use crate::{ - ListRef, SignedExecutionPayloadBid, + ListRef, SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, + SignedExecutionPayloadBidRef, attestation::{ AttestationBase, AttestationElectra, AttestationGloas, AttestationRef, AttestationRefMut, PayloadAttestation, @@ -215,8 +216,16 @@ pub struct BeaconBlockBody = FullPay pub blob_kzg_commitments: KzgCommitments, #[superstruct(only(Electra, Fulu))] pub execution_requests: ExecutionRequestsElectra, - #[superstruct(only(Gloas, Heze))] - pub signed_execution_payload_bid: SignedExecutionPayloadBid, + #[superstruct( + only(Gloas), + partial_getter(rename = "signed_execution_payload_bid_gloas") + )] + pub signed_execution_payload_bid: SignedExecutionPayloadBidGloas, + #[superstruct( + only(Heze), + partial_getter(rename = "signed_execution_payload_bid_heze") + )] + pub signed_execution_payload_bid: SignedExecutionPayloadBidHeze, #[superstruct(only(Gloas, Heze))] pub payload_attestations: ProgressiveVariableList, E::MaxPayloadAttestations>, @@ -261,6 +270,12 @@ impl> BeaconBlockBody { pub fn fork_name(&self) -> ForkName { self.to_ref().fork_name() } + + pub fn signed_execution_payload_bid( + &self, + ) -> Result, BeaconStateError> { + self.to_ref().signed_execution_payload_bid() + } } impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, Payload> { @@ -277,6 +292,26 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, } } + pub fn signed_execution_payload_bid( + &self, + ) -> Result, BeaconStateError> { + match self { + Self::Base(_) + | Self::Altair(_) + | Self::Bellatrix(_) + | Self::Capella(_) + | Self::Deneb(_) + | Self::Electra(_) + | Self::Fulu(_) => Err(BeaconStateError::IncorrectStateVariant), + Self::Gloas(body) => Ok(SignedExecutionPayloadBidRef::Gloas( + &body.signed_execution_payload_bid, + )), + Self::Heze(body) => Ok(SignedExecutionPayloadBidRef::Heze( + &body.signed_execution_payload_bid, + )), + } + } + pub fn body_merkle_leaves(&self) -> Vec { let mut leaves = vec![]; match self { diff --git a/consensus/types/src/block/signed_beacon_block.rs b/consensus/types/src/block/signed_beacon_block.rs index 29b639c1216..4ae7e97da95 100644 --- a/consensus/types/src/block/signed_beacon_block.rs +++ b/consensus/types/src/block/signed_beacon_block.rs @@ -369,7 +369,7 @@ impl> SignedBeaconBlock self.message() .body() .signed_execution_payload_bid() - .map(|bid| bid.message.blob_kzg_commitments.len()) + .map(|bid| bid.message().blob_kzg_commitments().len()) }) .unwrap_or(0) } @@ -390,7 +390,7 @@ impl> SignedBeaconBlock self.message() .body() .signed_execution_payload_bid() - .map(|bid| bid.message.block_hash) + .map(|bid| bid.message().block_hash()) } /// Convenience accessor for the block's bid's `parent_block_hash`. @@ -400,7 +400,7 @@ impl> SignedBeaconBlock self.message() .body() .signed_execution_payload_bid() - .map(|bid| bid.message.parent_block_hash) + .map(|bid| bid.message().parent_block_hash()) } /// Check if the `parent_hash` in this block's `signed_payload_bid` matches `parent_block_hash`. @@ -416,7 +416,7 @@ impl> SignedBeaconBlock // Prior to Gloas. return false; }; - signed_payload_bid.message.parent_block_hash == parent_block_hash + signed_payload_bid.message().parent_block_hash() == parent_block_hash } } diff --git a/consensus/types/src/core/eth_spec.rs b/consensus/types/src/core/eth_spec.rs index 8aef0618c78..9e0dc91ddab 100644 --- a/consensus/types/src/core/eth_spec.rs +++ b/consensus/types/src/core/eth_spec.rs @@ -460,6 +460,9 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq + /// Returns the `MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE` constant for this specification. fn max_signed_execution_payload_bid_size() -> usize; + /// Returns the `MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE_HEZE` constant for this specification. + fn max_signed_execution_payload_bid_size_heze() -> usize; + /// Returns the `MAX_SIGNED_INCLUSION_LIST_SIZE` constant for this specification. fn max_signed_inclusion_list_size() -> usize; @@ -575,6 +578,10 @@ impl EthSpec for MainnetEthSpec { 196932 } + fn max_signed_execution_payload_bid_size_heze() -> usize { + 196934 + } + fn max_signed_inclusion_list_size() -> usize { 41112 } @@ -671,6 +678,10 @@ impl EthSpec for MinimalEthSpec { 196932 } + fn max_signed_execution_payload_bid_size_heze() -> usize { + 196934 + } + fn max_signed_inclusion_list_size() -> usize { 41112 } @@ -764,6 +775,10 @@ impl EthSpec for GnosisEthSpec { 196932 } + fn max_signed_execution_payload_bid_size_heze() -> usize { + 196934 + } + fn max_signed_inclusion_list_size() -> usize { 41112 } diff --git a/consensus/types/src/core/preset.rs b/consensus/types/src/core/preset.rs index 9bf15e3ef83..0afb1664c5b 100644 --- a/consensus/types/src/core/preset.rs +++ b/consensus/types/src/core/preset.rs @@ -374,6 +374,8 @@ pub struct HezePreset { #[serde(with = "serde_utils::quoted_u64")] pub inclusion_list_committee_size: u64, #[serde(with = "serde_utils::quoted_u64")] + pub max_signed_execution_payload_bid_size_heze: u64, + #[serde(with = "serde_utils::quoted_u64")] pub max_signed_inclusion_list_size: u64, } @@ -381,6 +383,8 @@ impl HezePreset { pub fn from_chain_spec(_spec: &ChainSpec) -> Self { Self { inclusion_list_committee_size: E::inclusion_list_committee_size() as u64, + max_signed_execution_payload_bid_size_heze: + E::max_signed_execution_payload_bid_size_heze() as u64, max_signed_inclusion_list_size: E::max_signed_inclusion_list_size() as u64, } } diff --git a/consensus/types/src/execution/execution_payload_bid.rs b/consensus/types/src/execution/execution_payload_bid.rs index 31b29301160..8072ce565aa 100644 --- a/consensus/types/src/execution/execution_payload_bid.rs +++ b/consensus/types/src/execution/execution_payload_bid.rs @@ -1,52 +1,219 @@ use crate::kzg_ext::ProgressiveKzgCommitments; -use crate::{Address, EthSpec, ExecutionBlockHash, ForkName, Hash256, SignedRoot, Slot}; -use context_deserialize::context_deserialize; +use crate::{ + Address, BeaconStateError, ChainSpec, EthSpec, ExecutionBlockHash, ForkName, ForkVersionDecode, + Hash256, InconsistentFork, SignedRoot, Slot, +}; +use context_deserialize::{ContextDeserialize, context_deserialize}; use educe::Educe; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; +use ssz::Decode; use ssz_derive::{Decode, Encode}; +use ssz_types::BitVector; +use superstruct::superstruct; use tree_hash_derive::TreeHash; -#[derive(Default, Debug, Clone, Serialize, Encode, Decode, Deserialize, TreeHash, Educe)] +#[superstruct( + variants(Gloas, Heze), + variant_attributes( + derive( + Default, + Debug, + Clone, + Serialize, + Deserialize, + Encode, + Decode, + TreeHash, + Educe, + ), + context_deserialize(ForkName), + educe(PartialEq, Hash(bound(E: EthSpec))), + serde(bound = "E: EthSpec", deny_unknown_fields), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), + ), + ref_attributes(derive(Debug, PartialEq, TreeHash), tree_hash(enum_behaviour = "transparent")), + specific_variant_attributes( + Gloas(tree_hash( + struct_behaviour = "progressive_container", + active_fields(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1) + )), + Heze(tree_hash( + struct_behaviour = "progressive_container", + active_fields(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1) + )) + ), + cast_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ), + partial_getter_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ) +)] #[cfg_attr( feature = "arbitrary", derive(arbitrary::Arbitrary), arbitrary(bound = "E: EthSpec") )] -#[educe(PartialEq, Hash)] -#[serde(bound = "E: EthSpec")] -#[context_deserialize(ForkName)] -// https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#executionpayloadbid -#[tree_hash( - struct_behaviour = "progressive_container", - active_fields(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1) -)] +#[derive(Debug, Clone, Serialize, Encode, TreeHash, Educe)] +#[educe(PartialEq, Hash(bound(E: EthSpec)))] +#[serde(bound = "E: EthSpec", untagged)] +#[ssz(enum_behaviour = "transparent")] +#[tree_hash(enum_behaviour = "transparent")] +// https://github.com/ethereum/consensus-specs/blob/master/specs/heze/beacon-chain.md#executionpayloadbid pub struct ExecutionPayloadBid { + #[superstruct(getter(copy))] pub parent_block_hash: ExecutionBlockHash, + #[superstruct(getter(copy))] pub parent_block_root: Hash256, + #[superstruct(getter(copy))] pub block_hash: ExecutionBlockHash, + #[superstruct(getter(copy))] pub prev_randao: Hash256, + #[superstruct(getter(copy))] #[serde(with = "serde_utils::address_hex")] pub fee_recipient: Address, + #[superstruct(getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub gas_limit: u64, + #[superstruct(getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub builder_index: u64, + #[superstruct(getter(copy))] pub slot: Slot, + #[superstruct(getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub value: u64, + #[superstruct(getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub execution_payment: u64, // [Modified in Gloas:EIP7688] pub blob_kzg_commitments: ProgressiveKzgCommitments, + #[superstruct(getter(copy))] pub execution_requests_root: Hash256, + // [New in Heze:EIP7805] + #[superstruct(only(Heze))] + pub inclusion_list_bits: BitVector, } impl SignedRoot for ExecutionPayloadBid {} +impl<'a, E: EthSpec> SignedRoot for ExecutionPayloadBidRef<'a, E> {} + +impl ForkVersionDecode for ExecutionPayloadBid { + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + match fork_name { + ForkName::Base + | ForkName::Altair + | ForkName::Bellatrix + | ForkName::Capella + | ForkName::Deneb + | ForkName::Electra + | ForkName::Fulu => Err(ssz::DecodeError::BytesInvalid(format!( + "unsupported fork for ExecutionPayloadBid: {fork_name}" + ))), + ForkName::Gloas => ExecutionPayloadBidGloas::from_ssz_bytes(bytes).map(Self::Gloas), + ForkName::Heze => ExecutionPayloadBidHeze::from_ssz_bytes(bytes).map(Self::Heze), + } + } +} + +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for ExecutionPayloadBid { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + let convert_err = |e| { + serde::de::Error::custom(format!( + "ExecutionPayloadBid failed to deserialize: {:?}", + e + )) + }; + Ok(match context { + ForkName::Base + | ForkName::Altair + | ForkName::Bellatrix + | ForkName::Capella + | ForkName::Deneb + | ForkName::Electra + | ForkName::Fulu => { + return Err(serde::de::Error::custom(format!( + "ExecutionPayloadBid failed to deserialize: unsupported fork '{}'", + context + ))); + } + ForkName::Gloas => { + Self::Gloas(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Heze => { + Self::Heze(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + }) + } +} + +impl<'a, E: EthSpec> ExecutionPayloadBidRef<'a, E> { + /// Returns the name of the fork pertaining to `self`. + /// + /// Will return an `Err` if `self` has been instantiated to a variant conflicting with the fork + /// dictated by `self.slot()`. + pub fn fork_name(&self, spec: &ChainSpec) -> Result { + let fork_at_slot = spec.fork_name_at_slot::(self.slot()); + let object_fork = self.fork_name_unchecked(); + + if fork_at_slot == object_fork { + Ok(object_fork) + } else { + Err(InconsistentFork { + fork_at_slot, + object_fork, + }) + } + } + + /// Returns the name of the fork pertaining to `self`. + /// Does not check that the fork is consistent with the slot. + pub fn fork_name_unchecked(&self) -> ForkName { + match self { + ExecutionPayloadBidRef::Gloas(_) => ForkName::Gloas, + ExecutionPayloadBidRef::Heze(_) => ForkName::Heze, + } + } +} #[cfg(test)] -mod tests { +mod gloas_tests { use super::*; use crate::Spec; - ssz_and_tree_hash_tests!(ExecutionPayloadBid); + ssz_and_tree_hash_tests!(ExecutionPayloadBidGloas); +} + +#[cfg(test)] +mod heze_tests { + use super::*; + use crate::Spec; + + ssz_and_tree_hash_tests!(ExecutionPayloadBidHeze); + + #[test] + fn inclusion_list_bits_committed_to_signing_root() { + let bid = ExecutionPayloadBidHeze::::default(); + let mut bid_with_bits = bid.clone(); + bid_with_bits + .inclusion_list_bits + .set(0, true) + .expect("bit index should be within the committee size"); + + let domain = Hash256::ZERO; + assert_ne!( + ExecutionPayloadBidRef::Heze(&bid).signing_root(domain), + ExecutionPayloadBidRef::Heze(&bid_with_bits).signing_root(domain), + "inclusion_list_bits must be covered by the Heze bid signing root", + ); + } } diff --git a/consensus/types/src/execution/mod.rs b/consensus/types/src/execution/mod.rs index a41c68ee23c..f0d5bfb4e83 100644 --- a/consensus/types/src/execution/mod.rs +++ b/consensus/types/src/execution/mod.rs @@ -26,7 +26,10 @@ pub use execution_payload::{ ExecutionPayloadHeze, ExecutionPayloadRef, ProgressiveTransactions, ProgressiveWithdrawals, Transaction, Transactions, TransactionsIter, TransactionsRef, WithdrawalsRef, }; -pub use execution_payload_bid::ExecutionPayloadBid; +pub use execution_payload_bid::{ + ExecutionPayloadBid, ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadBidRef, + ExecutionPayloadBidRefMut, +}; pub use execution_payload_envelope::ExecutionPayloadEnvelope; pub use execution_payload_header::{ ExecutionPayloadHeader, ExecutionPayloadHeaderBellatrix, ExecutionPayloadHeaderCapella, @@ -51,7 +54,10 @@ pub use payload::{ NewPayloadRequest, OwnedExecPayload, VersionedHashes, }; pub use signed_bls_to_execution_change::SignedBlsToExecutionChange; -pub use signed_execution_payload_bid::SignedExecutionPayloadBid; +pub use signed_execution_payload_bid::{ + SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, + SignedExecutionPayloadBidRef, +}; pub use signed_execution_payload_envelope::{ ExecutionPayloadBody, ExecutionPayloadHeaderGloas, SignedExecutionPayloadEnvelope, SignedExecutionPayloadEnvelopeSummary, diff --git a/consensus/types/src/execution/signed_execution_payload_bid.rs b/consensus/types/src/execution/signed_execution_payload_bid.rs index 850dbc9b937..9d095a9737e 100644 --- a/consensus/types/src/execution/signed_execution_payload_bid.rs +++ b/consensus/types/src/execution/signed_execution_payload_bid.rs @@ -1,52 +1,194 @@ -use crate::execution::ExecutionPayloadBid; -use crate::{EthSpec, ForkName}; +use crate::execution::{ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadBidRef}; +use crate::{ChainSpec, Epoch, EthSpec, ForkName, ForkVersionDecode, InconsistentFork, Slot}; use bls::Signature; -use context_deserialize::context_deserialize; +use context_deserialize::{ContextDeserialize, context_deserialize}; use educe::Educe; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; +use ssz::Decode; use ssz_derive::{Decode, Encode}; +use superstruct::superstruct; use tree_hash_derive::TreeHash; -#[derive(TreeHash, Debug, Clone, Encode, Decode, Serialize, Deserialize, Educe)] +/// An `ExecutionPayloadBid` and a signature from its builder. +#[superstruct( + variants(Gloas, Heze), + variant_attributes( + derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, Educe), + educe(PartialEq, Hash(bound(E: EthSpec))), + serde(bound = "E: EthSpec"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), + context_deserialize(ForkName), + ) +)] #[cfg_attr( feature = "arbitrary", derive(arbitrary::Arbitrary), arbitrary(bound = "E: EthSpec") )] -#[educe(PartialEq, Hash)] -#[serde(bound = "E: EthSpec")] -#[context_deserialize(ForkName)] -// https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#signedexecutionpayloadbid +#[derive(Debug, Clone, Serialize, Encode, TreeHash, Educe)] +#[educe(PartialEq, Hash(bound(E: EthSpec)))] +#[serde(bound = "E: EthSpec", untagged)] +#[ssz(enum_behaviour = "transparent")] +#[tree_hash(enum_behaviour = "transparent")] +// https://github.com/ethereum/consensus-specs/blob/master/specs/heze/beacon-chain.md#signedexecutionpayloadbid pub struct SignedExecutionPayloadBid { - pub message: ExecutionPayloadBid, + #[superstruct(only(Gloas), partial_getter(rename = "message_gloas"))] + pub message: ExecutionPayloadBidGloas, + #[superstruct(only(Heze), partial_getter(rename = "message_heze"))] + pub message: ExecutionPayloadBidHeze, pub signature: Signature, } impl SignedExecutionPayloadBid { - pub fn epoch(&self) -> crate::Epoch { - self.message.slot.epoch(E::slots_per_epoch()) + /// The bid message as a fork-agnostic reference. + pub fn message(&self) -> ExecutionPayloadBidRef<'_, E> { + match self { + Self::Gloas(inner) => ExecutionPayloadBidRef::Gloas(&inner.message), + Self::Heze(inner) => ExecutionPayloadBidRef::Heze(&inner.message), + } + } + + pub fn fork_name(&self, spec: &ChainSpec) -> Result { + self.message().fork_name(spec) } pub fn slot(&self) -> crate::Slot { - self.message.slot + self.message().slot() + } + + pub fn epoch(&self) -> crate::Epoch { + self.slot().epoch(E::slots_per_epoch()) + } + + pub fn num_blobs_expected(&self) -> usize { + self.message().blob_kzg_commitments().len() } +} +impl SignedExecutionPayloadBidGloas { pub fn empty() -> Self { Self { - message: ExecutionPayloadBid::default(), + message: ExecutionPayloadBidGloas::default(), signature: Signature::empty(), } } +} + +impl SignedExecutionPayloadBidHeze { + pub fn empty() -> Self { + Self { + message: ExecutionPayloadBidHeze::default(), + signature: Signature::empty(), + } + } +} + +impl<'a, E: EthSpec> SignedExecutionPayloadBidRef<'a, E> { + pub fn slot(&self) -> Slot { + self.message().slot() + } + + pub fn epoch(&self) -> Epoch { + self.slot().epoch(E::slots_per_epoch()) + } pub fn num_blobs_expected(&self) -> usize { - self.message.blob_kzg_commitments.len() + self.message().blob_kzg_commitments().len() + } + + /// The bid message as a fork-agnostic reference. + pub fn message(&self) -> ExecutionPayloadBidRef<'a, E> { + match self { + SignedExecutionPayloadBidRef::Gloas(inner) => { + ExecutionPayloadBidRef::Gloas(&inner.message) + } + SignedExecutionPayloadBidRef::Heze(inner) => { + ExecutionPayloadBidRef::Heze(&inner.message) + } + } } + + /// Clone this reference into an owned `SignedExecutionPayloadBid`. + pub fn clone_as_signed_execution_payload_bid(self) -> SignedExecutionPayloadBid { + match self { + SignedExecutionPayloadBidRef::Gloas(bid) => { + SignedExecutionPayloadBid::Gloas(bid.clone()) + } + SignedExecutionPayloadBidRef::Heze(bid) => SignedExecutionPayloadBid::Heze(bid.clone()), + } + } +} + +impl ForkVersionDecode for SignedExecutionPayloadBid { + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + match fork_name { + ForkName::Base + | ForkName::Altair + | ForkName::Bellatrix + | ForkName::Capella + | ForkName::Deneb + | ForkName::Electra + | ForkName::Fulu => Err(ssz::DecodeError::BytesInvalid(format!( + "unsupported fork for SignedExecutionPayloadBid: {fork_name}" + ))), + ForkName::Gloas => { + SignedExecutionPayloadBidGloas::from_ssz_bytes(bytes).map(Self::Gloas) + } + ForkName::Heze => SignedExecutionPayloadBidHeze::from_ssz_bytes(bytes).map(Self::Heze), + } + } +} + +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for SignedExecutionPayloadBid { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + let convert_err = |e| { + serde::de::Error::custom(format!( + "SignedExecutionPayloadBid failed to deserialize: {:?}", + e + )) + }; + Ok(match context { + ForkName::Base + | ForkName::Altair + | ForkName::Bellatrix + | ForkName::Capella + | ForkName::Deneb + | ForkName::Electra + | ForkName::Fulu => { + return Err(serde::de::Error::custom(format!( + "SignedExecutionPayloadBid failed to deserialize: unsupported fork '{}'", + context + ))); + } + ForkName::Gloas => { + Self::Gloas(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Heze => { + Self::Heze(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + }) + } +} + +#[cfg(test)] +mod gloas_tests { + use super::*; + use crate::Spec; + + ssz_and_tree_hash_tests!(SignedExecutionPayloadBidGloas); } #[cfg(test)] -mod tests { +mod heze_tests { use super::*; use crate::Spec; - ssz_and_tree_hash_tests!(SignedExecutionPayloadBid); + ssz_and_tree_hash_tests!(SignedExecutionPayloadBidHeze); } diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 2067f39416d..81b32545ed3 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -23,7 +23,8 @@ use tree_hash_derive::TreeHash; use typenum::Unsigned; use crate::{ - ExecutionBlockHash, ExecutionPayloadBid, Withdrawal, + ExecutionBlockHash, ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadBidRef, + ExecutionPayloadBidRefMut, Withdrawal, attestation::{ AttestationData, AttestationDuty, BeaconCommittee, Checkpoint, CommitteeIndex, PTC, ParticipationFlags, PendingAttestation, @@ -783,9 +784,19 @@ where pub builder_pending_withdrawals: ProgressiveList, #[cfg_attr(feature = "arbitrary", arbitrary(default))] - #[superstruct(only(Gloas, Heze))] + #[superstruct( + only(Gloas), + partial_getter(rename = "latest_execution_payload_bid_gloas") + )] + #[metastruct(exclude_from(tree_lists))] + pub latest_execution_payload_bid: ExecutionPayloadBidGloas, + #[cfg_attr(feature = "arbitrary", arbitrary(default))] + #[superstruct( + only(Heze), + partial_getter(rename = "latest_execution_payload_bid_heze") + )] #[metastruct(exclude_from(tree_lists))] - pub latest_execution_payload_bid: ExecutionPayloadBid, + pub latest_execution_payload_bid: ExecutionPayloadBidHeze, #[compare_fields(as_iter)] #[cfg_attr(feature = "arbitrary", arbitrary(default))] @@ -1556,6 +1567,47 @@ impl BeaconState { } } + /// Convenience accessor for `latest_execution_payload_bid` as an `ExecutionPayloadBidRef`. + pub fn latest_execution_payload_bid( + &self, + ) -> Result, BeaconStateError> { + match self { + BeaconState::Base(_) + | BeaconState::Altair(_) + | BeaconState::Bellatrix(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) + | BeaconState::Electra(_) + | BeaconState::Fulu(_) => Err(BeaconStateError::IncorrectStateVariant), + BeaconState::Gloas(state) => Ok(ExecutionPayloadBidRef::Gloas( + &state.latest_execution_payload_bid, + )), + BeaconState::Heze(state) => Ok(ExecutionPayloadBidRef::Heze( + &state.latest_execution_payload_bid, + )), + } + } + + pub fn latest_execution_payload_bid_mut( + &mut self, + ) -> Result, BeaconStateError> { + match self { + BeaconState::Base(_) + | BeaconState::Altair(_) + | BeaconState::Bellatrix(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) + | BeaconState::Electra(_) + | BeaconState::Fulu(_) => Err(BeaconStateError::IncorrectStateVariant), + BeaconState::Gloas(state) => Ok(ExecutionPayloadBidRefMut::Gloas( + &mut state.latest_execution_payload_bid, + )), + BeaconState::Heze(state) => Ok(ExecutionPayloadBidRefMut::Heze( + &mut state.latest_execution_payload_bid, + )), + } + } + /// Return `true` if the validator who produced `slot_signature` is eligible to aggregate. /// /// Spec v0.12.1 diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 54119eeb551..27133b04989 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -64,7 +64,7 @@ pub struct VoluntaryExitChurn { } /// Newtype for testing execution payload bids. -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone)] pub struct ExecutionPayloadBidBlock { signed_bid: SignedExecutionPayloadBid, } @@ -133,7 +133,7 @@ impl Operation for Attestation { initialize_progressive_balances_cache(state, spec)?; let mut ctxt = ConsensusContext::new(state.slot()); if state.fork_name_unchecked().gloas_enabled() { - let parent_slot = Some(state.latest_execution_payload_bid()?.slot); + let parent_slot = Some(state.latest_execution_payload_bid()?.slot()); gloas::process_attestation( state, self.to_ref(), @@ -551,8 +551,11 @@ impl Operation for ExecutionPayloadBidBlock { fork_name.gloas_enabled() } - fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { - ssz_decode_file(path).map(|signed_bid| ExecutionPayloadBidBlock { signed_bid }) + fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result { + ssz_decode_file_with(path, |bytes| { + SignedExecutionPayloadBid::from_ssz_bytes_by_fork(bytes, fork_name) + }) + .map(|signed_bid| ExecutionPayloadBidBlock { signed_bid }) } fn apply_to( @@ -561,7 +564,12 @@ impl Operation for ExecutionPayloadBidBlock { spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { - process_execution_payload_bid(state, &self.signed_bid, VerifySignatures::True, spec)?; + process_execution_payload_bid( + state, + self.signed_bid.to_ref(), + VerifySignatures::True, + spec, + )?; Ok(()) } } diff --git a/testing/ef_tests/src/type_name.rs b/testing/ef_tests/src/type_name.rs index 133bbc42d01..c1446e0d228 100644 --- a/testing/ef_tests/src/type_name.rs +++ b/testing/ef_tests/src/type_name.rs @@ -98,7 +98,11 @@ type_name_generic!(ExecutionPayloadHeaderDeneb, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadHeaderElectra, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadHeaderFulu, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadBid); +type_name_generic!(ExecutionPayloadBidGloas, "ExecutionPayloadBid"); +type_name_generic!(ExecutionPayloadBidHeze, "ExecutionPayloadBid"); type_name_generic!(SignedExecutionPayloadBid); +type_name_generic!(SignedExecutionPayloadBidGloas, "SignedExecutionPayloadBid"); +type_name_generic!(SignedExecutionPayloadBidHeze, "SignedExecutionPayloadBid"); type_name_generic!(ExecutionRequestsElectra, "ExecutionRequests"); type_name_generic!(ExecutionRequestsGloas, "ExecutionRequests"); type_name_generic!(ExecutionPayloadEnvelope); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index f6394ba6bf2..61de98ebba2 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -257,11 +257,10 @@ mod ssz_static { use types::state::HistoricalSummary; use types::{ AttesterSlashingBase, AttesterSlashingElectra, Builder, BuilderPendingPayment, - BuilderPendingWithdrawal, ConsolidationRequest, DepositRequest, ExecutionPayloadBid, - ExecutionPayloadEnvelope, IndexedPayloadAttestation, LightClientBootstrapAltair, - PayloadAttestation, PayloadAttestationData, PayloadAttestationMessage, PendingDeposit, - PendingPartialWithdrawal, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, - WithdrawalRequest, *, + BuilderPendingWithdrawal, ConsolidationRequest, DepositRequest, ExecutionPayloadEnvelope, + IndexedPayloadAttestation, LightClientBootstrapAltair, PayloadAttestation, + PayloadAttestationData, PayloadAttestationMessage, PendingDeposit, + PendingPartialWithdrawal, SignedExecutionPayloadEnvelope, WithdrawalRequest, *, }; ssz_static_test!(attestation_data, AttestationData); @@ -470,12 +469,14 @@ mod ssz_static { #[test] fn execution_payload_bid() { - SszStaticHandler::, Spec>::gloas_and_later().run(); + SszStaticHandler::, Spec>::gloas_only().run(); + SszStaticHandler::, Spec>::heze_only().run(); } #[test] fn signed_execution_payload_bid() { - SszStaticHandler::, Spec>::gloas_and_later().run(); + SszStaticHandler::, Spec>::gloas_only().run(); + SszStaticHandler::, Spec>::heze_only().run(); } #[test] diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index 7e9f22a6481..d772d0d2a57 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -340,7 +340,7 @@ impl EnvelopeAction { } (true, ProduceBlockV4Response::BlockOnly(block)) => { let action = match block.body().signed_execution_payload_bid() { - Ok(bid) if bid.message.builder_index == BUILDER_INDEX_SELF_BUILD => { + Ok(bid) if bid.message().builder_index() == BUILDER_INDEX_SELF_BUILD => { warn!( slot = slot.as_u64(), "Beacon node omitted the payload contents for a self-built block, \ @@ -351,7 +351,7 @@ impl EnvelopeAction { Ok(bid) => { info!( slot = slot.as_u64(), - builder_index = bid.message.builder_index, + builder_index = bid.message().builder_index(), "Builder bid won; the builder reveals the payload envelope" ); Self::Skip @@ -369,13 +369,13 @@ impl EnvelopeAction { (false, response) => { let block = response.into_block(); let action = match block.body().signed_execution_payload_bid() { - Ok(bid) if bid.message.builder_index == BUILDER_INDEX_SELF_BUILD => { + Ok(bid) if bid.message().builder_index() == BUILDER_INDEX_SELF_BUILD => { Self::Fetch(block.canonical_root()) } Ok(bid) => { info!( slot = slot.as_u64(), - builder_index = bid.message.builder_index, + builder_index = bid.message().builder_index(), "Builder bid won; the builder reveals the payload envelope" ); Self::Skip From 22f43dc4b004b3033b6c937a4ac5d9c1046da20e Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 7 Oct 2026 20:23:11 +0300 Subject: [PATCH 42/44] Produce inclusion lists for the current slot when the clock didn't pass over the due time --- .../src/inclusion_list_service.rs | 130 +++++++++++++++--- 1 file changed, 113 insertions(+), 17 deletions(-) diff --git a/validator_client/validator_services/src/inclusion_list_service.rs b/validator_client/validator_services/src/inclusion_list_service.rs index ba515615015..0237fdf2bb6 100644 --- a/validator_client/validator_services/src/inclusion_list_service.rs +++ b/validator_client/validator_services/src/inclusion_list_service.rs @@ -132,38 +132,37 @@ where .saturating_sub(INCLUSION_LIST_PRODUCTION_MARGIN) } + /// Waits until inclusion lists are due for production and returns the slot to produce them for. async fn wait_for_inclusion_list_production_due(&self) -> Option { let slot_duration = self.chain_spec.get_slot_duration(); - let inclusion_list_production_due = self.inclusion_list_production_due(); - let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() else { + let Some(slot) = self.inclusion_list_production_slot() else { error!("Failed to read slot clock"); sleep(slot_duration).await; return None; }; - let Some(current_slot) = self.slot_clock.now() else { - error!("Failed to read slot clock after trigger"); - return None; - }; - - // Ensure that the current slot is in the Heze fork + // Ensure that the slot is in the Heze fork if !self .chain_spec - .fork_name_at_slot::(current_slot) + .fork_name_at_slot::(slot) .heze_enabled() { let duration_to_next_epoch = self .slot_clock .duration_to_next_epoch(S::E::slots_per_epoch()) - .unwrap_or_else(|| { - self.chain_spec.get_slot_duration() * S::E::slots_per_epoch() as u32 - }); + .unwrap_or_else(|| slot_duration * S::E::slots_per_epoch() as u32); sleep(duration_to_next_epoch).await; return None; } - sleep(duration_to_next_slot + inclusion_list_production_due).await; + let Some(duration_to_production_due) = self.duration_to_inclusion_list_production_due(slot) + else { + error!("Failed to read slot clock"); + sleep(slot_duration).await; + return None; + }; + sleep(duration_to_production_due).await; let Some(current_slot) = self.slot_clock.now() else { error!("Failed to read slot clock after sleep"); @@ -173,6 +172,26 @@ where Some(current_slot) } + /// The slot to produce inclusion lists for next. + /// Returns the current slot if its production time has not passed yet, otherwise the next slot. + fn inclusion_list_production_slot(&self) -> Option { + let current_slot = self.slot_clock.now()?; + let time_into_slot = self.slot_clock.millis_from_current_slot_start()?; + + if time_into_slot < self.inclusion_list_production_due() { + Some(current_slot) + } else { + Some(current_slot + 1) + } + } + + /// Duration until inclusion lists are due for production at `production_slot`. + fn duration_to_inclusion_list_production_due(&self, production_slot: Slot) -> Option { + let production_due_at = + self.slot_clock.start_of(production_slot)? + self.inclusion_list_production_due(); + Some(production_due_at.saturating_sub(self.slot_clock.now_duration()?)) + } + /// Produce the inclusion list data for `slot`, returned alongside the duties to sign. /// /// Returns `Ok(None)` when there is nothing to produce (the slot's duties have not been @@ -453,6 +472,55 @@ mod tests { // This call should yield no slot, so nothing should be produced, signed or published. advance_time(&harness.service.slot_clock, Duration::from_secs(1)).await; assert_eq!(service_wait.as_mut().now_or_never(), Some(Ok(()))); + + // The next wait targets the first Heze slot, 1s into it + let first_heze_slot = Slot::new(E::slots_per_epoch()); + let production_due = service.inclusion_list_production_due(); + let service_wait = service.wait_for_inclusion_list_production_due(); + tokio::pin!(service_wait); + assert!(service_wait.as_mut().now_or_never().is_none()); + advance_time( + &harness.service.slot_clock, + production_due - Duration::from_secs(1), + ) + .await; + assert!(service_wait.as_mut().now_or_never().is_none()); + advance_time(&harness.service.slot_clock, Duration::from_secs(1)).await; + assert_eq!( + service_wait.as_mut().now_or_never().unwrap(), + Some(first_heze_slot) + ); + } + + #[tokio::test] + async fn targets_first_heze_slot_from_last_gloas_slot() { + tokio::time::pause(); + + let harness = TestHarness::new_with_heze_at(1, Epoch::new(1)).await; + let service = &harness.service; + let production_due = service.inclusion_list_production_due(); + let first_heze_slot = Slot::new(E::slots_per_epoch()); + + // Last pre-Heze slot, past its production time + let last_pre_heze_slot = first_heze_slot - 1; + advance_time( + &service.slot_clock, + service.slot_clock.start_of(last_pre_heze_slot).unwrap() + production_due, + ) + .await; + let service_wait = service.wait_for_inclusion_list_production_due(); + tokio::pin!(service_wait); + assert!(service_wait.as_mut().now_or_never().is_none()); + + // Advance to the first Heze slot's production time + let duration_to_wait = service.slot_clock.duration_to_next_slot().unwrap() + production_due; + advance_time(&service.slot_clock, duration_to_wait).await; + assert!(service_wait.as_mut().now_or_never().is_none()); + advance_time(&service.slot_clock, Duration::from_secs(1)).await; + assert_eq!( + service_wait.as_mut().now_or_never().unwrap(), + Some(first_heze_slot) + ); } #[tokio::test] @@ -461,6 +529,11 @@ mod tests { let harness = TestHarness::new_with_validators(1).await; let service = &harness.service; + let production_due = service.inclusion_list_production_due(); + assert_eq!(production_due, Duration::from_secs(7)); + + // Past the current slot's production time, so the next slot is targeted + advance_time(&service.slot_clock, production_due).await; let service_wait = service.wait_for_inclusion_list_production_due(); tokio::pin!(service_wait); @@ -468,8 +541,6 @@ mod tests { assert!(service_wait.as_mut().now_or_never().is_none()); // 7s into the next slot: 1s before the inclusion list deadline - let production_due = service.inclusion_list_production_due(); - assert_eq!(production_due, Duration::from_secs(7)); let duration_to_wait = service.slot_clock.duration_to_next_slot().unwrap() + production_due; // Advance both slot_clock and tokio::time to 19s advance_time(&harness.service.slot_clock, duration_to_wait).await; @@ -485,6 +556,33 @@ mod tests { ); } + #[tokio::test] + async fn waits_until_production_due_in_current_slot() { + tokio::time::pause(); + + let harness = TestHarness::new_with_validators(1).await; + let service = &harness.service; + let production_due = service.inclusion_list_production_due(); + + // 2s into the current slot + advance_time(&service.slot_clock, Duration::from_secs(2)).await; + let service_wait = service.wait_for_inclusion_list_production_due(); + tokio::pin!(service_wait); + assert!(service_wait.as_mut().now_or_never().is_none()); + + advance_time(&service.slot_clock, production_due - Duration::from_secs(2)).await; + assert!( + service_wait.as_mut().now_or_never().is_none(), + "Function should return None before the sleep duration has elapsed" + ); + + advance_time(&service.slot_clock, Duration::from_secs(1)).await; + assert_eq!( + service_wait.as_mut().now_or_never().unwrap(), + Some(Slot::new(0)) + ); + } + #[tokio::test] async fn no_duties_no_fetch_no_publish() { let mut harness = TestHarness::new_with_validators(3).await; @@ -545,8 +643,6 @@ mod tests { assert!(result.is_err()); } - /// First BN errors on the produce endpoint, second serves: `first_success` walks - /// past and transactions come from the second BN. #[tokio::test] async fn produce_falls_back_to_second_bn() { let mut harness = TestHarness::new_with_validators(3).await; From fe193a31289d61339d0b8779d6eb2883e1c6c56c Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 7 Oct 2026 22:03:03 +0300 Subject: [PATCH 43/44] Use ProgressiveTransactions for inclusion list transactions in the service --- .../src/inclusion_list_service.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/validator_client/validator_services/src/inclusion_list_service.rs b/validator_client/validator_services/src/inclusion_list_service.rs index 0237fdf2bb6..85ec16f0e4c 100644 --- a/validator_client/validator_services/src/inclusion_list_service.rs +++ b/validator_client/validator_services/src/inclusion_list_service.rs @@ -1,6 +1,6 @@ use crate::duties_service::DutiesService; use beacon_node_fallback::BeaconNodeFallback; -use eth2::types::{InclusionListDuty, InclusionListTransactions}; +use eth2::types::InclusionListDuty; use logging::crit; use slot_clock::SlotClock; use std::ops::Deref; @@ -9,7 +9,10 @@ use std::time::Duration; use task_executor::TaskExecutor; use tokio::time::sleep; use tracing::{debug, error, info}; -use types::{ChainSpec, EthSpec, ForkName, Hash256, InclusionList, SignedInclusionList, Slot}; +use types::{ + ChainSpec, EthSpec, ForkName, Hash256, InclusionList, ProgressiveTransactions, + SignedInclusionList, Slot, +}; use validator_store::ValidatorStore; type DependentRoot = Hash256; @@ -21,7 +24,7 @@ const INCLUSION_LIST_PRODUCTION_MARGIN: Duration = Duration::from_secs(1); struct InclusionListData { dependent_root: DependentRoot, - transactions: InclusionListTransactions, + transactions: ProgressiveTransactions, } pub struct Inner { @@ -223,7 +226,7 @@ where beacon_node .get_validator_inclusion_list(slot) .await - .map(|resp| resp.data) + .map(|resp| resp.data.transactions) }) .await .map_err(|e| e.to_string())?; @@ -231,9 +234,8 @@ where debug!( %slot, ?dependent_root, - tx_count = transactions.transactions.len(), + tx_count = transactions.len(), tx_bytes = transactions - .transactions .iter() .map(|tx| tx.len()) .sum::(), @@ -261,7 +263,7 @@ where slot, validator_index: duty.validator_index, dependent_root: inclusion_list_data.dependent_root, - transactions: inclusion_list_data.transactions.transactions.clone(), + transactions: inclusion_list_data.transactions.clone(), }; match self @@ -361,6 +363,7 @@ where mod tests { use super::*; use crate::duties_service::DutiesServiceBuilder; + use eth2::types::InclusionListTransactions; use futures::FutureExt; use slot_clock::ManualSlotClock; use std::time::Duration; @@ -670,7 +673,7 @@ mod tests { .unwrap(); assert_eq!(duties.len(), 3); assert_eq!(data.dependent_root, dependent_root); - assert_eq!(data.transactions, transactions); + assert_eq!(data.transactions, transactions.transactions); bn2_mock.expect(1).assert(); } From 1ffb3c12e9964580306681bc9279149daa81eb35 Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 7 Oct 2026 22:23:10 +0300 Subject: [PATCH 44/44] Cleanup --- consensus/types/src/core/chain_spec.rs | 4 ++-- .../validator_services/src/inclusion_list_service.rs | 4 ++-- validator_client/validator_store/src/lib.rs | 11 ++++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index 1cce81d6377..33ab23aede0 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -1644,8 +1644,8 @@ impl ChainSpec { sync_message_due: Duration::from_millis(1999), sync_message_due_gloas: Duration::from_millis(1500), contribution_and_proof_due: Duration::from_millis(4000), - inclusion_list_due: Duration::from_millis(4000), contribution_and_proof_due_gloas: Duration::from_millis(3000), + inclusion_list_due: Duration::from_millis(4000), // Networking Fulu blob_schedule: BlobSchedule::default(), @@ -1751,8 +1751,8 @@ impl ChainSpec { sync_message_due: Duration::from_millis(1666), sync_message_due_gloas: Duration::from_millis(1250), contribution_and_proof_due: Duration::from_millis(3333), - inclusion_list_due: Duration::from_millis(3333), contribution_and_proof_due_gloas: Duration::from_millis(2500), + inclusion_list_due: Duration::from_millis(3333), /* * Reward and penalty quotients diff --git a/validator_client/validator_services/src/inclusion_list_service.rs b/validator_client/validator_services/src/inclusion_list_service.rs index 85ec16f0e4c..9890094d19f 100644 --- a/validator_client/validator_services/src/inclusion_list_service.rs +++ b/validator_client/validator_services/src/inclusion_list_service.rs @@ -540,7 +540,7 @@ mod tests { let service_wait = service.wait_for_inclusion_list_production_due(); tokio::pin!(service_wait); - // Start the timer and registers the sleep timer with tokio + // Start the wait, which registers the sleep timer with tokio assert!(service_wait.as_mut().now_or_never().is_none()); // 7s into the next slot: 1s before the inclusion list deadline @@ -712,7 +712,7 @@ mod tests { .await .unwrap(); - // One POST per duty,and no JSON fallback + // One POST per duty, and no JSON fallback ssz_mock.expect(3).assert(); json_mock.expect(0).assert(); diff --git a/validator_client/validator_store/src/lib.rs b/validator_client/validator_store/src/lib.rs index 84b79f53bd0..4b17f251346 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -227,6 +227,12 @@ pub trait ValidatorStore: Send + Sync { request_auth_v1: RequestAuth, ) -> impl Future>> + Send; + fn sign_inclusion_list( + &self, + validator_pubkey: PublicKeyBytes, + inclusion_list: InclusionList, + ) -> impl Future>> + Send; + /// Returns `ProposalData` for the provided `pubkey` if it exists in `InitializedValidators`. /// `ProposalData` fields include defaulting logic described in `get_fee_recipient_defaulting`, /// `get_gas_limit_defaulting`, and `get_builder_proposals_defaulting`. @@ -235,11 +241,6 @@ pub trait ValidatorStore: Send + Sync { /// Like `proposal_data`, with the gas limit schedule evaluated at `epoch`. fn proposal_data_at_epoch(&self, pubkey: &PublicKeyBytes, epoch: Epoch) -> Option; - fn sign_inclusion_list( - &self, - validator_pubkey: PublicKeyBytes, - inclusion_list: InclusionList, - ) -> impl Future>> + Send; } #[derive(Debug)]