From 474d0cc36f1432932df97d2f6d655e5718ba983a Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 6 Apr 2026 01:53:47 -0700 Subject: [PATCH 01/34] Set gloas attestation data index to 0 or 1 depending on payload --- beacon_node/beacon_chain/src/beacon_chain.rs | 25 ++++++++++++++++++- .../beacon_chain/src/early_attester_cache.rs | 1 + beacon_node/beacon_chain/src/test_utils.rs | 2 ++ consensus/fork_choice/src/fork_choice.rs | 5 ++++ .../types/src/attestation/attestation.rs | 10 +++++++- .../src/attestation_service.rs | 1 + 6 files changed, 42 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index e226c707a4..6d316e3271 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1941,6 +1941,7 @@ impl BeaconChain { let beacon_block_root; let beacon_state_root; let target; + let is_attesting_to_head_slot; let current_epoch_attesting_info: Option<(Checkpoint, usize)>; let head_timer = metrics::start_timer(&metrics::ATTESTATION_PRODUCTION_HEAD_SCRAPE_SECONDS); let head_span = debug_span!("attestation_production_head_scrape").entered(); @@ -1977,7 +1978,8 @@ impl BeaconChain { }); } - if request_slot >= head_state.slot() { + is_attesting_to_head_slot = request_slot >= head_state.slot(); + if is_attesting_to_head_slot { // When attesting to the head slot or later, always use the head of the chain. beacon_block_root = head.beacon_block_root; beacon_state_root = head.beacon_state_root(); @@ -2080,6 +2082,26 @@ impl BeaconChain { ) }; + // TODO(gloas): add integration test: verify that + // `produce_unaggregated_attestation` sets index=1 when payload received for a prior + // slot, index=0 for same-slot, and index=0 when payload not received. + // + // For gloas the attestation data index indicates payload presence: + // `payload_present=false` for same-slot attestations or when payload not received. + // `payload_present=true` when attesting to a prior slot whose payload has been received. + let payload_present = if self + .spec + .fork_name_at_slot::(request_slot) + .gloas_enabled() + && !is_attesting_to_head_slot + { + self.canonical_head + .fork_choice_read_lock() + .is_payload_received(&beacon_block_root) + } else { + false + }; + Ok(Attestation::::empty_for_signing( request_index, committee_len, @@ -2087,6 +2109,7 @@ impl BeaconChain { beacon_block_root, justified_checkpoint, target, + payload_present, &self.spec, )?) } diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index 752e4d1a96..5b55c8e2da 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -204,6 +204,7 @@ impl EarlyAttesterCache { item.beacon_block_root, item.source, item.target, + false, spec, ) .map_err(Error::AttestationError)?; diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 13dcf22108..baff1424ba 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -1400,6 +1400,7 @@ where epoch, root: target_root, }, + false, &self.spec, )?; @@ -1509,6 +1510,7 @@ where epoch, root: target_root, }, + false, &self.spec, )?) } diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 92fd4c1faf..6c1a79f820 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1484,6 +1484,11 @@ where && self.is_finalized_checkpoint_or_descendant(*block_root) } + /// Returns `true` if the block's execution payload envelope has been received. + pub fn is_payload_received(&self, block_root: &Hash256) -> bool { + self.proto_array.is_payload_received(block_root) + } + /// Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root. pub fn get_block(&self, block_root: &Hash256) -> Option { if self.is_finalized_checkpoint_or_descendant(*block_root) { diff --git a/consensus/types/src/attestation/attestation.rs b/consensus/types/src/attestation/attestation.rs index 693b5889f5..5131be4424 100644 --- a/consensus/types/src/attestation/attestation.rs +++ b/consensus/types/src/attestation/attestation.rs @@ -109,6 +109,7 @@ impl Attestation { beacon_block_root: Hash256, source: Checkpoint, target: Checkpoint, + payload_present: bool, spec: &ChainSpec, ) -> Result { if spec.fork_name_at_slot::(slot).electra_enabled() { @@ -116,12 +117,19 @@ impl Attestation { committee_bits .set(committee_index as usize, true) .map_err(|_| Error::InvalidCommitteeIndex)?; + // Gloas attestation data index now indicates payload presence. + // Pre-gloas index is always 0. + let index = if spec.fork_name_at_slot::(slot).gloas_enabled() && payload_present { + 1u64 + } else { + 0u64 + }; Ok(Attestation::Electra(AttestationElectra { aggregation_bits: BitList::with_capacity(committee_length) .map_err(|_| Error::InvalidCommitteeLength)?, data: AttestationData { slot, - index: 0u64, + index, beacon_block_root, source, target, diff --git a/validator_client/validator_services/src/attestation_service.rs b/validator_client/validator_services/src/attestation_service.rs index fe808efd88..de55422f49 100644 --- a/validator_client/validator_services/src/attestation_service.rs +++ b/validator_client/validator_services/src/attestation_service.rs @@ -546,6 +546,7 @@ impl AttestationService attestation, From 2c8df63f00de50970b7c63fefde56cf71bf80165 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 6 Apr 2026 02:01:50 -0700 Subject: [PATCH 02/34] allow too many args --- consensus/types/src/attestation/attestation.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/consensus/types/src/attestation/attestation.rs b/consensus/types/src/attestation/attestation.rs index 5131be4424..187003ec8d 100644 --- a/consensus/types/src/attestation/attestation.rs +++ b/consensus/types/src/attestation/attestation.rs @@ -101,7 +101,9 @@ impl Hash for Attestation { } impl Attestation { + /// Produces an attestation with empty signature. + #[allow(clippy::too_many_arguments)] pub fn empty_for_signing( committee_index: u64, committee_length: usize, From 2b1f0435213e7bf89f4a131e6fd04077aedd12e4 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 6 Apr 2026 02:15:02 -0700 Subject: [PATCH 03/34] FMT --- consensus/types/src/attestation/attestation.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/consensus/types/src/attestation/attestation.rs b/consensus/types/src/attestation/attestation.rs index 187003ec8d..28059efee6 100644 --- a/consensus/types/src/attestation/attestation.rs +++ b/consensus/types/src/attestation/attestation.rs @@ -101,7 +101,6 @@ impl Hash for Attestation { } impl Attestation { - /// Produces an attestation with empty signature. #[allow(clippy::too_many_arguments)] pub fn empty_for_signing( From 036d9c995d50fb229c65a9304db3a2cab92efb17 Mon Sep 17 00:00:00 2001 From: hopinheimer Date: Fri, 17 Apr 2026 19:40:15 -0400 Subject: [PATCH 04/34] adding payload verification `handlers` --- .../src/payload_attestation_verification.rs | 326 ++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 beacon_node/beacon_chain/src/payload_attestation_verification.rs diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification.rs b/beacon_node/beacon_chain/src/payload_attestation_verification.rs new file mode 100644 index 0000000000..308a9c7cae --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_attestation_verification.rs @@ -0,0 +1,326 @@ +//! Provides verification for `PayloadAttestationMessage` received from the gossip network. +//! +//! Similar to `crate::attestation_verification`, we try to avoid doing duplicate verification +//! work as a payload attestation message passes through different stages of verification. +//! +//! ```ignore +//! types::PayloadAttestationMessage +//! | +//! ▼ +//! IndexedPayloadAttestationMessage +//! | +//! ▼ +//! VerifiedPayloadAttestationMessage +//! ``` + +use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; +use bls::AggregateSignature; +use slot_clock::SlotClock; +use state_processing::per_block_processing::signature_sets::indexed_payload_attestation_signature_set; +use std::borrow::Cow; +use strum::AsRefStr; +use types::{ + BeaconState, BeaconStateError, ChainSpec, EthSpec, Hash256, IndexedPayloadAttestation, PTC, + PayloadAttestationMessage, Slot, +}; + +/// Returned when a payload attestation message was not successfully verified. It might not have +/// been verified for two reasons: +/// +/// - The message is malformed or inappropriate for the context (indicated by all variants +/// other than `BeaconChainError`). +/// - The application encountered an internal error whilst attempting to determine validity +/// (the `BeaconChainError` variant) +#[derive(Debug, AsRefStr)] +pub enum Error { + /// The payload attestation message is from a slot that is later than the current slot + /// (with respect to the gossip clock disparity). + /// + /// ## Peer scoring + /// + /// Assuming the local clock is correct, the peer has sent an invalid message. + FutureSlot { + message_slot: Slot, + latest_permissible_slot: Slot, + }, + /// The payload attestation message is from a slot that is prior to the earliest + /// permissible slot (with respect to the gossip clock disparity). + /// + /// ## Peer scoring + /// + /// Assuming the local clock is correct, the peer has sent an invalid message. + PastSlot { + message_slot: Slot, + earliest_permissible_slot: Slot, + }, + /// We have already observed a valid payload attestation message from this validator + /// for this slot. + /// + /// ## Peer scoring + /// + /// The peer is not necessarily faulty. + PriorPayloadAttestationMessageKnown { validator_index: u64, slot: Slot }, + /// The beacon block referenced by the payload attestation message is not known. + /// + /// ## Peer scoring + /// + /// The attestation points to a block we have not yet imported. It's unclear if the + /// attestation is valid or not. + UnknownHeadBlock { beacon_block_root: Hash256 }, + /// The validator index is not a member of the PTC for this slot. + /// + /// ## Peer scoring + /// + /// The peer has sent an invalid message. + NotInPTC { validator_index: u64, slot: Slot }, + /// The validator index is unknown. + /// + /// ## Peer scoring + /// + /// The peer has sent an invalid message. + UnknownValidatorIndex(u64), + /// The signature on the payload attestation message is invalid. + /// + /// ## Peer scoring + /// + /// The peer has sent an invalid message. + InvalidSignature, + /// There was an error whilst processing the payload attestation message. It is not known + /// if it is valid or invalid. + /// + /// ## Peer scoring + /// + /// We were unable to process this message due to an internal error. It's unclear if the + /// message is valid. + BeaconChainError(Box), + /// An error reading beacon state. + /// + /// ## Peer scoring + /// + /// We were unable to process this message due to an internal error. + BeaconStateError(BeaconStateError), +} + +impl From for Error { + fn from(e: BeaconChainError) -> Self { + Error::BeaconChainError(Box::new(e)) + } +} + +impl From for Error { + fn from(e: BeaconStateError) -> Self { + Error::BeaconStateError(e) + } +} + +/// Wraps a `PayloadAttestationMessage` that has passed early and middle checks but has *not* +/// undergone signature verification. +struct IndexedPayloadAttestationMessage { + payload_attestation_message: PayloadAttestationMessage, + indexed_payload_attestation: IndexedPayloadAttestation, + ptc: PTC, +} + +/// Wraps a `PayloadAttestationMessage` that has been fully verified for propagation on the +/// gossip network. +#[derive(Clone, Debug)] +pub struct VerifiedPayloadAttestationMessage { + payload_attestation_message: PayloadAttestationMessage, + indexed_payload_attestation: IndexedPayloadAttestation, + ptc: PTC, +} + +impl IndexedPayloadAttestationMessage { + fn verify_early_checks( + payload_attestation_message: &PayloadAttestationMessage, + chain: &BeaconChain, + ) -> Result<(), Error> { + let slot = payload_attestation_message.data.slot; + let validator_index = payload_attestation_message.validator_index; + + verify_propagation_slot_range(&chain.slot_clock, slot, &chain.spec)?; + + if chain + .observed_payload_attesters + .read() + .validator_has_been_observed(slot, validator_index as usize) + .map_err(BeaconChainError::from)? + { + return Err(Error::PriorPayloadAttestationMessageKnown { + validator_index, + slot, + }); + } + + let beacon_block_root = payload_attestation_message.data.beacon_block_root; + if chain + .canonical_head + .fork_choice_read_lock() + .get_block(&beacon_block_root) + .is_none() + { + return Err(Error::UnknownHeadBlock { beacon_block_root }); + } + + Ok(()) + } + + #[allow(clippy::type_complexity)] + fn verify_middle_checks( + payload_attestation_message: &PayloadAttestationMessage, + chain: &BeaconChain, + state: &BeaconState, + ) -> Result<(IndexedPayloadAttestation, PTC), Error> { + let slot = payload_attestation_message.data.slot; + let validator_index = payload_attestation_message.validator_index; + + let ptc = state.get_ptc(slot, &chain.spec)?; + if !ptc.0.contains(&(validator_index as usize)) { + return Err(Error::NotInPTC { + validator_index, + slot, + }); + } + + let indexed_payload_attestation = IndexedPayloadAttestation { + attesting_indices: vec![validator_index] + .try_into() + .map_err(|_| Error::UnknownValidatorIndex(validator_index))?, + data: payload_attestation_message.data.clone(), + signature: AggregateSignature::from(&payload_attestation_message.signature), + }; + + Ok((indexed_payload_attestation, ptc)) + } + + /// Runs early and middle checks, producing an intermediate verified form. + fn verify( + payload_attestation_message: PayloadAttestationMessage, + chain: &BeaconChain, + state: &BeaconState, + ) -> Result { + Self::verify_early_checks(&payload_attestation_message, chain)?; + let (indexed_payload_attestation, ptc) = + Self::verify_middle_checks(&payload_attestation_message, chain, state)?; + + Ok(Self { + payload_attestation_message, + indexed_payload_attestation, + ptc, + }) + } +} + +impl VerifiedPayloadAttestationMessage { + /// Returns `Ok(Self)` if the `payload_attestation_message` is valid to be (re)published on + /// the gossip network. + pub fn verify>( + payload_attestation_message: PayloadAttestationMessage, + chain: &BeaconChain, + ) -> Result { + let head_snapshot = chain.head_snapshot(); + let head_state = &head_snapshot.beacon_state; + + let indexed = IndexedPayloadAttestationMessage::verify( + payload_attestation_message, + chain, + head_state, + )?; + + Self::from_indexed(indexed, chain, head_state) + } + + fn from_indexed>( + indexed: IndexedPayloadAttestationMessage, + chain: &BeaconChain, + state: &BeaconState, + ) -> Result { + let slot = indexed.payload_attestation_message.data.slot; + let validator_index = indexed.payload_attestation_message.validator_index; + + let pubkey_cache = chain.validator_pubkey_cache.read(); + + let signature_set = indexed_payload_attestation_signature_set( + state, + |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), + &indexed.indexed_payload_attestation.signature, + &indexed.indexed_payload_attestation, + &chain.spec, + ) + .map_err(|_| Error::UnknownValidatorIndex(validator_index))?; + + if !signature_set.verify() { + return Err(Error::InvalidSignature); + } + + // Now that the message has been fully verified, store that we have received a valid + // payload attestation message from this validator. + // + // Double check with the write lock to handle race conditions. + if chain + .observed_payload_attesters + .write() + .observe_validator(slot, validator_index as usize, ()) + .map_err(BeaconChainError::from)? + { + return Err(Error::PriorPayloadAttestationMessageKnown { + validator_index, + slot, + }); + } + + Ok(Self { + payload_attestation_message: indexed.payload_attestation_message, + indexed_payload_attestation: indexed.indexed_payload_attestation, + ptc: indexed.ptc, + }) + } + + pub fn payload_attestation_message(&self) -> &PayloadAttestationMessage { + &self.payload_attestation_message + } + + pub fn indexed_payload_attestation(&self) -> &IndexedPayloadAttestation { + &self.indexed_payload_attestation + } + + pub fn ptc(&self) -> &PTC { + &self.ptc + } + + pub fn into_payload_attestation_message(self) -> PayloadAttestationMessage { + self.payload_attestation_message + } +} + +/// Verify that the `slot` is within the acceptable gossip propagation range, with reference +/// to the current slot of the clock. +/// +/// Accounts for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. +fn verify_propagation_slot_range( + slot_clock: &S, + message_slot: Slot, + spec: &ChainSpec, +) -> Result<(), Error> { + let latest_permissible_slot = slot_clock + .now_with_future_tolerance(spec.maximum_gossip_clock_disparity()) + .ok_or(BeaconChainError::UnableToReadSlot)?; + if message_slot > latest_permissible_slot { + return Err(Error::FutureSlot { + message_slot, + latest_permissible_slot, + }); + } + + let earliest_permissible_slot = slot_clock + .now_with_past_tolerance(spec.maximum_gossip_clock_disparity()) + .ok_or(BeaconChainError::UnableToReadSlot)?; + if message_slot < earliest_permissible_slot { + return Err(Error::PastSlot { + message_slot, + earliest_permissible_slot, + }); + } + + Ok(()) +} From 4bbc74cf59a155590c724b1bbcde19ff56369d73 Mon Sep 17 00:00:00 2001 From: hopinheimer Date: Fri, 17 Apr 2026 19:43:49 -0400 Subject: [PATCH 05/34] wiring up `process_gossip_payload_attestation` and implement observe cache --- beacon_node/beacon_chain/src/beacon_chain.rs | 37 ++++- beacon_node/beacon_chain/src/builder.rs | 1 + beacon_node/beacon_chain/src/lib.rs | 1 + .../beacon_chain/src/observed_attesters.rs | 42 ++++- .../gossip_methods.rs | 151 ++++++++++++++++-- consensus/types/src/core/consts.rs | 1 + 6 files changed, 219 insertions(+), 14 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index acf7ad9c4c..6718b33958 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -48,12 +48,16 @@ use crate::observed_aggregates::{ Error as AttestationObservationError, ObservedAggregateAttestations, ObservedSyncContributions, }; use crate::observed_attesters::{ - ObservedAggregators, ObservedAttesters, ObservedSyncAggregators, ObservedSyncContributors, + ObservedAggregators, ObservedAttesters, ObservedPayloadAttesters, ObservedSyncAggregators, + ObservedSyncContributors, }; use crate::observed_block_producers::ObservedBlockProducers; use crate::observed_data_sidecars::ObservedDataSidecars; use crate::observed_operations::{ObservationOutcome, ObservedOperations}; use crate::observed_slashable::ObservedSlashable; +use crate::payload_attestation_verification::{ + Error as PayloadAttestationError, VerifiedPayloadAttestationMessage, +}; use crate::payload_bid_verification::payload_bid_cache::GossipVerifiedPayloadBidCache; #[cfg(not(test))] use crate::payload_envelope_streamer::{EnvelopeRequestSource, launch_payload_envelope_stream}; @@ -412,6 +416,9 @@ pub struct BeaconChain { /// Maintains a record of which validators have been seen to create `SignedContributionAndProofs` /// in recent epochs. pub(crate) observed_sync_aggregators: RwLock>, + /// Maintains a record of which validators have sent payload attestation messages + /// in recent slots. + pub(crate) observed_payload_attesters: RwLock>, /// Maintains a record of which validators have proposed blocks for each slot. pub observed_block_producers: RwLock>, /// Maintains a record of blob sidecars seen over the gossip network. @@ -2194,6 +2201,34 @@ impl BeaconChain { }) } + pub fn verify_payload_attestation_message_for_gossip( + &self, + payload_attestation_message: PayloadAttestationMessage, + ) -> Result, PayloadAttestationError> { + metrics::inc_counter(&metrics::PAYLOAD_ATTESTATION_PROCESSING_REQUESTS); + let _timer = metrics::start_timer(&metrics::PAYLOAD_ATTESTATION_GOSSIP_VERIFICATION_TIMES); + + VerifiedPayloadAttestationMessage::verify(payload_attestation_message, self).inspect(|_| { + metrics::inc_counter(&metrics::PAYLOAD_ATTESTATION_PROCESSING_SUCCESSES); + }) + } + + pub fn apply_payload_attestation_to_fork_choice( + &self, + indexed_payload_attestation: &IndexedPayloadAttestation, + ptc: &PTC, + ) -> Result<(), Error> { + self.canonical_head + .fork_choice_write_lock() + .on_payload_attestation( + self.slot()?, + indexed_payload_attestation, + AttestationFromBlock::False, + &ptc.0, + ) + .map_err(Into::into) + } + /// Accepts some `SyncCommitteeMessage` from the network and attempts to verify it, returning `Ok(_)` if /// it is valid to be (re)broadcast on the gossip network. pub fn verify_sync_committee_message_for_gossip( diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index b963f7c342..ed726b5d10 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -1010,6 +1010,7 @@ where observed_aggregators: <_>::default(), // TODO: allow for persisting and loading the pool from disk. observed_sync_aggregators: <_>::default(), + observed_payload_attesters: <_>::default(), // TODO: allow for persisting and loading the pool from disk. observed_block_producers: <_>::default(), observed_column_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())), diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index a8a706d8bc..0cd6c1b8d3 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -43,6 +43,7 @@ pub mod observed_block_producers; pub mod observed_data_sidecars; pub mod observed_operations; mod observed_slashable; +pub mod payload_attestation_verification; pub mod payload_bid_verification; pub mod payload_envelope_streamer; pub mod payload_envelope_verification; diff --git a/beacon_node/beacon_chain/src/observed_attesters.rs b/beacon_node/beacon_chain/src/observed_attesters.rs index 277bf38ffc..79bc181843 100644 --- a/beacon_node/beacon_chain/src/observed_attesters.rs +++ b/beacon_node/beacon_chain/src/observed_attesters.rs @@ -14,7 +14,7 @@ //! - `ObservedSyncAggregators`: allows filtering sync committee contributions from the same aggregators in //! the same slot and in the same subcommittee. -use crate::types::consts::altair::TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE; +use crate::types::consts::{altair::TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE, gloas::PTC_SIZE}; use bitvec::vec::BitVec; use std::collections::{HashMap, HashSet}; use std::hash::Hash; @@ -42,6 +42,8 @@ pub type ObservedSyncContributors = pub type ObservedAggregators = AutoPruningEpochContainer; pub type ObservedSyncAggregators = AutoPruningSlotContainer; +pub type ObservedPayloadAttesters = + AutoPruningSlotContainer; #[derive(Debug, PartialEq)] pub enum Error { @@ -255,6 +257,44 @@ impl Item<()> for SyncAggregatorSlotHashSet { } } +/// Stores a `HashSet` of validator indices that have sent a payload attestation gossip +/// message during a slot. +pub struct PayloadAttesterSlotHashSet { + set: HashSet, +} + +impl Item<()> for PayloadAttesterSlotHashSet { + fn with_capacity(capacity: usize) -> Self { + Self { + set: HashSet::with_capacity(capacity), + } + } + + /// Defaults to `PTC_SIZE`, the maximum number of payload attesters per slot. + fn default_capacity() -> usize { + PTC_SIZE as usize + } + + fn len(&self) -> usize { + self.set.len() + } + + fn validator_count(&self) -> usize { + self.set.len() + } + + /// Inserts the `validator_index` in the set. Returns `true` if the `validator_index` was + /// already in the set. + fn insert(&mut self, validator_index: usize, _value: ()) -> bool { + !self.set.insert(validator_index) + } + + /// Returns `true` if the `validator_index` is in the set. + fn get(&self, validator_index: usize) -> Option<()> { + self.set.contains(&validator_index).then_some(()) + } +} + /// A container that stores some number of `T` items. /// /// This container is "auto-pruning" since it gets an idea of the current slot by which 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 2238cb2f17..9387a4df3f 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -13,6 +13,9 @@ use beacon_chain::{ light_client_finality_update_verification::Error as LightClientFinalityUpdateError, light_client_optimistic_update_verification::Error as LightClientOptimisticUpdateError, observed_operations::ObservationOutcome, + payload_attestation_verification::{ + Error as PayloadAttestationError, VerifiedPayloadAttestationMessage, + }, sync_committee_verification::{self, Error as SyncCommitteeError}, validator_monitor::{get_block_delay_ms, get_slot_delay_ms}, }; @@ -130,6 +133,11 @@ struct RejectedAggregate { error: AttnError, } +struct RejectedPayloadAttestation { + error: PayloadAttestationError, + message_slot: Slot, +} + /// Data for an aggregated or unaggregated attestation that failed verification. enum FailedAtt { Unaggregate { @@ -3648,25 +3656,144 @@ impl NetworkBeaconProcessor { } } - // TODO(gloas) dont forget to add tracing instrumentation + #[instrument( + level = "trace", + skip(self, message_id, peer_id, payload_attestation_message), + fields( + peer_id = %peer_id, + slot = %payload_attestation_message.data.slot, + validator_index = payload_attestation_message.validator_index, + ) + )] pub fn process_gossip_payload_attestation( self: &Arc, message_id: MessageId, peer_id: PeerId, payload_attestation_message: PayloadAttestationMessage, ) { - // TODO(EIP-7732): Implement proper payload attestation message gossip processing. - // This should integrate with a payload_attestation_verification.rs module once it's implemented. + let message_slot = payload_attestation_message.data.slot; - trace!( - %peer_id, - validator_index = payload_attestation_message.validator_index, - slot = %payload_attestation_message.data.slot, - beacon_block_root = %payload_attestation_message.data.beacon_block_root, - "Processing payload attestation message" - ); + let result = self + .chain + .verify_payload_attestation_message_for_gossip(payload_attestation_message) + .map_err(|error| RejectedPayloadAttestation { + error, + message_slot, + }); - // For now, ignore all payload attestation messages since verification is not implemented - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + self.process_gossip_payload_attestation_result(result, message_id, peer_id); + } + + fn process_gossip_payload_attestation_result( + self: &Arc, + result: Result, RejectedPayloadAttestation>, + message_id: MessageId, + peer_id: PeerId, + ) { + match result { + Ok(verified) => { + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); + + if let Err(e) = self.chain.apply_payload_attestation_to_fork_choice( + verified.indexed_payload_attestation(), + verified.ptc(), + ) { + match e { + BeaconChainError::ForkChoiceError( + ForkChoiceError::InvalidPayloadAttestation(e), + ) => { + debug!( + reason = ?e, + %peer_id, + "Payload attestation invalid for fork choice" + ) + } + e => error!( + reason = ?e, + %peer_id, + "Error applying payload attestation to fork choice" + ), + } + } + } + Err(RejectedPayloadAttestation { + error, + message_slot, + }) => { + self.handle_payload_attestation_verification_failure( + peer_id, + message_id, + error, + message_slot, + ); + } + } + } + + fn handle_payload_attestation_verification_failure( + &self, + peer_id: PeerId, + message_id: MessageId, + error: PayloadAttestationError, + message_slot: Slot, + ) { + match &error { + PayloadAttestationError::FutureSlot { .. } => { + self.gossip_penalize_peer( + peer_id, + PeerAction::HighToleranceError, + "payload_attn_future_slot", + ); + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + } + PayloadAttestationError::PastSlot { .. } => { + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + } + PayloadAttestationError::PriorPayloadAttestationMessageKnown { .. } => { + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + } + PayloadAttestationError::UnknownHeadBlock { .. } => { + debug!( + %peer_id, + %message_slot, + "Payload attestation references unknown block" + ); + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + } + PayloadAttestationError::NotInPTC { .. } => { + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); + self.gossip_penalize_peer( + peer_id, + PeerAction::LowToleranceError, + "payload_attn_not_in_ptc", + ); + } + PayloadAttestationError::UnknownValidatorIndex(_) => { + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); + self.gossip_penalize_peer( + peer_id, + PeerAction::LowToleranceError, + "payload_attn_unknown_validator", + ); + } + PayloadAttestationError::InvalidSignature => { + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); + self.gossip_penalize_peer( + peer_id, + PeerAction::LowToleranceError, + "payload_attn_invalid_sig", + ); + } + PayloadAttestationError::BeaconChainError(_) + | PayloadAttestationError::BeaconStateError(_) => { + debug!( + %peer_id, + %message_slot, + ?error, + "Internal error verifying payload attestation" + ); + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + } + } } } diff --git a/consensus/types/src/core/consts.rs b/consensus/types/src/core/consts.rs index 049094da76..211208fc80 100644 --- a/consensus/types/src/core/consts.rs +++ b/consensus/types/src/core/consts.rs @@ -38,4 +38,5 @@ pub mod gloas { pub const ATTESTATION_TIMELINESS_INDEX: usize = 0; pub const PTC_TIMELINESS_INDEX: usize = 1; pub const NUM_BLOCK_TIMELINESS_DEADLINES: usize = 2; + pub const PTC_SIZE: u64 = 512; } From e0b980256994cf13b5778fb53af39ffdf46738b9 Mon Sep 17 00:00:00 2001 From: hopinheimer Date: Fri, 17 Apr 2026 19:44:04 -0400 Subject: [PATCH 06/34] adding metrics --- beacon_node/beacon_chain/src/metrics.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 786daa09da..b197d96ec5 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1457,6 +1457,27 @@ pub static SYNC_MESSAGE_GOSSIP_VERIFICATION_TIMES: LazyLock> = "Full runtime of sync contribution gossip verification", ) }); +pub static PAYLOAD_ATTESTATION_PROCESSING_REQUESTS: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "beacon_payload_attestation_processing_requests_total", + "Count of all payload attestation messages submitted for processing", + ) + }); +pub static PAYLOAD_ATTESTATION_PROCESSING_SUCCESSES: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "beacon_payload_attestation_processing_successes_total", + "Number of payload attestation messages verified for gossip", + ) + }); +pub static PAYLOAD_ATTESTATION_GOSSIP_VERIFICATION_TIMES: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "beacon_payload_attestation_gossip_verification_seconds", + "Full runtime of payload attestation gossip verification", + ) + }); pub static SYNC_MESSAGE_EQUIVOCATIONS: LazyLock> = LazyLock::new(|| { try_create_int_counter( "sync_message_equivocations_total", From ec111259c1e02a45f5f2b25a8a019acf211bde2a Mon Sep 17 00:00:00 2001 From: hopinheimer Date: Sat, 18 Apr 2026 01:13:00 -0400 Subject: [PATCH 07/34] adding `PayloadAttestationMessage` to `RejectedPayloadAttestation` --- .../src/payload_attestation_verification.rs | 2 ++ .../gossip_methods.rs | 23 ++++++++++--------- .../src/network_beacon_processor/mod.rs | 2 +- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification.rs b/beacon_node/beacon_chain/src/payload_attestation_verification.rs index 308a9c7cae..1ee6ae149a 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification.rs @@ -218,6 +218,8 @@ impl VerifiedPayloadAttestationMessage { payload_attestation_message: PayloadAttestationMessage, chain: &BeaconChain, ) -> Result { + // TODO(manas): i think we can have a shuffling cache. but this an interim solution + // can be discussed. let head_snapshot = chain.head_snapshot(); let head_state = &head_snapshot.beacon_state; 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 9387a4df3f..edeb191bb6 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -134,8 +134,8 @@ struct RejectedAggregate { } struct RejectedPayloadAttestation { + payload_attestation_message: Box, error: PayloadAttestationError, - message_slot: Slot, } /// Data for an aggregated or unaggregated attestation that failed verification. @@ -3669,17 +3669,18 @@ impl NetworkBeaconProcessor { self: &Arc, message_id: MessageId, peer_id: PeerId, - payload_attestation_message: PayloadAttestationMessage, + payload_attestation_message: Box, ) { - let message_slot = payload_attestation_message.data.slot; - - let result = self + let result = match self .chain - .verify_payload_attestation_message_for_gossip(payload_attestation_message) - .map_err(|error| RejectedPayloadAttestation { + .verify_payload_attestation_message_for_gossip(*payload_attestation_message.clone()) + { + Ok(verified) => Ok(verified), + Err(error) => Err(RejectedPayloadAttestation { + payload_attestation_message: payload_attestation_message.clone(), error, - message_slot, - }); + }), + }; self.process_gossip_payload_attestation_result(result, message_id, peer_id); } @@ -3717,14 +3718,14 @@ impl NetworkBeaconProcessor { } } Err(RejectedPayloadAttestation { + payload_attestation_message, error, - message_slot, }) => { self.handle_payload_attestation_verification_failure( peer_id, message_id, error, - message_slot, + payload_attestation_message.data.slot, ); } } diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 2b354aaa20..a2f030a0fc 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -485,7 +485,7 @@ impl NetworkBeaconProcessor { processor.process_gossip_payload_attestation( message_id, peer_id, - *payload_attestation_message, + payload_attestation_message, ) }; From eef1bf6bb3a0c28c3d06895f8eace080ce4f69fb Mon Sep 17 00:00:00 2001 From: hopinheimer Date: Sun, 19 Apr 2026 01:19:12 -0400 Subject: [PATCH 08/34] shifting `payload_attestation_verification` to separate module --- beacon_node/beacon_chain/src/beacon_chain.rs | 18 +- .../src/payload_attestation_verification.rs | 328 ------------------ .../gossip_verified_payload_attestation.rs | 181 ++++++++++ .../payload_attestation_verification/mod.rs | 107 ++++++ .../gossip_methods.rs | 2 +- consensus/fork_choice/src/fork_choice.rs | 2 +- 6 files changed, 305 insertions(+), 333 deletions(-) delete mode 100644 beacon_node/beacon_chain/src/payload_attestation_verification.rs create mode 100644 beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs create mode 100644 beacon_node/beacon_chain/src/payload_attestation_verification/mod.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 6718b33958..38122a03f3 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -56,7 +56,8 @@ use crate::observed_data_sidecars::ObservedDataSidecars; use crate::observed_operations::{ObservationOutcome, ObservedOperations}; use crate::observed_slashable::ObservedSlashable; use crate::payload_attestation_verification::{ - Error as PayloadAttestationError, VerifiedPayloadAttestationMessage, + Error as PayloadAttestationError, GossipVerificationContext as PayloadAttestationGossipContext, + VerifiedPayloadAttestationMessage, }; use crate::payload_bid_verification::payload_bid_cache::GossipVerifiedPayloadBidCache; #[cfg(not(test))] @@ -2201,14 +2202,25 @@ impl BeaconChain { }) } + pub fn payload_attestation_gossip_context(&self) -> PayloadAttestationGossipContext<'_, T> { + PayloadAttestationGossipContext { + slot_clock: &self.slot_clock, + spec: &self.spec, + observed_payload_attesters: &self.observed_payload_attesters, + canonical_head: &self.canonical_head, + validator_pubkey_cache: &self.validator_pubkey_cache, + } + } + pub fn verify_payload_attestation_message_for_gossip( &self, payload_attestation_message: PayloadAttestationMessage, - ) -> Result, PayloadAttestationError> { + ) -> Result, PayloadAttestationError> { metrics::inc_counter(&metrics::PAYLOAD_ATTESTATION_PROCESSING_REQUESTS); let _timer = metrics::start_timer(&metrics::PAYLOAD_ATTESTATION_GOSSIP_VERIFICATION_TIMES); - VerifiedPayloadAttestationMessage::verify(payload_attestation_message, self).inspect(|_| { + let ctx = self.payload_attestation_gossip_context(); + VerifiedPayloadAttestationMessage::new(payload_attestation_message, &ctx).inspect(|_| { metrics::inc_counter(&metrics::PAYLOAD_ATTESTATION_PROCESSING_SUCCESSES); }) } diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification.rs b/beacon_node/beacon_chain/src/payload_attestation_verification.rs deleted file mode 100644 index 1ee6ae149a..0000000000 --- a/beacon_node/beacon_chain/src/payload_attestation_verification.rs +++ /dev/null @@ -1,328 +0,0 @@ -//! Provides verification for `PayloadAttestationMessage` received from the gossip network. -//! -//! Similar to `crate::attestation_verification`, we try to avoid doing duplicate verification -//! work as a payload attestation message passes through different stages of verification. -//! -//! ```ignore -//! types::PayloadAttestationMessage -//! | -//! ▼ -//! IndexedPayloadAttestationMessage -//! | -//! ▼ -//! VerifiedPayloadAttestationMessage -//! ``` - -use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; -use bls::AggregateSignature; -use slot_clock::SlotClock; -use state_processing::per_block_processing::signature_sets::indexed_payload_attestation_signature_set; -use std::borrow::Cow; -use strum::AsRefStr; -use types::{ - BeaconState, BeaconStateError, ChainSpec, EthSpec, Hash256, IndexedPayloadAttestation, PTC, - PayloadAttestationMessage, Slot, -}; - -/// Returned when a payload attestation message was not successfully verified. It might not have -/// been verified for two reasons: -/// -/// - The message is malformed or inappropriate for the context (indicated by all variants -/// other than `BeaconChainError`). -/// - The application encountered an internal error whilst attempting to determine validity -/// (the `BeaconChainError` variant) -#[derive(Debug, AsRefStr)] -pub enum Error { - /// The payload attestation message is from a slot that is later than the current slot - /// (with respect to the gossip clock disparity). - /// - /// ## Peer scoring - /// - /// Assuming the local clock is correct, the peer has sent an invalid message. - FutureSlot { - message_slot: Slot, - latest_permissible_slot: Slot, - }, - /// The payload attestation message is from a slot that is prior to the earliest - /// permissible slot (with respect to the gossip clock disparity). - /// - /// ## Peer scoring - /// - /// Assuming the local clock is correct, the peer has sent an invalid message. - PastSlot { - message_slot: Slot, - earliest_permissible_slot: Slot, - }, - /// We have already observed a valid payload attestation message from this validator - /// for this slot. - /// - /// ## Peer scoring - /// - /// The peer is not necessarily faulty. - PriorPayloadAttestationMessageKnown { validator_index: u64, slot: Slot }, - /// The beacon block referenced by the payload attestation message is not known. - /// - /// ## Peer scoring - /// - /// The attestation points to a block we have not yet imported. It's unclear if the - /// attestation is valid or not. - UnknownHeadBlock { beacon_block_root: Hash256 }, - /// The validator index is not a member of the PTC for this slot. - /// - /// ## Peer scoring - /// - /// The peer has sent an invalid message. - NotInPTC { validator_index: u64, slot: Slot }, - /// The validator index is unknown. - /// - /// ## Peer scoring - /// - /// The peer has sent an invalid message. - UnknownValidatorIndex(u64), - /// The signature on the payload attestation message is invalid. - /// - /// ## Peer scoring - /// - /// The peer has sent an invalid message. - InvalidSignature, - /// There was an error whilst processing the payload attestation message. It is not known - /// if it is valid or invalid. - /// - /// ## Peer scoring - /// - /// We were unable to process this message due to an internal error. It's unclear if the - /// message is valid. - BeaconChainError(Box), - /// An error reading beacon state. - /// - /// ## Peer scoring - /// - /// We were unable to process this message due to an internal error. - BeaconStateError(BeaconStateError), -} - -impl From for Error { - fn from(e: BeaconChainError) -> Self { - Error::BeaconChainError(Box::new(e)) - } -} - -impl From for Error { - fn from(e: BeaconStateError) -> Self { - Error::BeaconStateError(e) - } -} - -/// Wraps a `PayloadAttestationMessage` that has passed early and middle checks but has *not* -/// undergone signature verification. -struct IndexedPayloadAttestationMessage { - payload_attestation_message: PayloadAttestationMessage, - indexed_payload_attestation: IndexedPayloadAttestation, - ptc: PTC, -} - -/// Wraps a `PayloadAttestationMessage` that has been fully verified for propagation on the -/// gossip network. -#[derive(Clone, Debug)] -pub struct VerifiedPayloadAttestationMessage { - payload_attestation_message: PayloadAttestationMessage, - indexed_payload_attestation: IndexedPayloadAttestation, - ptc: PTC, -} - -impl IndexedPayloadAttestationMessage { - fn verify_early_checks( - payload_attestation_message: &PayloadAttestationMessage, - chain: &BeaconChain, - ) -> Result<(), Error> { - let slot = payload_attestation_message.data.slot; - let validator_index = payload_attestation_message.validator_index; - - verify_propagation_slot_range(&chain.slot_clock, slot, &chain.spec)?; - - if chain - .observed_payload_attesters - .read() - .validator_has_been_observed(slot, validator_index as usize) - .map_err(BeaconChainError::from)? - { - return Err(Error::PriorPayloadAttestationMessageKnown { - validator_index, - slot, - }); - } - - let beacon_block_root = payload_attestation_message.data.beacon_block_root; - if chain - .canonical_head - .fork_choice_read_lock() - .get_block(&beacon_block_root) - .is_none() - { - return Err(Error::UnknownHeadBlock { beacon_block_root }); - } - - Ok(()) - } - - #[allow(clippy::type_complexity)] - fn verify_middle_checks( - payload_attestation_message: &PayloadAttestationMessage, - chain: &BeaconChain, - state: &BeaconState, - ) -> Result<(IndexedPayloadAttestation, PTC), Error> { - let slot = payload_attestation_message.data.slot; - let validator_index = payload_attestation_message.validator_index; - - let ptc = state.get_ptc(slot, &chain.spec)?; - if !ptc.0.contains(&(validator_index as usize)) { - return Err(Error::NotInPTC { - validator_index, - slot, - }); - } - - let indexed_payload_attestation = IndexedPayloadAttestation { - attesting_indices: vec![validator_index] - .try_into() - .map_err(|_| Error::UnknownValidatorIndex(validator_index))?, - data: payload_attestation_message.data.clone(), - signature: AggregateSignature::from(&payload_attestation_message.signature), - }; - - Ok((indexed_payload_attestation, ptc)) - } - - /// Runs early and middle checks, producing an intermediate verified form. - fn verify( - payload_attestation_message: PayloadAttestationMessage, - chain: &BeaconChain, - state: &BeaconState, - ) -> Result { - Self::verify_early_checks(&payload_attestation_message, chain)?; - let (indexed_payload_attestation, ptc) = - Self::verify_middle_checks(&payload_attestation_message, chain, state)?; - - Ok(Self { - payload_attestation_message, - indexed_payload_attestation, - ptc, - }) - } -} - -impl VerifiedPayloadAttestationMessage { - /// Returns `Ok(Self)` if the `payload_attestation_message` is valid to be (re)published on - /// the gossip network. - pub fn verify>( - payload_attestation_message: PayloadAttestationMessage, - chain: &BeaconChain, - ) -> Result { - // TODO(manas): i think we can have a shuffling cache. but this an interim solution - // can be discussed. - let head_snapshot = chain.head_snapshot(); - let head_state = &head_snapshot.beacon_state; - - let indexed = IndexedPayloadAttestationMessage::verify( - payload_attestation_message, - chain, - head_state, - )?; - - Self::from_indexed(indexed, chain, head_state) - } - - fn from_indexed>( - indexed: IndexedPayloadAttestationMessage, - chain: &BeaconChain, - state: &BeaconState, - ) -> Result { - let slot = indexed.payload_attestation_message.data.slot; - let validator_index = indexed.payload_attestation_message.validator_index; - - let pubkey_cache = chain.validator_pubkey_cache.read(); - - let signature_set = indexed_payload_attestation_signature_set( - state, - |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), - &indexed.indexed_payload_attestation.signature, - &indexed.indexed_payload_attestation, - &chain.spec, - ) - .map_err(|_| Error::UnknownValidatorIndex(validator_index))?; - - if !signature_set.verify() { - return Err(Error::InvalidSignature); - } - - // Now that the message has been fully verified, store that we have received a valid - // payload attestation message from this validator. - // - // Double check with the write lock to handle race conditions. - if chain - .observed_payload_attesters - .write() - .observe_validator(slot, validator_index as usize, ()) - .map_err(BeaconChainError::from)? - { - return Err(Error::PriorPayloadAttestationMessageKnown { - validator_index, - slot, - }); - } - - Ok(Self { - payload_attestation_message: indexed.payload_attestation_message, - indexed_payload_attestation: indexed.indexed_payload_attestation, - ptc: indexed.ptc, - }) - } - - pub fn payload_attestation_message(&self) -> &PayloadAttestationMessage { - &self.payload_attestation_message - } - - pub fn indexed_payload_attestation(&self) -> &IndexedPayloadAttestation { - &self.indexed_payload_attestation - } - - pub fn ptc(&self) -> &PTC { - &self.ptc - } - - pub fn into_payload_attestation_message(self) -> PayloadAttestationMessage { - self.payload_attestation_message - } -} - -/// Verify that the `slot` is within the acceptable gossip propagation range, with reference -/// to the current slot of the clock. -/// -/// Accounts for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. -fn verify_propagation_slot_range( - slot_clock: &S, - message_slot: Slot, - spec: &ChainSpec, -) -> Result<(), Error> { - let latest_permissible_slot = slot_clock - .now_with_future_tolerance(spec.maximum_gossip_clock_disparity()) - .ok_or(BeaconChainError::UnableToReadSlot)?; - if message_slot > latest_permissible_slot { - return Err(Error::FutureSlot { - message_slot, - latest_permissible_slot, - }); - } - - let earliest_permissible_slot = slot_clock - .now_with_past_tolerance(spec.maximum_gossip_clock_disparity()) - .ok_or(BeaconChainError::UnableToReadSlot)?; - if message_slot < earliest_permissible_slot { - return Err(Error::PastSlot { - message_slot, - earliest_permissible_slot, - }); - } - - Ok(()) -} diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs new file mode 100644 index 0000000000..f04144c754 --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs @@ -0,0 +1,181 @@ +use super::Error; +use crate::canonical_head::CanonicalHead; +use crate::observed_attesters::ObservedPayloadAttesters; +use crate::validator_pubkey_cache::ValidatorPubkeyCache; +use crate::{BeaconChainError, BeaconChainTypes}; +use bls::AggregateSignature; +use educe::Educe; +use parking_lot::RwLock; +use slot_clock::SlotClock; +use state_processing::per_block_processing::signature_sets::indexed_payload_attestation_signature_set; +use std::borrow::Cow; +use types::{ChainSpec, IndexedPayloadAttestation, PTC, PayloadAttestationMessage, Slot}; + +/// Bundles only the dependencies needed for gossip verification of payload attestation messages, +/// decoupling verification from the full `BeaconChain`. +pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { + pub slot_clock: &'a T::SlotClock, + pub spec: &'a ChainSpec, + pub observed_payload_attesters: &'a RwLock>, + pub canonical_head: &'a CanonicalHead, + pub validator_pubkey_cache: &'a RwLock>, +} + +/// A `PayloadAttestationMessage` that has been verified for propagation on the gossip network. +#[derive(Educe)] +#[educe(Clone, Debug)] +pub struct VerifiedPayloadAttestationMessage { + payload_attestation_message: PayloadAttestationMessage, + indexed_payload_attestation: IndexedPayloadAttestation, + ptc: PTC, +} + +impl VerifiedPayloadAttestationMessage { + pub fn new( + payload_attestation_message: PayloadAttestationMessage, + ctx: &GossipVerificationContext<'_, T>, + ) -> Result { + let slot = payload_attestation_message.data.slot; + let validator_index = payload_attestation_message.validator_index; + + // [IGNORE] `data.slot` is within the `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance. + verify_propagation_slot_range(ctx.slot_clock, slot, ctx.spec)?; + + // [IGNORE] There has been no other valid payload attestation message for this + // validator index. + if ctx + .observed_payload_attesters + .read() + .validator_has_been_observed(slot, validator_index as usize) + .map_err(BeaconChainError::from)? + { + return Err(Error::PriorPayloadAttestationMessageKnown { + validator_index, + slot, + }); + } + + // [IGNORE] `data.beacon_block_root` has been seen (via gossip or non-optimistic RPC). + // [REJECT] `data.beacon_block_root` passes validation. + // + // TODO(EIP-7732): These two conditions are conflated. We need a status table to + // differentiate between: + // 1. Blocks we haven't seen (IGNORE), and + // 2. Blocks we've seen that are invalid (REJECT). + // Presently both cases return IGNORE. + let beacon_block_root = payload_attestation_message.data.beacon_block_root; + if ctx + .canonical_head + .fork_choice_read_lock() + .get_block(&beacon_block_root) + .is_none() + { + return Err(Error::UnknownHeadBlock { beacon_block_root }); + } + + // Get head state for PTC computation. + let head = ctx.canonical_head.cached_head(); + let head_state = &head.snapshot.beacon_state; + + // [REJECT] `validator_index` is within `get_ptc(state, data.slot)`. + let ptc = head_state.get_ptc(slot, ctx.spec)?; + if !ptc.0.contains(&(validator_index as usize)) { + return Err(Error::NotInPTC { + validator_index, + slot, + }); + } + + // Build the indexed form for signature verification and downstream fork choice. + let indexed_payload_attestation = IndexedPayloadAttestation { + attesting_indices: vec![validator_index] + .try_into() + .map_err(|_| Error::UnknownValidatorIndex(validator_index))?, + data: payload_attestation_message.data.clone(), + signature: AggregateSignature::from(&payload_attestation_message.signature), + }; + + // [REJECT] The signature is valid with respect to the `validator_index`. + let pubkey_cache = ctx.validator_pubkey_cache.read(); + let signature_set = indexed_payload_attestation_signature_set( + head_state, + |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), + &indexed_payload_attestation.signature, + &indexed_payload_attestation, + ctx.spec, + ) + .map_err(|_| Error::UnknownValidatorIndex(validator_index))?; + + if !signature_set.verify() { + return Err(Error::InvalidSignature); + } + + // Record that we have received a valid payload attestation message from this + // validator. Double check with the write lock to handle race conditions. + if ctx + .observed_payload_attesters + .write() + .observe_validator(slot, validator_index as usize, ()) + .map_err(BeaconChainError::from)? + { + return Err(Error::PriorPayloadAttestationMessageKnown { + validator_index, + slot, + }); + } + + Ok(Self { + payload_attestation_message, + indexed_payload_attestation, + ptc, + }) + } + + pub fn payload_attestation_message(&self) -> &PayloadAttestationMessage { + &self.payload_attestation_message + } + + pub fn indexed_payload_attestation(&self) -> &IndexedPayloadAttestation { + &self.indexed_payload_attestation + } + + pub fn ptc(&self) -> &PTC { + &self.ptc + } + + pub fn into_payload_attestation_message(self) -> PayloadAttestationMessage { + self.payload_attestation_message + } +} + +/// Verify that the `slot` is within the acceptable gossip propagation range, with reference +/// to the current slot of the clock. +/// +/// Accounts for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. +fn verify_propagation_slot_range( + slot_clock: &S, + message_slot: Slot, + spec: &ChainSpec, +) -> Result<(), Error> { + let latest_permissible_slot = slot_clock + .now_with_future_tolerance(spec.maximum_gossip_clock_disparity()) + .ok_or(BeaconChainError::UnableToReadSlot)?; + if message_slot > latest_permissible_slot { + return Err(Error::FutureSlot { + message_slot, + latest_permissible_slot, + }); + } + + let earliest_permissible_slot = slot_clock + .now_with_past_tolerance(spec.maximum_gossip_clock_disparity()) + .ok_or(BeaconChainError::UnableToReadSlot)?; + if message_slot < earliest_permissible_slot { + return Err(Error::PastSlot { + message_slot, + earliest_permissible_slot, + }); + } + + Ok(()) +} diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/mod.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/mod.rs new file mode 100644 index 0000000000..38cd36d75e --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/mod.rs @@ -0,0 +1,107 @@ +//! Provides verification for `PayloadAttestationMessage` received from the gossip network. +//! +//! ```ignore +//! types::PayloadAttestationMessage +//! | +//! ▼ +//! VerifiedPayloadAttestationMessage +//! ``` + +use crate::BeaconChainError; +use strum::AsRefStr; +use types::{BeaconStateError, Hash256, Slot}; + +pub mod gossip_verified_payload_attestation; + +pub use gossip_verified_payload_attestation::{ + GossipVerificationContext, VerifiedPayloadAttestationMessage, +}; + +/// Returned when a payload attestation message was not successfully verified. It might not have +/// been verified for two reasons: +/// +/// - The message is malformed or inappropriate for the context (indicated by all variants +/// other than `BeaconChainError`). +/// - The application encountered an internal error whilst attempting to determine validity +/// (the `BeaconChainError` variant) +#[derive(Debug, AsRefStr)] +pub enum Error { + /// The payload attestation message is from a slot that is later than the current slot + /// (with respect to the gossip clock disparity). + /// + /// ## Peer scoring + /// + /// Assuming the local clock is correct, the peer has sent an invalid message. + FutureSlot { + message_slot: Slot, + latest_permissible_slot: Slot, + }, + /// The payload attestation message is from a slot that is prior to the earliest + /// permissible slot (with respect to the gossip clock disparity). + /// + /// ## Peer scoring + /// + /// Assuming the local clock is correct, the peer has sent an invalid message. + PastSlot { + message_slot: Slot, + earliest_permissible_slot: Slot, + }, + /// We have already observed a valid payload attestation message from this validator + /// for this slot. + /// + /// ## Peer scoring + /// + /// The peer is not necessarily faulty. + PriorPayloadAttestationMessageKnown { validator_index: u64, slot: Slot }, + /// The beacon block referenced by the payload attestation message is not known. + /// + /// ## Peer scoring + /// + /// The attestation points to a block we have not yet imported. It's unclear if the + /// attestation is valid or not. + UnknownHeadBlock { beacon_block_root: Hash256 }, + /// The validator index is not a member of the PTC for this slot. + /// + /// ## Peer scoring + /// + /// The peer has sent an invalid message. + NotInPTC { validator_index: u64, slot: Slot }, + /// The validator index is unknown. + /// + /// ## Peer scoring + /// + /// The peer has sent an invalid message. + UnknownValidatorIndex(u64), + /// The signature on the payload attestation message is invalid. + /// + /// ## Peer scoring + /// + /// The peer has sent an invalid message. + InvalidSignature, + /// There was an error whilst processing the payload attestation message. It is not known + /// if it is valid or invalid. + /// + /// ## Peer scoring + /// + /// We were unable to process this message due to an internal error. It's unclear if the + /// message is valid. + BeaconChainError(Box), + /// An error reading beacon state. + /// + /// ## Peer scoring + /// + /// We were unable to process this message due to an internal error. + BeaconStateError(BeaconStateError), +} + +impl From for Error { + fn from(e: BeaconChainError) -> Self { + Error::BeaconChainError(Box::new(e)) + } +} + +impl From for Error { + fn from(e: BeaconStateError) -> Self { + Error::BeaconStateError(e) + } +} 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 edeb191bb6..bdaff57688 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -3687,7 +3687,7 @@ impl NetworkBeaconProcessor { fn process_gossip_payload_attestation_result( self: &Arc, - result: Result, RejectedPayloadAttestation>, + result: Result, RejectedPayloadAttestation>, message_id: MessageId, peer_id: PeerId, ) { diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 92fd4c1faf..e7d0c1e9fc 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1337,7 +1337,7 @@ where let ptc_indices: Vec = attestation .attesting_indices .iter() - .filter_map(|vi| ptc.iter().position(|&p| p == *vi as usize)) + .filter_map(|validator_index| ptc.iter().position(|&p| p == *validator_index as usize)) .collect(); // Check that all the attesters are in the PTC From 9c9ba192b4fcb13acc6135dc19cd2d760fa9e696 Mon Sep 17 00:00:00 2001 From: hopinheimer Date: Sun, 19 Apr 2026 01:31:33 -0400 Subject: [PATCH 09/34] fmt --- .../gossip_verified_payload_attestation.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs index f04144c754..169add81cf 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs @@ -55,10 +55,10 @@ impl VerifiedPayloadAttestationMessage { }); } - // [IGNORE] `data.beacon_block_root` has been seen (via gossip or non-optimistic RPC). + // [IGNORE] `data.beacon_block_root` has been seen // [REJECT] `data.beacon_block_root` passes validation. // - // TODO(EIP-7732): These two conditions are conflated. We need a status table to + // TODO(gloas): These two conditions are conflated. We need a status table to // differentiate between: // 1. Blocks we haven't seen (IGNORE), and // 2. Blocks we've seen that are invalid (REJECT). From 96feda027d978a1bb9b0cda4dd86db46a6a0f05b Mon Sep 17 00:00:00 2001 From: hopinheimer Date: Sun, 19 Apr 2026 13:33:22 -0400 Subject: [PATCH 10/34] invert `BeaconChain` dependency --- beacon_node/beacon_chain/src/beacon_chain.rs | 27 ----------------- .../gossip_verified_payload_attestation.rs | 29 +++++++++++++++++-- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index bc96e7a2eb..1016f2d00d 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -55,10 +55,6 @@ use crate::observed_block_producers::ObservedBlockProducers; use crate::observed_data_sidecars::ObservedDataSidecars; use crate::observed_operations::{ObservationOutcome, ObservedOperations}; use crate::observed_slashable::ObservedSlashable; -use crate::payload_attestation_verification::{ - Error as PayloadAttestationError, GossipVerificationContext as PayloadAttestationGossipContext, - VerifiedPayloadAttestationMessage, -}; use crate::payload_bid_verification::payload_bid_cache::GossipVerifiedPayloadBidCache; #[cfg(not(test))] use crate::payload_envelope_streamer::{EnvelopeRequestSource, launch_payload_envelope_stream}; @@ -2246,29 +2242,6 @@ impl BeaconChain { }) } - pub fn payload_attestation_gossip_context(&self) -> PayloadAttestationGossipContext<'_, T> { - PayloadAttestationGossipContext { - slot_clock: &self.slot_clock, - spec: &self.spec, - observed_payload_attesters: &self.observed_payload_attesters, - canonical_head: &self.canonical_head, - validator_pubkey_cache: &self.validator_pubkey_cache, - } - } - - pub fn verify_payload_attestation_message_for_gossip( - &self, - payload_attestation_message: PayloadAttestationMessage, - ) -> Result, PayloadAttestationError> { - metrics::inc_counter(&metrics::PAYLOAD_ATTESTATION_PROCESSING_REQUESTS); - let _timer = metrics::start_timer(&metrics::PAYLOAD_ATTESTATION_GOSSIP_VERIFICATION_TIMES); - - let ctx = self.payload_attestation_gossip_context(); - VerifiedPayloadAttestationMessage::new(payload_attestation_message, &ctx).inspect(|_| { - metrics::inc_counter(&metrics::PAYLOAD_ATTESTATION_PROCESSING_SUCCESSES); - }) - } - pub fn apply_payload_attestation_to_fork_choice( &self, indexed_payload_attestation: &IndexedPayloadAttestation, diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs index 169add81cf..5ee50ece95 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs @@ -2,7 +2,7 @@ use super::Error; use crate::canonical_head::CanonicalHead; use crate::observed_attesters::ObservedPayloadAttesters; use crate::validator_pubkey_cache::ValidatorPubkeyCache; -use crate::{BeaconChainError, BeaconChainTypes}; +use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, metrics}; use bls::AggregateSignature; use educe::Educe; use parking_lot::RwLock; @@ -11,8 +11,6 @@ use state_processing::per_block_processing::signature_sets::indexed_payload_atte use std::borrow::Cow; use types::{ChainSpec, IndexedPayloadAttestation, PTC, PayloadAttestationMessage, Slot}; -/// Bundles only the dependencies needed for gossip verification of payload attestation messages, -/// decoupling verification from the full `BeaconChain`. pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { pub slot_clock: &'a T::SlotClock, pub spec: &'a ChainSpec, @@ -148,6 +146,31 @@ impl VerifiedPayloadAttestationMessage { } } +impl BeaconChain { + pub fn payload_attestation_gossip_context(&self) -> GossipVerificationContext<'_, T> { + GossipVerificationContext { + slot_clock: &self.slot_clock, + spec: &self.spec, + observed_payload_attesters: &self.observed_payload_attesters, + canonical_head: &self.canonical_head, + validator_pubkey_cache: &self.validator_pubkey_cache, + } + } + + pub fn verify_payload_attestation_message_for_gossip( + &self, + payload_attestation_message: PayloadAttestationMessage, + ) -> Result, Error> { + metrics::inc_counter(&metrics::PAYLOAD_ATTESTATION_PROCESSING_REQUESTS); + let _timer = metrics::start_timer(&metrics::PAYLOAD_ATTESTATION_GOSSIP_VERIFICATION_TIMES); + + let ctx = self.payload_attestation_gossip_context(); + VerifiedPayloadAttestationMessage::new(payload_attestation_message, &ctx).inspect(|_| { + metrics::inc_counter(&metrics::PAYLOAD_ATTESTATION_PROCESSING_SUCCESSES); + }) + } +} + /// Verify that the `slot` is within the acceptable gossip propagation range, with reference /// to the current slot of the clock. /// From 8e9627ce11a2e76235e8640f65a53842eb9b615a Mon Sep 17 00:00:00 2001 From: hopinheimer Date: Mon, 20 Apr 2026 23:49:09 -0400 Subject: [PATCH 11/34] consolidate `IGNORE` cases and tests --- .../payload_attestation_verification/mod.rs | 3 + .../payload_attestation_verification/tests.rs | 352 ++++++++++++++++++ .../gossip_methods.rs | 6 +- 3 files changed, 357 insertions(+), 4 deletions(-) create mode 100644 beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/mod.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/mod.rs index 38cd36d75e..477527c0aa 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/mod.rs @@ -105,3 +105,6 @@ impl From for Error { Error::BeaconStateError(e) } } + +#[cfg(test)] +mod tests; diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs new file mode 100644 index 0000000000..eb3359b291 --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs @@ -0,0 +1,352 @@ +use std::sync::Arc; +use std::time::Duration; + +use bls::{Keypair, Signature}; +use fork_choice::ForkChoice; +use genesis::{generate_deterministic_keypairs, interop_genesis_state}; +use parking_lot::RwLock; +use proto_array::PayloadStatus; +use slot_clock::{SlotClock, TestingSlotClock}; +use store::{HotColdDB, StoreConfig}; +use types::{ + BeaconBlock, ChainSpec, Checkpoint, Domain, Epoch, EthSpec, Hash256, MinimalEthSpec, + PayloadAttestationData, PayloadAttestationMessage, SignedBeaconBlock, SignedRoot, Slot, +}; + +use crate::{ + beacon_fork_choice_store::BeaconForkChoiceStore, + beacon_snapshot::BeaconSnapshot, + canonical_head::CanonicalHead, + observed_attesters::ObservedPayloadAttesters, + payload_attestation_verification::{ + Error as PayloadAttestationError, + gossip_verified_payload_attestation::{ + GossipVerificationContext, VerifiedPayloadAttestationMessage, + }, + }, + test_utils::{EphemeralHarnessType, fork_name_from_env, test_spec}, + validator_pubkey_cache::ValidatorPubkeyCache, +}; + +type E = MinimalEthSpec; +type T = EphemeralHarnessType; + +const NUM_VALIDATORS: usize = 64; + +struct TestContext { + canonical_head: CanonicalHead, + observed_payload_attesters: RwLock>, + validator_pubkey_cache: RwLock>, + slot_clock: TestingSlotClock, + keypairs: Vec, + spec: ChainSpec, + genesis_block_root: Hash256, +} + +impl TestContext { + fn new() -> Self { + let spec = test_spec::(); + let store = Arc::new( + HotColdDB::open_ephemeral(StoreConfig::default(), Arc::new(spec.clone())) + .expect("should open ephemeral store"), + ); + + let keypairs = generate_deterministic_keypairs(NUM_VALIDATORS); + + let mut state = + interop_genesis_state::(&keypairs, 0, Hash256::repeat_byte(0x42), None, &spec) + .expect("should build genesis state"); + + *state.finalized_checkpoint_mut() = Checkpoint { + epoch: Epoch::new(1), + root: Hash256::ZERO, + }; + + let mut genesis_block = BeaconBlock::empty(&spec); + *genesis_block.state_root_mut() = state + .update_tree_hash_cache() + .expect("should hash genesis state"); + let signed_block = SignedBeaconBlock::from_block(genesis_block, Signature::empty()); + let block_root = signed_block.canonical_root(); + + let snapshot = BeaconSnapshot::new( + Arc::new(signed_block.clone()), + None, + block_root, + state.clone(), + ); + + let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store.clone(), snapshot.clone()) + .expect("should create fork choice store"); + let fork_choice = + ForkChoice::from_anchor(fc_store, block_root, &signed_block, &state, None, &spec) + .expect("should create fork choice"); + + let canonical_head = + CanonicalHead::new(fork_choice, Arc::new(snapshot), PayloadStatus::Pending); + + let slot_clock = TestingSlotClock::new( + Slot::new(0), + Duration::from_secs(0), + spec.get_slot_duration(), + ); + // Advance past genesis so `now_with_past_tolerance` doesn't underflow. + slot_clock.set_current_time(spec.get_slot_duration()); + + let validator_pubkey_cache = + ValidatorPubkeyCache::new(&state, store.clone()).expect("should create pubkey cache"); + + Self { + canonical_head, + observed_payload_attesters: RwLock::new(ObservedPayloadAttesters::default()), + validator_pubkey_cache: RwLock::new(validator_pubkey_cache), + slot_clock, + keypairs, + spec, + genesis_block_root: block_root, + } + } + + fn gossip_ctx(&self) -> GossipVerificationContext<'_, T> { + GossipVerificationContext { + slot_clock: &self.slot_clock, + spec: &self.spec, + observed_payload_attesters: &self.observed_payload_attesters, + canonical_head: &self.canonical_head, + validator_pubkey_cache: &self.validator_pubkey_cache, + } + } + + fn ptc_members(&self, slot: Slot) -> Vec { + let head = self.canonical_head.cached_head(); + let state = &head.snapshot.beacon_state; + let ptc = state.get_ptc(slot, &self.spec).expect("should get PTC"); + ptc.0.to_vec() + } + + fn sign_payload_attestation( + &self, + data: PayloadAttestationData, + validator_index: u64, + ) -> PayloadAttestationMessage { + let head = self.canonical_head.cached_head(); + let state = &head.snapshot.beacon_state; + let domain = self.spec.get_domain( + data.slot.epoch(E::slots_per_epoch()), + Domain::PTCAttester, + &state.fork(), + state.genesis_validators_root(), + ); + let message = data.signing_root(domain); + let signature = self.keypairs[validator_index as usize].sk.sign(message); + PayloadAttestationMessage { + validator_index, + data, + signature, + } + } +} + +fn make_payload_attestation( + slot: Slot, + validator_index: u64, + beacon_block_root: Hash256, +) -> PayloadAttestationMessage { + PayloadAttestationMessage { + validator_index, + data: PayloadAttestationData { + beacon_block_root, + slot, + payload_present: true, + blob_data_available: true, + }, + signature: Signature::empty(), + } +} + +#[test] +fn future_slot() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + + let future_slot = Slot::new(5); + let msg = make_payload_attestation(future_slot, 0, ctx.genesis_block_root); + let result = VerifiedPayloadAttestationMessage::new(msg, &gossip); + assert!(matches!( + result, + Err(PayloadAttestationError::FutureSlot { .. }) + )); +} + +#[test] +fn past_slot() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + let ctx = TestContext::new(); + ctx.slot_clock.set_slot(5); + let gossip = ctx.gossip_ctx(); + + let msg = make_payload_attestation(Slot::new(0), 0, ctx.genesis_block_root); + let result = VerifiedPayloadAttestationMessage::new(msg, &gossip); + assert!(matches!( + result, + Err(PayloadAttestationError::PastSlot { .. }) + )); +} + +#[test] +fn prior_payload_attestation_message_known() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + let slot = Slot::new(1); + let ptc_members = ctx.ptc_members(slot); + let validator_index = ptc_members[0] as u64; + + ctx.observed_payload_attesters + .write() + .observe_validator(slot, validator_index as usize, ()) + .expect("should observe"); + + let msg = make_payload_attestation(slot, validator_index, ctx.genesis_block_root); + let result = VerifiedPayloadAttestationMessage::new(msg, &gossip); + assert!( + matches!( + result, + Err(PayloadAttestationError::PriorPayloadAttestationMessageKnown { .. }) + ), + "expected PriorPayloadAttestationMessageKnown, got: {:?}", + result + ); +} + +#[test] +fn unknown_head_block() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + + let unknown_root = Hash256::repeat_byte(0xff); + let msg = make_payload_attestation(Slot::new(1), 0, unknown_root); + let result = VerifiedPayloadAttestationMessage::new(msg, &gossip); + assert!( + matches!( + result, + Err(PayloadAttestationError::UnknownHeadBlock { .. }) + ), + "expected UnknownHeadBlock, got: {:?}", + result + ); +} + +#[test] +fn not_in_ptc() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + let slot = Slot::new(1); + + let ptc_members = ctx.ptc_members(slot); + let non_ptc_validator = (0..NUM_VALIDATORS as u64) + .find(|&i| !ptc_members.contains(&(i as usize))) + .expect("should find non-PTC validator"); + + let msg = make_payload_attestation(slot, non_ptc_validator, ctx.genesis_block_root); + let result = VerifiedPayloadAttestationMessage::new(msg, &gossip); + assert!(matches!( + result, + Err(PayloadAttestationError::NotInPTC { .. }) + )); +} + +#[test] +fn invalid_signature() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + let slot = Slot::new(1); + + let ptc_members = ctx.ptc_members(slot); + let validator_index = ptc_members[0] as u64; + + let msg = make_payload_attestation(slot, validator_index, ctx.genesis_block_root); + let result = VerifiedPayloadAttestationMessage::new(msg, &gossip); + assert!(matches!( + result, + Err(PayloadAttestationError::InvalidSignature) + )); +} + +#[test] +fn valid_payload_attestation() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + let slot = Slot::new(1); + + let ptc_members = ctx.ptc_members(slot); + let validator_index = ptc_members[0] as u64; + + let data = PayloadAttestationData { + beacon_block_root: ctx.genesis_block_root, + slot, + payload_present: true, + blob_data_available: true, + }; + let msg = ctx.sign_payload_attestation(data, validator_index); + let result = VerifiedPayloadAttestationMessage::new(msg, &gossip); + assert!( + result.is_ok(), + "expected Ok, got: {:?}", + result.unwrap_err() + ); +} + +#[test] +fn duplicate_after_valid() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + let slot = Slot::new(1); + + let ptc_members = ctx.ptc_members(slot); + let validator_index = ptc_members[0] as u64; + + let data = PayloadAttestationData { + beacon_block_root: ctx.genesis_block_root, + slot, + payload_present: true, + blob_data_available: true, + }; + + let msg1 = ctx.sign_payload_attestation(data.clone(), validator_index); + let result1 = VerifiedPayloadAttestationMessage::new(msg1, &gossip); + assert!( + result1.is_ok(), + "first message should pass: {:?}", + result1.unwrap_err() + ); + + let msg2 = ctx.sign_payload_attestation(data, validator_index); + let result2 = VerifiedPayloadAttestationMessage::new(msg2, &gossip); + assert!(matches!( + result2, + Err(PayloadAttestationError::PriorPayloadAttestationMessageKnown { .. }) + )); +} 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 e52454d999..0a31efdbad 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -3747,10 +3747,8 @@ impl NetworkBeaconProcessor { ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); } - PayloadAttestationError::PastSlot { .. } => { - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); - } - PayloadAttestationError::PriorPayloadAttestationMessageKnown { .. } => { + PayloadAttestationError::PastSlot { .. } + | PayloadAttestationError::PriorPayloadAttestationMessageKnown { .. } => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); } PayloadAttestationError::UnknownHeadBlock { .. } => { From b36219d83d7a628be224d01673f1b903abf2281e Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 14:08:55 +0900 Subject: [PATCH 12/34] Add canonicity check and tests --- beacon_node/beacon_chain/src/beacon_chain.rs | 7 +- .../tests/attestation_production.rs | 115 +++++++++++++++++- 2 files changed, 115 insertions(+), 7 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 96ed5206bd..232da16388 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2092,10 +2092,6 @@ impl BeaconChain { ) }; - // TODO(gloas): add integration test: verify that - // `produce_unaggregated_attestation` sets index=1 when payload received for a prior - // slot, index=0 for same-slot, and index=0 when payload not received. - // // For gloas the attestation data index indicates payload presence: // `payload_present=false` for same-slot attestations or when payload not received. // `payload_present=true` when attesting to a prior slot whose payload has been received. @@ -2106,8 +2102,7 @@ impl BeaconChain { && !is_attesting_to_head_slot { self.canonical_head - .fork_choice_read_lock() - .is_payload_received(&beacon_block_root) + .block_has_canonical_payload(&beacon_block_root, &self.spec)? } else { false }; diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index a3ab959d12..c021f6d6c2 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -8,7 +8,7 @@ use beacon_chain::{StateSkipConfig, WhenSlotSkipped, metrics}; use bls::{AggregateSignature, Keypair}; use std::sync::{Arc, LazyLock}; use tree_hash::TreeHash; -use types::{Attestation, EthSpec, MainnetEthSpec, RelativeEpoch, Slot}; +use types::{Attestation, ChainSpec, EthSpec, ForkName, MainnetEthSpec, RelativeEpoch, Slot}; pub const VALIDATOR_COUNT: usize = 32; @@ -313,3 +313,116 @@ async fn early_attester_cache_old_request() { .unwrap(); assert_eq!(attested_block.slot(), attest_slot); } + +/// Verify that `produce_unaggregated_attestation` sets `data.index = 1` (payload_present) +/// when a gloas validator attests to a prior slot whose block+envelope have been received. +/// +/// Setup: build a chain at gloas genesis, produce a block with envelope at slot N, +/// then advance the clock to slot N+1 without producing a block (skipped slot). +/// Attesting at slot N+1 should target the block at slot N with payload_present = true. +#[tokio::test] +async fn gloas_attestation_index_payload_present() { + let spec = Arc::new(ForkName::Gloas.make_genesis_spec(ChainSpec::mainnet())); + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + let chain = &harness.chain; + + // Build a few blocks so the chain is established (slots 1..=3). + harness.advance_slot(); + harness + .extend_chain( + 3, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let head = chain.head_snapshot(); + assert_eq!(head.beacon_block.slot(), Slot::new(3)); + + // Advance clock to slot 4 without producing a block (skipped slot). + harness.advance_slot(); + let attest_slot = chain.slot().unwrap(); + assert_eq!(attest_slot, Slot::new(4)); + + // Attest at slot 4 — this should target the block at slot 3 whose payload was received. + let attestation = chain + .produce_unaggregated_attestation(attest_slot, 0) + .expect("should produce attestation"); + + assert_eq!(attestation.data().slot, attest_slot); + assert_eq!( + attestation.data().index, + 1, + "gloas attestation to prior slot with payload should have index=1 (payload_present)" + ); +} + +/// Verify that `produce_unaggregated_attestation` sets `data.index = 0` (payload NOT present) +/// when a gloas validator attests to a prior slot whose block was imported but whose +/// payload envelope was never received. +/// +/// Setup: build a chain at gloas genesis through slot 2, then at slot 3 import only the +/// beacon block (no envelope), advance to slot 4 (skipped), and attest. +#[tokio::test] +async fn gloas_attestation_index_payload_absent() { + let spec = Arc::new(ForkName::Gloas.make_genesis_spec(ChainSpec::mainnet())); + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + let chain = &harness.chain; + + // Build slots 1..=2 normally (with envelopes). + harness.advance_slot(); + harness + .extend_chain( + 2, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + assert_eq!(chain.head_snapshot().beacon_block.slot(), Slot::new(2)); + + // Slot 3: produce and import the beacon block but do NOT process the envelope. + harness.advance_slot(); + let state = harness.get_current_state(); + let (block_contents, _envelope, _new_state) = + harness.make_block_with_envelope(state, Slot::new(3)).await; + + let block_root = block_contents.0.canonical_root(); + harness + .process_block(Slot::new(3), block_root, block_contents) + .await + .expect("block should import without envelope"); + + assert_eq!(chain.head_snapshot().beacon_block.slot(), Slot::new(3)); + + // Advance clock to slot 4 without producing a block (skipped slot). + harness.advance_slot(); + let attest_slot = chain.slot().unwrap(); + assert_eq!(attest_slot, Slot::new(4)); + + // Attest at slot 4 — targets slot 3 whose payload was NOT received. + let attestation = chain + .produce_unaggregated_attestation(attest_slot, 0) + .expect("should produce attestation"); + + assert_eq!(attestation.data().slot, attest_slot); + assert_eq!( + attestation.data().index, + 0, + "gloas attestation to prior slot without payload should have index=0 (payload_absent)" + ); +} From 1229abf5cf2bd76a27b2b49c5298c0742b27c32f Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 14:22:24 +0900 Subject: [PATCH 13/34] Fix test --- .../tests/attestation_production.rs | 18 ++++++++++++------ consensus/fork_choice/src/fork_choice.rs | 5 ----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index c021f6d6c2..185a7b4c90 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -2,13 +2,15 @@ use beacon_chain::attestation_simulator::produce_unaggregated_attestation; use beacon_chain::custody_context::NodeCustodyType; -use beacon_chain::test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy}; +use beacon_chain::test_utils::{ + AttestationStrategy, BeaconChainHarness, BlockStrategy, fork_name_from_env, test_spec, +}; use beacon_chain::validator_monitor::UNAGGREGATED_ATTESTATION_LAG_SLOTS; use beacon_chain::{StateSkipConfig, WhenSlotSkipped, metrics}; use bls::{AggregateSignature, Keypair}; use std::sync::{Arc, LazyLock}; use tree_hash::TreeHash; -use types::{Attestation, ChainSpec, EthSpec, ForkName, MainnetEthSpec, RelativeEpoch, Slot}; +use types::{Attestation, EthSpec, ForkName, MainnetEthSpec, RelativeEpoch, Slot}; pub const VALIDATOR_COUNT: usize = 32; @@ -322,10 +324,12 @@ async fn early_attester_cache_old_request() { /// Attesting at slot N+1 should target the block at slot N with payload_present = true. #[tokio::test] async fn gloas_attestation_index_payload_present() { - let spec = Arc::new(ForkName::Gloas.make_genesis_spec(ChainSpec::mainnet())); + if fork_name_from_env().is_some_and(|f| !f.gloas_enabled()) { + return; + } let harness = BeaconChainHarness::builder(MainnetEthSpec) - .spec(spec) + .default_spec() .keypairs(KEYPAIRS[..].to_vec()) .fresh_ephemeral_store() .mock_execution_layer() @@ -372,10 +376,12 @@ async fn gloas_attestation_index_payload_present() { /// beacon block (no envelope), advance to slot 4 (skipped), and attest. #[tokio::test] async fn gloas_attestation_index_payload_absent() { - let spec = Arc::new(ForkName::Gloas.make_genesis_spec(ChainSpec::mainnet())); + if fork_name_from_env().is_some_and(|f| !f.gloas_enabled()) { + return; + } let harness = BeaconChainHarness::builder(MainnetEthSpec) - .spec(spec) + .default_spec() .keypairs(KEYPAIRS[..].to_vec()) .fresh_ephemeral_store() .mock_execution_layer() diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 781777cab7..f9d779fd24 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1498,11 +1498,6 @@ where && self.is_finalized_checkpoint_or_descendant(*block_root) } - /// Returns `true` if the block's execution payload envelope has been received. - pub fn is_payload_received(&self, block_root: &Hash256) -> bool { - self.proto_array.is_payload_received(block_root) - } - /// Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root. pub fn get_block(&self, block_root: &Hash256) -> Option { if self.is_finalized_checkpoint_or_descendant(*block_root) { From 7e16aadde521bf0fec26ee9c8215916cdd2fdbfa Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 14:32:49 +0900 Subject: [PATCH 14/34] Lint --- beacon_node/beacon_chain/tests/attestation_production.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index 185a7b4c90..7c584deff3 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -3,14 +3,14 @@ use beacon_chain::attestation_simulator::produce_unaggregated_attestation; use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::test_utils::{ - AttestationStrategy, BeaconChainHarness, BlockStrategy, fork_name_from_env, test_spec, + AttestationStrategy, BeaconChainHarness, BlockStrategy, fork_name_from_env, }; use beacon_chain::validator_monitor::UNAGGREGATED_ATTESTATION_LAG_SLOTS; use beacon_chain::{StateSkipConfig, WhenSlotSkipped, metrics}; use bls::{AggregateSignature, Keypair}; use std::sync::{Arc, LazyLock}; use tree_hash::TreeHash; -use types::{Attestation, EthSpec, ForkName, MainnetEthSpec, RelativeEpoch, Slot}; +use types::{Attestation, EthSpec, MainnetEthSpec, RelativeEpoch, Slot}; pub const VALIDATOR_COUNT: usize = 32; From dacb8aeffeee89f07ad3758968def056aeadec86 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 15:30:34 +0900 Subject: [PATCH 15/34] Small fix --- beacon_node/beacon_chain/src/beacon_chain.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 232da16388..d44a8d7ecc 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1956,7 +1956,7 @@ impl BeaconChain { let beacon_block_root; let beacon_state_root; let target; - let is_attesting_to_head_slot; + let is_same_slot_attestation; let current_epoch_attesting_info: Option<(Checkpoint, usize)>; let head_timer = metrics::start_timer(&metrics::ATTESTATION_PRODUCTION_HEAD_SCRAPE_SECONDS); let head_span = debug_span!("attestation_production_head_scrape").entered(); @@ -1993,16 +1993,20 @@ impl BeaconChain { }); } - is_attesting_to_head_slot = request_slot >= head_state.slot(); + let is_attesting_to_head_slot = request_slot >= head_state.slot(); + if is_attesting_to_head_slot { // When attesting to the head slot or later, always use the head of the chain. beacon_block_root = head.beacon_block_root; beacon_state_root = head.beacon_state_root(); + is_same_slot_attestation = request_slot == head.beacon_block.slot(); } else { // Permit attesting to slots *prior* to the current head. This is desirable when // the VC and BN are out-of-sync due to time issues or overloading. beacon_block_root = *head_state.get_block_root(request_slot)?; beacon_state_root = *head_state.get_state_root(request_slot)?; + // TODO(gloas) if request_slot is a skipped slot, this is not a same slot attestation. + is_same_slot_attestation = true; }; let target_slot = request_epoch.start_slot(T::EthSpec::slots_per_epoch()); @@ -2099,7 +2103,7 @@ impl BeaconChain { .spec .fork_name_at_slot::(request_slot) .gloas_enabled() - && !is_attesting_to_head_slot + && !is_same_slot_attestation { self.canonical_head .block_has_canonical_payload(&beacon_block_root, &self.spec)? From 2e561a2a9f9f0f933617b075c45d8d0265f87e3a Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 15:44:33 +0900 Subject: [PATCH 16/34] Handle historic attestations correctly --- beacon_node/beacon_chain/src/beacon_chain.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index d44a8d7ecc..390cbf5cb8 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2005,8 +2005,14 @@ impl BeaconChain { // the VC and BN are out-of-sync due to time issues or overloading. beacon_block_root = *head_state.get_block_root(request_slot)?; beacon_state_root = *head_state.get_state_root(request_slot)?; - // TODO(gloas) if request_slot is a skipped slot, this is not a same slot attestation. - is_same_slot_attestation = true; + + // Fetch the previous block root. If the previous block root equals + // the block root being attested to, the `request_slot` is a skipped slot + // and this is not a same slot attestation. + let prior_slot_root = head_state + .get_block_root(request_slot.saturating_sub(1u64)) + .ok(); + is_same_slot_attestation = prior_slot_root != Some(&beacon_block_root); }; let target_slot = request_epoch.start_slot(T::EthSpec::slots_per_epoch()); From 5beddd17cef654150f265c9cc149b2d37ceb1cf6 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 15:52:11 +0900 Subject: [PATCH 17/34] Fix tests --- .../beacon_chain/tests/attestation_production.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index 7c584deff3..224d40290f 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -208,7 +208,15 @@ async fn produces_attestations() { &AggregateSignature::infinity(), "bad signature" ); - assert_eq!(data.index, index, "bad index"); + if harness + .spec + .fork_name_at_slot::(data.slot) + .gloas_enabled() + { + assert!(data.index <= 1, "invalid index"); + } else { + assert_eq!(data.index, index, "bad index"); + } assert_eq!(data.slot, slot, "bad slot"); assert_eq!(data.beacon_block_root, block_root, "bad block root"); assert_eq!( From a7fc388a9af40df1beeab5166bad8e5dc3fc6f78 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 16:15:54 +0900 Subject: [PATCH 18/34] Fix early attester cache --- beacon_node/beacon_chain/src/early_attester_cache.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index 5b55c8e2da..a433c4fc75 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -197,6 +197,15 @@ impl EarlyAttesterCache { item.committee_lengths .get_committee_length::(request_slot, request_index, spec)?; + let is_same_slot_attestation = request_slot == item.block.slot(); + let payload_present = if spec.fork_name_at_slot::(request_slot).gloas_enabled() + && !is_same_slot_attestation + { + item.proto_block.payload_status == PayloadStatus::Full + } else { + false + }; + let attestation = Attestation::empty_for_signing( request_index, committee_len, @@ -204,7 +213,7 @@ impl EarlyAttesterCache { item.beacon_block_root, item.source, item.target, - false, + payload_present, spec, ) .map_err(Error::AttestationError)?; From 9ef3799c3610befc50239fb367bbdd2e9a99dae8 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 16:34:17 +0900 Subject: [PATCH 19/34] Add same slot attestation logic to early attester cache --- .../beacon_chain/src/early_attester_cache.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index a433c4fc75..8b8c8edc9b 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -165,6 +165,9 @@ impl EarlyAttesterCache { /// - There is a cache `item` present. /// - If `request_slot` is in the same epoch as `item.epoch`. /// - If `request_index` does not exceed `item.committee_count`. + /// + /// Post gloas an additional condition must be met: + /// - If `request_slot` is the same slot as `item.block.slot` (i.e. a same slot attestation) #[instrument(skip_all, fields(%request_slot, %request_index), level = "debug")] pub fn try_attest( &self, @@ -198,13 +201,10 @@ impl EarlyAttesterCache { .get_committee_length::(request_slot, request_index, spec)?; let is_same_slot_attestation = request_slot == item.block.slot(); - let payload_present = if spec.fork_name_at_slot::(request_slot).gloas_enabled() - && !is_same_slot_attestation - { - item.proto_block.payload_status == PayloadStatus::Full - } else { - false - }; + if spec.fork_name_at_slot::(request_slot).gloas_enabled() && !is_same_slot_attestation { + return Ok(None); + } + let payload_present = false; let attestation = Attestation::empty_for_signing( request_index, From 3f8621fd521efa0f6de517f44e25aa73a2c18474 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 16:48:56 +0900 Subject: [PATCH 20/34] Disable early attester cache test for non same slot attestaitons psot gloas --- .../tests/attestation_production.rs | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index 224d40290f..1b87fc041a 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -236,27 +236,35 @@ async fn produces_attestations() { .build_range_sync_block_from_store_blobs(Some(block_root), Arc::new(block.clone())); let available_block = range_sync_block.into_available_block(); - let early_attestation = { - let proto_block = chain - .canonical_head - .fork_choice_read_lock() - .get_block(&block_root) - .unwrap(); - chain - .early_attester_cache - .add_head_block(block_root, &available_block, proto_block, &state) - .unwrap(); - chain - .early_attester_cache - .try_attest(slot, index, &chain.spec) - .unwrap() - .unwrap() - }; + // For Gloas non-same-slot attestations, the early attester cache returns None. + let is_same_slot_attestation = slot == block_slot; + let is_gloas = harness + .spec + .fork_name_at_slot::(slot) + .gloas_enabled(); + if !is_gloas || is_same_slot_attestation { + let early_attestation = { + let proto_block = chain + .canonical_head + .fork_choice_read_lock() + .get_block(&block_root) + .unwrap(); + chain + .early_attester_cache + .add_head_block(block_root, &available_block, proto_block, &state) + .unwrap(); + chain + .early_attester_cache + .try_attest(slot, index, &chain.spec) + .unwrap() + .unwrap() + }; - assert_eq!( - attestation, early_attestation, - "early attester cache inconsistent" - ); + assert_eq!( + attestation, early_attestation, + "early attester cache inconsistent" + ); + } } } } From 98212e8815c363889860d7662c8c97ff9db650c5 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 23:10:15 +0200 Subject: [PATCH 21/34] Fix builder exit signature batch verification logic and small refactor --- .../process_operations.rs | 27 +++++++++---------- .../per_block_processing/signature_sets.rs | 18 ++++++++++--- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index f1de284fc8..422e0afe06 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -8,6 +8,7 @@ use crate::per_block_processing::builder::{ convert_validator_index_to_builder_index, is_builder_index, }; use crate::per_block_processing::errors::{BlockProcessingError, ExitInvalid, IntoWithIndex}; +use crate::per_block_processing::signature_sets::{exit_signature_set, get_pubkey_from_state}; use crate::per_block_processing::verify_payload_attestation::verify_payload_attestation; use bls::{PublicKeyBytes, SignatureBytes}; use ssz_types::FixedVector; @@ -547,7 +548,8 @@ fn process_builder_voluntary_exit( let builder_index = convert_validator_index_to_builder_index(signed_exit.message.validator_index); - let builder = state + // Verify builder is known + state .builders()? .get(builder_index as usize) .cloned() @@ -570,22 +572,17 @@ fn process_builder_voluntary_exit( )); } - // Verify signature (using EIP-7044 domain: capella_fork_version for Deneb+) if verify_signatures.is_true() { - let pubkey = builder.pubkey; - let domain = spec.compute_domain( - Domain::VoluntaryExit, - spec.capella_fork_version, - state.genesis_validators_root(), + verify!( + exit_signature_set( + state, + |i| get_pubkey_from_state(state, i), + signed_exit, + spec + )? + .verify(), + ExitInvalid::BadSignature ); - let message = signed_exit.message.signing_root(domain); - // TODO(gloas): use builder pubkey cache once available - let bls_pubkey = pubkey - .decompress() - .map_err(|_| BlockOperationError::invalid(ExitInvalid::BadSignature))?; - if !signed_exit.signature.verify(&bls_pubkey, message) { - return Err(BlockOperationError::invalid(ExitInvalid::BadSignature)); - } } // Initiate builder exit 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 5c1767f227..0686c4d605 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -2,6 +2,7 @@ //! validated individually, or alongside in others in a potentially cheaper bulk operation. //! //! This module exposes one function to extract each type of `SignatureSet` from a `BeaconBlock`. +use super::builder::{convert_validator_index_to_builder_index, is_builder_index}; use bls::{AggregateSignature, PublicKey, PublicKeyBytes, Signature, SignatureSet}; use ssz::DecodeError; use std::borrow::Cow; @@ -503,7 +504,7 @@ pub fn deposit_pubkey_signature_message( } /// Returns a signature set that is valid if the `SignedVoluntaryExit` was signed by the indicated -/// validator. +/// validator (or builder, in the case of a builder exit). pub fn exit_signature_set<'a, E, F>( state: &'a BeaconState, get_pubkey: F, @@ -515,7 +516,18 @@ where F: Fn(usize) -> Option>, { let exit = &signed_exit.message; - let proposer_index = exit.validator_index as usize; + let validator_index = exit.validator_index; + + let is_builder_exit = + state.fork_name_unchecked().gloas_enabled() && is_builder_index(validator_index); + + let pubkey = if is_builder_exit { + let builder_index = convert_validator_index_to_builder_index(validator_index); + get_builder_pubkey_from_state(state, builder_index) + .ok_or(Error::ValidatorUnknown(validator_index))? + } else { + get_pubkey(validator_index as usize).ok_or(Error::ValidatorUnknown(validator_index))? + }; let domain = if state.fork_name_unchecked().deneb_enabled() { // EIP-7044 @@ -537,7 +549,7 @@ where Ok(SignatureSet::single_pubkey( &signed_exit.signature, - get_pubkey(proposer_index).ok_or(Error::ValidatorUnknown(proposer_index as u64))?, + pubkey, message, )) } From 774b6dca92a5057ea78e1cd4d34ccd916edf0891 Mon Sep 17 00:00:00 2001 From: hopinheimer Date: Mon, 27 Apr 2026 01:04:15 -0400 Subject: [PATCH 22/34] fetching from hot_state in case of liveness fault --- .../beacon_chain/src/observed_attesters.rs | 12 ++-- .../gossip_verified_payload_attestation.rs | 59 +++++++++++++++++-- .../payload_attestation_verification/tests.rs | 31 +--------- consensus/types/src/core/consts.rs | 1 - 4 files changed, 65 insertions(+), 38 deletions(-) diff --git a/beacon_node/beacon_chain/src/observed_attesters.rs b/beacon_node/beacon_chain/src/observed_attesters.rs index 79bc181843..4bb536880c 100644 --- a/beacon_node/beacon_chain/src/observed_attesters.rs +++ b/beacon_node/beacon_chain/src/observed_attesters.rs @@ -14,7 +14,7 @@ //! - `ObservedSyncAggregators`: allows filtering sync committee contributions from the same aggregators in //! the same slot and in the same subcommittee. -use crate::types::consts::{altair::TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE, gloas::PTC_SIZE}; +use crate::types::consts::altair::TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE; use bitvec::vec::BitVec; use std::collections::{HashMap, HashSet}; use std::hash::Hash; @@ -43,7 +43,7 @@ pub type ObservedAggregators = AutoPruningEpochContainer; pub type ObservedSyncAggregators = AutoPruningSlotContainer; pub type ObservedPayloadAttesters = - AutoPruningSlotContainer; + AutoPruningSlotContainer, E>; #[derive(Debug, PartialEq)] pub enum Error { @@ -259,20 +259,22 @@ impl Item<()> for SyncAggregatorSlotHashSet { /// Stores a `HashSet` of validator indices that have sent a payload attestation gossip /// message during a slot. -pub struct PayloadAttesterSlotHashSet { +pub struct PayloadAttesterSlotHashSet { set: HashSet, + phantom: PhantomData, } -impl Item<()> for PayloadAttesterSlotHashSet { +impl Item<()> for PayloadAttesterSlotHashSet { fn with_capacity(capacity: usize) -> Self { Self { set: HashSet::with_capacity(capacity), + phantom: PhantomData, } } /// Defaults to `PTC_SIZE`, the maximum number of payload attesters per slot. fn default_capacity() -> usize { - PTC_SIZE as usize + E::ptc_size() } fn len(&self) -> usize { diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs index 5ee50ece95..a166013d0a 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs @@ -1,4 +1,5 @@ use super::Error; +use crate::beacon_chain::BeaconStore; use crate::canonical_head::CanonicalHead; use crate::observed_attesters::ObservedPayloadAttesters; use crate::validator_pubkey_cache::ValidatorPubkeyCache; @@ -8,8 +9,12 @@ use educe::Educe; use parking_lot::RwLock; use slot_clock::SlotClock; use state_processing::per_block_processing::signature_sets::indexed_payload_attestation_signature_set; +use state_processing::state_advance::partial_state_advance; +use safe_arith::SafeArith; use std::borrow::Cow; -use types::{ChainSpec, IndexedPayloadAttestation, PTC, PayloadAttestationMessage, Slot}; +use types::{ + ChainSpec, EthSpec, IndexedPayloadAttestation, PTC, PayloadAttestationMessage, Slot, +}; pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { pub slot_clock: &'a T::SlotClock, @@ -17,6 +22,7 @@ pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { pub observed_payload_attesters: &'a RwLock>, pub canonical_head: &'a CanonicalHead, pub validator_pubkey_cache: &'a RwLock>, + pub store: &'a BeaconStore, } /// A `PayloadAttestationMessage` that has been verified for propagation on the gossip network. @@ -71,12 +77,56 @@ impl VerifiedPayloadAttestationMessage { return Err(Error::UnknownHeadBlock { beacon_block_root }); } - // Get head state for PTC computation. + // Get head state for PTC computation. If the cached head state is too stale + // (e.g. during liveness failures with many skipped slots), fall back to loading + // a more recent state from the store and advancing it if necessary. let head = ctx.canonical_head.cached_head(); let head_state = &head.snapshot.beacon_state; + let message_epoch = slot.epoch(T::EthSpec::slots_per_epoch()); + let state_epoch = head_state.current_epoch(); + + // get_ptc can serve epochs in [state_epoch - 1, state_epoch + min_seed_lookahead]. + // If the message epoch is beyond that range, the head state is stale. + let advanced_state = if message_epoch + > state_epoch + .safe_add(ctx.spec.min_seed_lookahead) + .map_err(BeaconChainError::from)? + { + let head_block_root = head.head_block_root(); + let target_slot = message_epoch.start_slot(T::EthSpec::slots_per_epoch()); + + let (state_root, mut state) = ctx + .store + .get_advanced_hot_state( + head_block_root, + target_slot, + head.snapshot.beacon_state_root(), + ) + .map_err(BeaconChainError::from)? + .ok_or(BeaconChainError::MissingBeaconState( + head.snapshot.beacon_state_root(), + ))?; + + if state + .current_epoch() + .safe_add(ctx.spec.min_seed_lookahead) + .map_err(BeaconChainError::from)? + < message_epoch + { + partial_state_advance(&mut state, Some(state_root), target_slot, ctx.spec) + .map_err(BeaconChainError::from)?; + } + + Some(state) + } else { + None + }; + + let state = advanced_state.as_ref().unwrap_or(head_state); + // [REJECT] `validator_index` is within `get_ptc(state, data.slot)`. - let ptc = head_state.get_ptc(slot, ctx.spec)?; + let ptc = state.get_ptc(slot, ctx.spec)?; if !ptc.0.contains(&(validator_index as usize)) { return Err(Error::NotInPTC { validator_index, @@ -96,7 +146,7 @@ impl VerifiedPayloadAttestationMessage { // [REJECT] The signature is valid with respect to the `validator_index`. let pubkey_cache = ctx.validator_pubkey_cache.read(); let signature_set = indexed_payload_attestation_signature_set( - head_state, + state, |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), &indexed_payload_attestation.signature, &indexed_payload_attestation, @@ -154,6 +204,7 @@ impl BeaconChain { observed_payload_attesters: &self.observed_payload_attesters, canonical_head: &self.canonical_head, validator_pubkey_cache: &self.validator_pubkey_cache, + store: &self.store, } } diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs index eb3359b291..b919dbf267 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs @@ -41,6 +41,7 @@ struct TestContext { keypairs: Vec, spec: ChainSpec, genesis_block_root: Hash256, + store: Arc, store::MemoryStore>>, } impl TestContext { @@ -104,6 +105,7 @@ impl TestContext { keypairs, spec, genesis_block_root: block_root, + store, } } @@ -114,6 +116,7 @@ impl TestContext { observed_payload_attesters: &self.observed_payload_attesters, canonical_head: &self.canonical_head, validator_pubkey_cache: &self.validator_pubkey_cache, + store: &self.store, } } @@ -198,34 +201,6 @@ fn past_slot() { )); } -#[test] -fn prior_payload_attestation_message_known() { - if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { - return; - } - let ctx = TestContext::new(); - let gossip = ctx.gossip_ctx(); - let slot = Slot::new(1); - let ptc_members = ctx.ptc_members(slot); - let validator_index = ptc_members[0] as u64; - - ctx.observed_payload_attesters - .write() - .observe_validator(slot, validator_index as usize, ()) - .expect("should observe"); - - let msg = make_payload_attestation(slot, validator_index, ctx.genesis_block_root); - let result = VerifiedPayloadAttestationMessage::new(msg, &gossip); - assert!( - matches!( - result, - Err(PayloadAttestationError::PriorPayloadAttestationMessageKnown { .. }) - ), - "expected PriorPayloadAttestationMessageKnown, got: {:?}", - result - ); -} - #[test] fn unknown_head_block() { if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { diff --git a/consensus/types/src/core/consts.rs b/consensus/types/src/core/consts.rs index 211208fc80..049094da76 100644 --- a/consensus/types/src/core/consts.rs +++ b/consensus/types/src/core/consts.rs @@ -38,5 +38,4 @@ pub mod gloas { pub const ATTESTATION_TIMELINESS_INDEX: usize = 0; pub const PTC_TIMELINESS_INDEX: usize = 1; pub const NUM_BLOCK_TIMELINESS_DEADLINES: usize = 2; - pub const PTC_SIZE: u64 = 512; } From 8d3bda0115e0dcfbf49a487ff6a237861e0eb61d Mon Sep 17 00:00:00 2001 From: hopinheimer Date: Mon, 27 Apr 2026 01:15:12 -0400 Subject: [PATCH 23/34] fmt --- beacon_node/beacon_chain/src/lib.rs | 2 +- .../gossip_verified_payload_attestation.rs | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index c3fc4a215d..d70fc1b3ec 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -43,8 +43,8 @@ pub mod observed_block_producers; pub mod observed_data_sidecars; pub mod observed_operations; mod observed_slashable; -pub mod payload_attestation_verification; pub mod partial_data_column_assembler; +pub mod payload_attestation_verification; pub mod payload_bid_verification; pub mod payload_envelope_streamer; pub mod payload_envelope_verification; diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs index a166013d0a..c2ec8cfa4d 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs @@ -7,14 +7,12 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, metrics}; use bls::AggregateSignature; use educe::Educe; use parking_lot::RwLock; +use safe_arith::SafeArith; use slot_clock::SlotClock; use state_processing::per_block_processing::signature_sets::indexed_payload_attestation_signature_set; use state_processing::state_advance::partial_state_advance; -use safe_arith::SafeArith; use std::borrow::Cow; -use types::{ - ChainSpec, EthSpec, IndexedPayloadAttestation, PTC, PayloadAttestationMessage, Slot, -}; +use types::{ChainSpec, EthSpec, IndexedPayloadAttestation, PTC, PayloadAttestationMessage, Slot}; pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { pub slot_clock: &'a T::SlotClock, From aca9765ae7310ae5d953a0ca625b9f6f7754d6ab Mon Sep 17 00:00:00 2001 From: hopinheimer Date: Mon, 27 Apr 2026 01:57:22 -0400 Subject: [PATCH 24/34] fix `genesis_block` init in tests --- .../src/payload_attestation_verification/tests.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs index b919dbf267..cd234d6fe4 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs @@ -4,12 +4,13 @@ use std::time::Duration; use bls::{Keypair, Signature}; use fork_choice::ForkChoice; use genesis::{generate_deterministic_keypairs, interop_genesis_state}; +use state_processing::genesis::genesis_block; use parking_lot::RwLock; use proto_array::PayloadStatus; use slot_clock::{SlotClock, TestingSlotClock}; use store::{HotColdDB, StoreConfig}; use types::{ - BeaconBlock, ChainSpec, Checkpoint, Domain, Epoch, EthSpec, Hash256, MinimalEthSpec, + ChainSpec, Checkpoint, Domain, Epoch, EthSpec, Hash256, MinimalEthSpec, PayloadAttestationData, PayloadAttestationMessage, SignedBeaconBlock, SignedRoot, Slot, }; @@ -63,11 +64,11 @@ impl TestContext { root: Hash256::ZERO, }; - let mut genesis_block = BeaconBlock::empty(&spec); - *genesis_block.state_root_mut() = state + let mut block = genesis_block(&state, &spec).expect("should build genesis block"); + *block.state_root_mut() = state .update_tree_hash_cache() .expect("should hash genesis state"); - let signed_block = SignedBeaconBlock::from_block(genesis_block, Signature::empty()); + let signed_block = SignedBeaconBlock::from_block(block, Signature::empty()); let block_root = signed_block.canonical_root(); let snapshot = BeaconSnapshot::new( From 2f98ca6d552458e25483d52df1c09da73a8bb8b8 Mon Sep 17 00:00:00 2001 From: hopinheimer Date: Mon, 27 Apr 2026 01:57:44 -0400 Subject: [PATCH 25/34] fmt --- .../src/payload_attestation_verification/tests.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs index cd234d6fe4..77ba5f8911 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs @@ -4,14 +4,14 @@ use std::time::Duration; use bls::{Keypair, Signature}; use fork_choice::ForkChoice; use genesis::{generate_deterministic_keypairs, interop_genesis_state}; -use state_processing::genesis::genesis_block; use parking_lot::RwLock; use proto_array::PayloadStatus; use slot_clock::{SlotClock, TestingSlotClock}; +use state_processing::genesis::genesis_block; use store::{HotColdDB, StoreConfig}; use types::{ - ChainSpec, Checkpoint, Domain, Epoch, EthSpec, Hash256, MinimalEthSpec, - PayloadAttestationData, PayloadAttestationMessage, SignedBeaconBlock, SignedRoot, Slot, + ChainSpec, Checkpoint, Domain, Epoch, EthSpec, Hash256, MinimalEthSpec, PayloadAttestationData, + PayloadAttestationMessage, SignedBeaconBlock, SignedRoot, Slot, }; use crate::{ From 0fe60af63a66b4c75966c2d8c6b4c377a2195d97 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 27 Apr 2026 08:48:06 +0200 Subject: [PATCH 26/34] Add payload attestation validator duty --- beacon_node/http_api/src/beacon/pool.rs | 36 ++- beacon_node/http_api/src/lib.rs | 5 + common/eth2/src/lib.rs | 20 +- .../lighthouse_validator_store/src/lib.rs | 44 ++- validator_client/signing_method/src/lib.rs | 5 + .../signing_method/src/web3signer.rs | 3 + validator_client/src/lib.rs | 17 ++ .../validator_services/src/lib.rs | 1 + .../src/payload_attestation_service.rs | 262 ++++++++++++++++++ validator_client/validator_store/src/lib.rs | 16 +- 10 files changed, 396 insertions(+), 13 deletions(-) create mode 100644 validator_client/validator_services/src/payload_attestation_service.rs diff --git a/beacon_node/http_api/src/beacon/pool.rs b/beacon_node/http_api/src/beacon/pool.rs index 059573c317..8c39814d35 100644 --- a/beacon_node/http_api/src/beacon/pool.rs +++ b/beacon_node/http_api/src/beacon/pool.rs @@ -17,8 +17,9 @@ use std::sync::Arc; use tokio::sync::mpsc::UnboundedSender; use tracing::{debug, info, warn}; use types::{ - Attestation, AttestationData, AttesterSlashing, ForkName, ProposerSlashing, - SignedBlsToExecutionChange, SignedVoluntaryExit, SingleAttestation, SyncCommitteeMessage, + Attestation, AttestationData, AttesterSlashing, ForkName, PayloadAttestationMessage, + ProposerSlashing, SignedBlsToExecutionChange, SignedVoluntaryExit, SingleAttestation, + SyncCommitteeMessage, }; use warp::filters::BoxedFilter; use warp::{Filter, Reply}; @@ -520,3 +521,34 @@ pub fn post_beacon_pool_attestations_v2( ) .boxed() } + +/// POST beacon/pool/payload_attestations +pub fn post_beacon_pool_payload_attestations( + network_tx_filter: &NetworkTxFilter, + beacon_pool_path: &BeaconPoolPathFilter, +) -> ResponseFilter { + beacon_pool_path + .clone() + .and(warp::path("payload_attestations")) + .and(warp::path::end()) + .and(warp_utils::json::json()) + .and(network_tx_filter.clone()) + .then( + |task_spawner: TaskSpawner, + _chain: Arc>, + messages: Vec, + network_tx: UnboundedSender>| { + task_spawner.blocking_json_task(Priority::P0, move || { + // TODO(gloas): add proper verification once payload_attestation_verification is implemented + for message in messages { + utils::publish_pubsub_message( + &network_tx, + PubsubMessage::PayloadAttestation(Box::new(message)), + )?; + } + Ok(()) + }) + }, + ) + .boxed() +} diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index bd80dd1e82..eafb978b38 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1487,6 +1487,10 @@ pub fn serve( let post_beacon_pool_sync_committees = post_beacon_pool_sync_committees(&network_tx_filter, &beacon_pool_path); + // POST beacon/pool/payload_attestations + let post_beacon_pool_payload_attestations = + post_beacon_pool_payload_attestations(&network_tx_filter, &beacon_pool_path); + // GET beacon/pool/bls_to_execution_changes let get_beacon_pool_bls_to_execution_changes = get_beacon_pool_bls_to_execution_changes(&beacon_pool_path); @@ -3411,6 +3415,7 @@ pub fn serve( .uor(post_beacon_pool_proposer_slashings) .uor(post_beacon_pool_voluntary_exits) .uor(post_beacon_pool_sync_committees) + .uor(post_beacon_pool_payload_attestations) .uor(post_beacon_pool_bls_to_execution_changes) .uor(post_beacon_execution_payload_envelope) .uor(post_beacon_state_validators) diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 4ec75468a2..25e3a8a3f4 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -46,7 +46,7 @@ use ssz::{Decode, Encode}; use std::fmt; use std::future::Future; use std::time::Duration; -use types::PayloadAttestationData; +use types::{PayloadAttestationData, PayloadAttestationMessage}; pub const V1: EndpointVersion = EndpointVersion(1); pub const V2: EndpointVersion = EndpointVersion(2); @@ -1789,6 +1789,24 @@ impl BeaconNodeHttpClient { Ok(()) } + /// `POST beacon/pool/payload_attestations` + pub async fn post_beacon_pool_payload_attestations( + &self, + messages: &[PayloadAttestationMessage], + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("pool") + .push("payload_attestations"); + + self.post(path, &messages).await?; + + Ok(()) + } + /// `POST beacon/pool/bls_to_execution_changes` pub async fn post_beacon_pool_bls_to_execution_changes( &self, diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index c5bcd88eb1..a3ab2ccbe4 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -22,10 +22,11 @@ use types::{ AbstractExecPayload, Address, AggregateAndProof, Attestation, BeaconBlock, BlindedPayload, ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, ExecutionPayloadEnvelope, Fork, FullPayload, Graffiti, Hash256, SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, - SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedRoot, - SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncAggregatorSelectionData, - SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, - ValidatorRegistrationData, VoluntaryExit, graffiti::GraffitiString, + PayloadAttestationData, PayloadAttestationMessage, SignedContributionAndProof, + SignedExecutionPayloadEnvelope, SignedRoot, SignedValidatorRegistrationData, + SignedVoluntaryExit, Slot, SyncAggregatorSelectionData, SyncCommitteeContribution, + SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, + VoluntaryExit, graffiti::GraffitiString, }; use validator_store::{ AggregateToSign, AttestationToSign, ContributionToSign, DoppelgangerStatus, @@ -1423,8 +1424,39 @@ impl ValidatorStore for LighthouseValidatorS }) } - /// Sign an `ExecutionPayloadEnvelope` for Gloas (local building). - /// The proposer acts as the builder and signs with the BeaconBuilder domain. + async fn sign_payload_attestation( + &self, + validator_pubkey: PublicKeyBytes, + data: PayloadAttestationData, + ) -> Result { + let signing_context = self.signing_context( + Domain::PTCAttester, + data.slot.epoch(E::slots_per_epoch()), + ); + + let validator_index = self + .validator_index(&validator_pubkey) + .ok_or(ValidatorStoreError::UnknownPubkey(validator_pubkey))?; + + let signing_method = self.doppelganger_bypassed_signing_method(validator_pubkey)?; + + let signature = signing_method + .get_signature::>( + SignableMessage::PayloadAttestationData(&data), + signing_context, + &self.spec, + &self.task_executor, + ) + .await + .map_err(Error::SpecificError)?; + + Ok(PayloadAttestationMessage { + validator_index, + data, + signature, + }) + } + async fn sign_execution_payload_envelope( &self, validator_pubkey: PublicKeyBytes, diff --git a/validator_client/signing_method/src/lib.rs b/validator_client/signing_method/src/lib.rs index c132d86c17..2f80fa5761 100644 --- a/validator_client/signing_method/src/lib.rs +++ b/validator_client/signing_method/src/lib.rs @@ -50,6 +50,7 @@ pub enum SignableMessage<'a, E: EthSpec, Payload: AbstractExecPayload = FullP ValidatorRegistration(&'a ValidatorRegistrationData), VoluntaryExit(&'a VoluntaryExit), ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope), + PayloadAttestationData(&'a PayloadAttestationData), } impl> SignableMessage<'_, E, Payload> { @@ -72,6 +73,7 @@ impl> SignableMessage<'_, E, Payload SignableMessage::ValidatorRegistration(v) => v.signing_root(domain), SignableMessage::VoluntaryExit(exit) => exit.signing_root(domain), SignableMessage::ExecutionPayloadEnvelope(e) => e.signing_root(domain), + SignableMessage::PayloadAttestationData(d) => d.signing_root(domain), } } } @@ -238,6 +240,9 @@ impl SigningMethod { SignableMessage::ExecutionPayloadEnvelope(e) => { Web3SignerObject::ExecutionPayloadEnvelope(e) } + SignableMessage::PayloadAttestationData(d) => { + Web3SignerObject::PayloadAttestationData(d) + } }; // Determine the Web3Signer message type. diff --git a/validator_client/signing_method/src/web3signer.rs b/validator_client/signing_method/src/web3signer.rs index e6fc8f3ba2..c2b7e06f92 100644 --- a/validator_client/signing_method/src/web3signer.rs +++ b/validator_client/signing_method/src/web3signer.rs @@ -21,6 +21,7 @@ pub enum MessageType { ValidatorRegistration, // TODO(gloas) verify w/ web3signer specs ExecutionPayloadEnvelope, + PayloadAttestation, } #[derive(Debug, PartialEq, Copy, Clone, Serialize)] @@ -78,6 +79,7 @@ pub enum Web3SignerObject<'a, E: EthSpec, Payload: AbstractExecPayload> { ContributionAndProof(&'a ContributionAndProof), ValidatorRegistration(&'a ValidatorRegistrationData), ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope), + PayloadAttestationData(&'a PayloadAttestationData), } impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Payload> { @@ -144,6 +146,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Pa } Web3SignerObject::ValidatorRegistration(_) => MessageType::ValidatorRegistration, Web3SignerObject::ExecutionPayloadEnvelope(_) => MessageType::ExecutionPayloadEnvelope, + Web3SignerObject::PayloadAttestationData(_) => MessageType::PayloadAttestation, } } } diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index e26d5c3d30..3fbb52fcff 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -45,6 +45,7 @@ use validator_services::{ block_service::{BlockService, BlockServiceBuilder}, duties_service::{self, DutiesService, DutiesServiceBuilder}, latency_service, + payload_attestation_service::{PayloadAttestationService, PayloadAttestationServiceBuilder}, preparation_service::{PreparationService, PreparationServiceBuilder}, sync_committee_service::SyncCommitteeService, }; @@ -83,6 +84,7 @@ pub struct ProductionValidatorClient { block_service: BlockService, SystemTimeSlotClock>, attestation_service: AttestationService, SystemTimeSlotClock>, sync_committee_service: SyncCommitteeService, SystemTimeSlotClock>, + payload_attestation_service: PayloadAttestationService, SystemTimeSlotClock>, doppelganger_service: Option>, preparation_service: PreparationService, SystemTimeSlotClock>, validator_store: Arc>, @@ -552,12 +554,22 @@ impl ProductionValidatorClient { context.executor.clone(), ); + let payload_attestation_service = PayloadAttestationServiceBuilder::new() + .duties_service(duties_service.clone()) + .validator_store(validator_store.clone()) + .slot_clock(slot_clock.clone()) + .beacon_nodes(beacon_nodes.clone()) + .executor(context.executor.clone()) + .chain_spec(context.eth2_config.spec.clone()) + .build()?; + Ok(Self { context, duties_service, block_service, attestation_service, sync_committee_service, + payload_attestation_service, doppelganger_service, preparation_service, validator_store, @@ -629,6 +641,11 @@ impl ProductionValidatorClient { .start_update_service(&self.context.eth2_config.spec) .map_err(|e| format!("Unable to start sync committee service: {}", e))?; + self.payload_attestation_service + .clone() + .start_update_service(&self.context.eth2_config.spec) + .map_err(|e| format!("Unable to start payload attestation service: {}", e))?; + self.preparation_service .clone() .start_update_service(&self.context.eth2_config.spec) diff --git a/validator_client/validator_services/src/lib.rs b/validator_client/validator_services/src/lib.rs index 3b8bd9ae14..0169335a7f 100644 --- a/validator_client/validator_services/src/lib.rs +++ b/validator_client/validator_services/src/lib.rs @@ -3,6 +3,7 @@ pub mod block_service; pub mod duties_service; pub mod latency_service; pub mod notifier_service; +pub mod payload_attestation_service; pub mod preparation_service; pub mod sync; pub mod sync_committee_service; diff --git a/validator_client/validator_services/src/payload_attestation_service.rs b/validator_client/validator_services/src/payload_attestation_service.rs new file mode 100644 index 0000000000..2ae089c762 --- /dev/null +++ b/validator_client/validator_services/src/payload_attestation_service.rs @@ -0,0 +1,262 @@ +use crate::duties_service::DutiesService; +use beacon_node_fallback::BeaconNodeFallback; +use logging::crit; +use slot_clock::SlotClock; +use std::ops::Deref; +use std::sync::Arc; +use task_executor::TaskExecutor; +use tokio::time::{Duration, sleep}; +use tracing::{debug, error, info, warn}; +use types::{ChainSpec, EthSpec}; +use validator_store::ValidatorStore; + +pub struct PayloadAttestationServiceBuilder { + duties_service: Option>>, + validator_store: Option>, + slot_clock: Option, + beacon_nodes: Option>>, + executor: Option, + chain_spec: Option>, +} + +impl PayloadAttestationServiceBuilder { + pub fn new() -> Self { + Self { + duties_service: None, + validator_store: None, + slot_clock: None, + beacon_nodes: None, + executor: None, + chain_spec: None, + } + } + + pub fn duties_service(mut self, service: Arc>) -> Self { + self.duties_service = Some(service); + self + } + + pub fn validator_store(mut self, store: Arc) -> Self { + self.validator_store = Some(store); + self + } + + pub fn slot_clock(mut self, slot_clock: T) -> Self { + self.slot_clock = Some(slot_clock); + self + } + + pub fn beacon_nodes(mut self, beacon_nodes: Arc>) -> Self { + self.beacon_nodes = Some(beacon_nodes); + self + } + + pub fn executor(mut self, executor: TaskExecutor) -> Self { + self.executor = Some(executor); + self + } + + pub fn chain_spec(mut self, chain_spec: Arc) -> Self { + self.chain_spec = Some(chain_spec); + self + } + + pub fn build(self) -> Result, String> { + Ok(PayloadAttestationService { + inner: Arc::new(Inner { + duties_service: self + .duties_service + .ok_or("Cannot build PayloadAttestationService without duties_service")?, + validator_store: self + .validator_store + .ok_or("Cannot build PayloadAttestationService without validator_store")?, + slot_clock: self + .slot_clock + .ok_or("Cannot build PayloadAttestationService without slot_clock")?, + beacon_nodes: self + .beacon_nodes + .ok_or("Cannot build PayloadAttestationService without beacon_nodes")?, + executor: self + .executor + .ok_or("Cannot build PayloadAttestationService without executor")?, + chain_spec: self + .chain_spec + .ok_or("Cannot build PayloadAttestationService without chain_spec")?, + }), + }) + } +} + +pub struct Inner { + duties_service: Arc>, + validator_store: Arc, + slot_clock: T, + beacon_nodes: Arc>, + executor: TaskExecutor, + chain_spec: Arc, +} + +pub struct PayloadAttestationService { + inner: Arc>, +} + +impl Clone for PayloadAttestationService { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl Deref for PayloadAttestationService { + type Target = Inner; + + fn deref(&self) -> &Self::Target { + self.inner.deref() + } +} + +impl PayloadAttestationService { + pub fn start_update_service(self, spec: &ChainSpec) -> Result<(), String> { + let slot_duration = spec.get_slot_duration(); + let payload_attestation_due = spec.get_payload_attestation_due(); + + info!( + payload_attestation_due_ms = payload_attestation_due.as_millis(), + "Payload attestation service started" + ); + + let executor = self.executor.clone(); + + let interval_fut = async move { + loop { + let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() else { + error!("Failed to read slot clock"); + sleep(slot_duration).await; + continue; + }; + + sleep(duration_to_next_slot + payload_attestation_due).await; + + let Some(current_slot) = self.slot_clock.now() else { + error!("Failed to read slot clock after trigger"); + continue; + }; + + let duties = self.duties_service.get_ptc_duties_for_slot(current_slot); + if duties.is_empty() { + continue; + } + + debug!( + %current_slot, + duty_count = duties.len(), + "Producing payload attestations" + ); + + let service = self.clone(); + self.executor.spawn( + async move { + service.produce_and_publish(current_slot).await; + }, + "payload_attestation_producer", + ); + } + }; + + executor.spawn(interval_fut, "payload_attestation_service"); + Ok(()) + } + + async fn produce_and_publish(&self, slot: types::Slot) { + let duties = self.duties_service.get_ptc_duties_for_slot(slot); + if duties.is_empty() { + return; + } + + let attestation_data = match self + .beacon_nodes + .first_success(|beacon_node| async move { + beacon_node + .get_validator_payload_attestation_data(slot) + .await + .map_err(|e| format!("Failed to get payload attestation data: {e:?}")) + .map(|resp| resp.into_data()) + }) + .await + { + Ok(data) => data, + Err(e) => { + crit!( + error = %e, + %slot, + "Failed to produce payload attestation data" + ); + return; + } + }; + + debug!( + %slot, + beacon_block_root = ?attestation_data.beacon_block_root, + payload_present = attestation_data.payload_present, + "Received payload attestation data" + ); + + let mut messages = Vec::with_capacity(duties.len()); + + for duty in &duties { + match self + .validator_store + .sign_payload_attestation(duty.pubkey, attestation_data.clone()) + .await + { + Ok(message) => { + messages.push(message); + } + Err(e) => { + crit!( + error = ?e, + validator = ?duty.pubkey, + %slot, + "Failed to sign payload attestation" + ); + } + } + } + + if messages.is_empty() { + return; + } + + let count = messages.len(); + match self + .beacon_nodes + .first_success(|beacon_node| { + let messages = messages.clone(); + async move { + beacon_node + .post_beacon_pool_payload_attestations(&messages) + .await + .map_err(|e| format!("Failed to publish payload attestations: {e:?}")) + } + }) + .await + { + Ok(()) => { + info!( + %slot, + %count, + "Successfully published payload attestations" + ); + } + Err(e) => { + crit!( + error = %e, + %slot, + "Failed to publish payload attestations" + ); + } + } + } +} diff --git a/validator_client/validator_store/src/lib.rs b/validator_client/validator_store/src/lib.rs index da0b33de18..4e5b415a41 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -7,10 +7,11 @@ use std::future::Future; use std::sync::Arc; use types::{ Address, Attestation, AttestationError, BlindedBeaconBlock, Epoch, EthSpec, - ExecutionPayloadEnvelope, Graffiti, Hash256, SelectionProof, SignedAggregateAndProof, - SignedBlindedBeaconBlock, SignedContributionAndProof, SignedExecutionPayloadEnvelope, - SignedValidatorRegistrationData, Slot, SyncCommitteeContribution, SyncCommitteeMessage, - SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, + ExecutionPayloadEnvelope, Graffiti, Hash256, PayloadAttestationData, PayloadAttestationMessage, + SelectionProof, SignedAggregateAndProof, SignedBlindedBeaconBlock, SignedContributionAndProof, + SignedExecutionPayloadEnvelope, SignedValidatorRegistrationData, Slot, + SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, + ValidatorRegistrationData, }; #[derive(Debug, PartialEq, Clone)] @@ -205,6 +206,13 @@ pub trait ValidatorStore: Send + Sync { envelope: ExecutionPayloadEnvelope, ) -> impl Future, Error>> + Send; + /// Sign a `PayloadAttestationData` for the PTC. + fn sign_payload_attestation( + &self, + validator_pubkey: PublicKeyBytes, + data: PayloadAttestationData, + ) -> 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`. From 0a1bdf840b571d8c7d118b450e2ea497edf60e18 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 27 Apr 2026 08:48:06 +0200 Subject: [PATCH 27/34] Add payload attestation validator duty --- beacon_node/http_api/src/beacon/pool.rs | 36 ++- beacon_node/http_api/src/lib.rs | 5 + common/eth2/src/lib.rs | 20 +- .../lighthouse_validator_store/src/lib.rs | 44 ++- validator_client/signing_method/src/lib.rs | 5 + .../signing_method/src/web3signer.rs | 3 + validator_client/src/lib.rs | 17 ++ .../validator_services/src/lib.rs | 1 + .../src/payload_attestation_service.rs | 262 ++++++++++++++++++ validator_client/validator_store/src/lib.rs | 16 +- 10 files changed, 396 insertions(+), 13 deletions(-) create mode 100644 validator_client/validator_services/src/payload_attestation_service.rs diff --git a/beacon_node/http_api/src/beacon/pool.rs b/beacon_node/http_api/src/beacon/pool.rs index 059573c317..8c39814d35 100644 --- a/beacon_node/http_api/src/beacon/pool.rs +++ b/beacon_node/http_api/src/beacon/pool.rs @@ -17,8 +17,9 @@ use std::sync::Arc; use tokio::sync::mpsc::UnboundedSender; use tracing::{debug, info, warn}; use types::{ - Attestation, AttestationData, AttesterSlashing, ForkName, ProposerSlashing, - SignedBlsToExecutionChange, SignedVoluntaryExit, SingleAttestation, SyncCommitteeMessage, + Attestation, AttestationData, AttesterSlashing, ForkName, PayloadAttestationMessage, + ProposerSlashing, SignedBlsToExecutionChange, SignedVoluntaryExit, SingleAttestation, + SyncCommitteeMessage, }; use warp::filters::BoxedFilter; use warp::{Filter, Reply}; @@ -520,3 +521,34 @@ pub fn post_beacon_pool_attestations_v2( ) .boxed() } + +/// POST beacon/pool/payload_attestations +pub fn post_beacon_pool_payload_attestations( + network_tx_filter: &NetworkTxFilter, + beacon_pool_path: &BeaconPoolPathFilter, +) -> ResponseFilter { + beacon_pool_path + .clone() + .and(warp::path("payload_attestations")) + .and(warp::path::end()) + .and(warp_utils::json::json()) + .and(network_tx_filter.clone()) + .then( + |task_spawner: TaskSpawner, + _chain: Arc>, + messages: Vec, + network_tx: UnboundedSender>| { + task_spawner.blocking_json_task(Priority::P0, move || { + // TODO(gloas): add proper verification once payload_attestation_verification is implemented + for message in messages { + utils::publish_pubsub_message( + &network_tx, + PubsubMessage::PayloadAttestation(Box::new(message)), + )?; + } + Ok(()) + }) + }, + ) + .boxed() +} diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index bd80dd1e82..eafb978b38 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1487,6 +1487,10 @@ pub fn serve( let post_beacon_pool_sync_committees = post_beacon_pool_sync_committees(&network_tx_filter, &beacon_pool_path); + // POST beacon/pool/payload_attestations + let post_beacon_pool_payload_attestations = + post_beacon_pool_payload_attestations(&network_tx_filter, &beacon_pool_path); + // GET beacon/pool/bls_to_execution_changes let get_beacon_pool_bls_to_execution_changes = get_beacon_pool_bls_to_execution_changes(&beacon_pool_path); @@ -3411,6 +3415,7 @@ pub fn serve( .uor(post_beacon_pool_proposer_slashings) .uor(post_beacon_pool_voluntary_exits) .uor(post_beacon_pool_sync_committees) + .uor(post_beacon_pool_payload_attestations) .uor(post_beacon_pool_bls_to_execution_changes) .uor(post_beacon_execution_payload_envelope) .uor(post_beacon_state_validators) diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 4ec75468a2..25e3a8a3f4 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -46,7 +46,7 @@ use ssz::{Decode, Encode}; use std::fmt; use std::future::Future; use std::time::Duration; -use types::PayloadAttestationData; +use types::{PayloadAttestationData, PayloadAttestationMessage}; pub const V1: EndpointVersion = EndpointVersion(1); pub const V2: EndpointVersion = EndpointVersion(2); @@ -1789,6 +1789,24 @@ impl BeaconNodeHttpClient { Ok(()) } + /// `POST beacon/pool/payload_attestations` + pub async fn post_beacon_pool_payload_attestations( + &self, + messages: &[PayloadAttestationMessage], + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("pool") + .push("payload_attestations"); + + self.post(path, &messages).await?; + + Ok(()) + } + /// `POST beacon/pool/bls_to_execution_changes` pub async fn post_beacon_pool_bls_to_execution_changes( &self, diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index c5bcd88eb1..a3ab2ccbe4 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -22,10 +22,11 @@ use types::{ AbstractExecPayload, Address, AggregateAndProof, Attestation, BeaconBlock, BlindedPayload, ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, ExecutionPayloadEnvelope, Fork, FullPayload, Graffiti, Hash256, SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, - SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedRoot, - SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncAggregatorSelectionData, - SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, - ValidatorRegistrationData, VoluntaryExit, graffiti::GraffitiString, + PayloadAttestationData, PayloadAttestationMessage, SignedContributionAndProof, + SignedExecutionPayloadEnvelope, SignedRoot, SignedValidatorRegistrationData, + SignedVoluntaryExit, Slot, SyncAggregatorSelectionData, SyncCommitteeContribution, + SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, + VoluntaryExit, graffiti::GraffitiString, }; use validator_store::{ AggregateToSign, AttestationToSign, ContributionToSign, DoppelgangerStatus, @@ -1423,8 +1424,39 @@ impl ValidatorStore for LighthouseValidatorS }) } - /// Sign an `ExecutionPayloadEnvelope` for Gloas (local building). - /// The proposer acts as the builder and signs with the BeaconBuilder domain. + async fn sign_payload_attestation( + &self, + validator_pubkey: PublicKeyBytes, + data: PayloadAttestationData, + ) -> Result { + let signing_context = self.signing_context( + Domain::PTCAttester, + data.slot.epoch(E::slots_per_epoch()), + ); + + let validator_index = self + .validator_index(&validator_pubkey) + .ok_or(ValidatorStoreError::UnknownPubkey(validator_pubkey))?; + + let signing_method = self.doppelganger_bypassed_signing_method(validator_pubkey)?; + + let signature = signing_method + .get_signature::>( + SignableMessage::PayloadAttestationData(&data), + signing_context, + &self.spec, + &self.task_executor, + ) + .await + .map_err(Error::SpecificError)?; + + Ok(PayloadAttestationMessage { + validator_index, + data, + signature, + }) + } + async fn sign_execution_payload_envelope( &self, validator_pubkey: PublicKeyBytes, diff --git a/validator_client/signing_method/src/lib.rs b/validator_client/signing_method/src/lib.rs index c132d86c17..2f80fa5761 100644 --- a/validator_client/signing_method/src/lib.rs +++ b/validator_client/signing_method/src/lib.rs @@ -50,6 +50,7 @@ pub enum SignableMessage<'a, E: EthSpec, Payload: AbstractExecPayload = FullP ValidatorRegistration(&'a ValidatorRegistrationData), VoluntaryExit(&'a VoluntaryExit), ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope), + PayloadAttestationData(&'a PayloadAttestationData), } impl> SignableMessage<'_, E, Payload> { @@ -72,6 +73,7 @@ impl> SignableMessage<'_, E, Payload SignableMessage::ValidatorRegistration(v) => v.signing_root(domain), SignableMessage::VoluntaryExit(exit) => exit.signing_root(domain), SignableMessage::ExecutionPayloadEnvelope(e) => e.signing_root(domain), + SignableMessage::PayloadAttestationData(d) => d.signing_root(domain), } } } @@ -238,6 +240,9 @@ impl SigningMethod { SignableMessage::ExecutionPayloadEnvelope(e) => { Web3SignerObject::ExecutionPayloadEnvelope(e) } + SignableMessage::PayloadAttestationData(d) => { + Web3SignerObject::PayloadAttestationData(d) + } }; // Determine the Web3Signer message type. diff --git a/validator_client/signing_method/src/web3signer.rs b/validator_client/signing_method/src/web3signer.rs index e6fc8f3ba2..c2b7e06f92 100644 --- a/validator_client/signing_method/src/web3signer.rs +++ b/validator_client/signing_method/src/web3signer.rs @@ -21,6 +21,7 @@ pub enum MessageType { ValidatorRegistration, // TODO(gloas) verify w/ web3signer specs ExecutionPayloadEnvelope, + PayloadAttestation, } #[derive(Debug, PartialEq, Copy, Clone, Serialize)] @@ -78,6 +79,7 @@ pub enum Web3SignerObject<'a, E: EthSpec, Payload: AbstractExecPayload> { ContributionAndProof(&'a ContributionAndProof), ValidatorRegistration(&'a ValidatorRegistrationData), ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope), + PayloadAttestationData(&'a PayloadAttestationData), } impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Payload> { @@ -144,6 +146,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Pa } Web3SignerObject::ValidatorRegistration(_) => MessageType::ValidatorRegistration, Web3SignerObject::ExecutionPayloadEnvelope(_) => MessageType::ExecutionPayloadEnvelope, + Web3SignerObject::PayloadAttestationData(_) => MessageType::PayloadAttestation, } } } diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index e26d5c3d30..3fbb52fcff 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -45,6 +45,7 @@ use validator_services::{ block_service::{BlockService, BlockServiceBuilder}, duties_service::{self, DutiesService, DutiesServiceBuilder}, latency_service, + payload_attestation_service::{PayloadAttestationService, PayloadAttestationServiceBuilder}, preparation_service::{PreparationService, PreparationServiceBuilder}, sync_committee_service::SyncCommitteeService, }; @@ -83,6 +84,7 @@ pub struct ProductionValidatorClient { block_service: BlockService, SystemTimeSlotClock>, attestation_service: AttestationService, SystemTimeSlotClock>, sync_committee_service: SyncCommitteeService, SystemTimeSlotClock>, + payload_attestation_service: PayloadAttestationService, SystemTimeSlotClock>, doppelganger_service: Option>, preparation_service: PreparationService, SystemTimeSlotClock>, validator_store: Arc>, @@ -552,12 +554,22 @@ impl ProductionValidatorClient { context.executor.clone(), ); + let payload_attestation_service = PayloadAttestationServiceBuilder::new() + .duties_service(duties_service.clone()) + .validator_store(validator_store.clone()) + .slot_clock(slot_clock.clone()) + .beacon_nodes(beacon_nodes.clone()) + .executor(context.executor.clone()) + .chain_spec(context.eth2_config.spec.clone()) + .build()?; + Ok(Self { context, duties_service, block_service, attestation_service, sync_committee_service, + payload_attestation_service, doppelganger_service, preparation_service, validator_store, @@ -629,6 +641,11 @@ impl ProductionValidatorClient { .start_update_service(&self.context.eth2_config.spec) .map_err(|e| format!("Unable to start sync committee service: {}", e))?; + self.payload_attestation_service + .clone() + .start_update_service(&self.context.eth2_config.spec) + .map_err(|e| format!("Unable to start payload attestation service: {}", e))?; + self.preparation_service .clone() .start_update_service(&self.context.eth2_config.spec) diff --git a/validator_client/validator_services/src/lib.rs b/validator_client/validator_services/src/lib.rs index 3b8bd9ae14..0169335a7f 100644 --- a/validator_client/validator_services/src/lib.rs +++ b/validator_client/validator_services/src/lib.rs @@ -3,6 +3,7 @@ pub mod block_service; pub mod duties_service; pub mod latency_service; pub mod notifier_service; +pub mod payload_attestation_service; pub mod preparation_service; pub mod sync; pub mod sync_committee_service; diff --git a/validator_client/validator_services/src/payload_attestation_service.rs b/validator_client/validator_services/src/payload_attestation_service.rs new file mode 100644 index 0000000000..2ae089c762 --- /dev/null +++ b/validator_client/validator_services/src/payload_attestation_service.rs @@ -0,0 +1,262 @@ +use crate::duties_service::DutiesService; +use beacon_node_fallback::BeaconNodeFallback; +use logging::crit; +use slot_clock::SlotClock; +use std::ops::Deref; +use std::sync::Arc; +use task_executor::TaskExecutor; +use tokio::time::{Duration, sleep}; +use tracing::{debug, error, info, warn}; +use types::{ChainSpec, EthSpec}; +use validator_store::ValidatorStore; + +pub struct PayloadAttestationServiceBuilder { + duties_service: Option>>, + validator_store: Option>, + slot_clock: Option, + beacon_nodes: Option>>, + executor: Option, + chain_spec: Option>, +} + +impl PayloadAttestationServiceBuilder { + pub fn new() -> Self { + Self { + duties_service: None, + validator_store: None, + slot_clock: None, + beacon_nodes: None, + executor: None, + chain_spec: None, + } + } + + pub fn duties_service(mut self, service: Arc>) -> Self { + self.duties_service = Some(service); + self + } + + pub fn validator_store(mut self, store: Arc) -> Self { + self.validator_store = Some(store); + self + } + + pub fn slot_clock(mut self, slot_clock: T) -> Self { + self.slot_clock = Some(slot_clock); + self + } + + pub fn beacon_nodes(mut self, beacon_nodes: Arc>) -> Self { + self.beacon_nodes = Some(beacon_nodes); + self + } + + pub fn executor(mut self, executor: TaskExecutor) -> Self { + self.executor = Some(executor); + self + } + + pub fn chain_spec(mut self, chain_spec: Arc) -> Self { + self.chain_spec = Some(chain_spec); + self + } + + pub fn build(self) -> Result, String> { + Ok(PayloadAttestationService { + inner: Arc::new(Inner { + duties_service: self + .duties_service + .ok_or("Cannot build PayloadAttestationService without duties_service")?, + validator_store: self + .validator_store + .ok_or("Cannot build PayloadAttestationService without validator_store")?, + slot_clock: self + .slot_clock + .ok_or("Cannot build PayloadAttestationService without slot_clock")?, + beacon_nodes: self + .beacon_nodes + .ok_or("Cannot build PayloadAttestationService without beacon_nodes")?, + executor: self + .executor + .ok_or("Cannot build PayloadAttestationService without executor")?, + chain_spec: self + .chain_spec + .ok_or("Cannot build PayloadAttestationService without chain_spec")?, + }), + }) + } +} + +pub struct Inner { + duties_service: Arc>, + validator_store: Arc, + slot_clock: T, + beacon_nodes: Arc>, + executor: TaskExecutor, + chain_spec: Arc, +} + +pub struct PayloadAttestationService { + inner: Arc>, +} + +impl Clone for PayloadAttestationService { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl Deref for PayloadAttestationService { + type Target = Inner; + + fn deref(&self) -> &Self::Target { + self.inner.deref() + } +} + +impl PayloadAttestationService { + pub fn start_update_service(self, spec: &ChainSpec) -> Result<(), String> { + let slot_duration = spec.get_slot_duration(); + let payload_attestation_due = spec.get_payload_attestation_due(); + + info!( + payload_attestation_due_ms = payload_attestation_due.as_millis(), + "Payload attestation service started" + ); + + let executor = self.executor.clone(); + + let interval_fut = async move { + loop { + let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() else { + error!("Failed to read slot clock"); + sleep(slot_duration).await; + continue; + }; + + sleep(duration_to_next_slot + payload_attestation_due).await; + + let Some(current_slot) = self.slot_clock.now() else { + error!("Failed to read slot clock after trigger"); + continue; + }; + + let duties = self.duties_service.get_ptc_duties_for_slot(current_slot); + if duties.is_empty() { + continue; + } + + debug!( + %current_slot, + duty_count = duties.len(), + "Producing payload attestations" + ); + + let service = self.clone(); + self.executor.spawn( + async move { + service.produce_and_publish(current_slot).await; + }, + "payload_attestation_producer", + ); + } + }; + + executor.spawn(interval_fut, "payload_attestation_service"); + Ok(()) + } + + async fn produce_and_publish(&self, slot: types::Slot) { + let duties = self.duties_service.get_ptc_duties_for_slot(slot); + if duties.is_empty() { + return; + } + + let attestation_data = match self + .beacon_nodes + .first_success(|beacon_node| async move { + beacon_node + .get_validator_payload_attestation_data(slot) + .await + .map_err(|e| format!("Failed to get payload attestation data: {e:?}")) + .map(|resp| resp.into_data()) + }) + .await + { + Ok(data) => data, + Err(e) => { + crit!( + error = %e, + %slot, + "Failed to produce payload attestation data" + ); + return; + } + }; + + debug!( + %slot, + beacon_block_root = ?attestation_data.beacon_block_root, + payload_present = attestation_data.payload_present, + "Received payload attestation data" + ); + + let mut messages = Vec::with_capacity(duties.len()); + + for duty in &duties { + match self + .validator_store + .sign_payload_attestation(duty.pubkey, attestation_data.clone()) + .await + { + Ok(message) => { + messages.push(message); + } + Err(e) => { + crit!( + error = ?e, + validator = ?duty.pubkey, + %slot, + "Failed to sign payload attestation" + ); + } + } + } + + if messages.is_empty() { + return; + } + + let count = messages.len(); + match self + .beacon_nodes + .first_success(|beacon_node| { + let messages = messages.clone(); + async move { + beacon_node + .post_beacon_pool_payload_attestations(&messages) + .await + .map_err(|e| format!("Failed to publish payload attestations: {e:?}")) + } + }) + .await + { + Ok(()) => { + info!( + %slot, + %count, + "Successfully published payload attestations" + ); + } + Err(e) => { + crit!( + error = %e, + %slot, + "Failed to publish payload attestations" + ); + } + } + } +} diff --git a/validator_client/validator_store/src/lib.rs b/validator_client/validator_store/src/lib.rs index da0b33de18..4e5b415a41 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -7,10 +7,11 @@ use std::future::Future; use std::sync::Arc; use types::{ Address, Attestation, AttestationError, BlindedBeaconBlock, Epoch, EthSpec, - ExecutionPayloadEnvelope, Graffiti, Hash256, SelectionProof, SignedAggregateAndProof, - SignedBlindedBeaconBlock, SignedContributionAndProof, SignedExecutionPayloadEnvelope, - SignedValidatorRegistrationData, Slot, SyncCommitteeContribution, SyncCommitteeMessage, - SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, + ExecutionPayloadEnvelope, Graffiti, Hash256, PayloadAttestationData, PayloadAttestationMessage, + SelectionProof, SignedAggregateAndProof, SignedBlindedBeaconBlock, SignedContributionAndProof, + SignedExecutionPayloadEnvelope, SignedValidatorRegistrationData, Slot, + SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, + ValidatorRegistrationData, }; #[derive(Debug, PartialEq, Clone)] @@ -205,6 +206,13 @@ pub trait ValidatorStore: Send + Sync { envelope: ExecutionPayloadEnvelope, ) -> impl Future, Error>> + Send; + /// Sign a `PayloadAttestationData` for the PTC. + fn sign_payload_attestation( + &self, + validator_pubkey: PublicKeyBytes, + data: PayloadAttestationData, + ) -> 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`. From d42784229a4cdbdd96a1e5e2a40a731c0e369f1e Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 27 Apr 2026 09:09:17 +0200 Subject: [PATCH 28/34] fmt --- validator_client/lighthouse_validator_store/src/lib.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index a3ab2ccbe4..dec65af242 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -21,8 +21,8 @@ use tracing::{Instrument, debug, error, info, info_span, instrument, warn}; use types::{ AbstractExecPayload, Address, AggregateAndProof, Attestation, BeaconBlock, BlindedPayload, ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, ExecutionPayloadEnvelope, Fork, - FullPayload, Graffiti, Hash256, SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, - PayloadAttestationData, PayloadAttestationMessage, SignedContributionAndProof, + FullPayload, Graffiti, Hash256, PayloadAttestationData, PayloadAttestationMessage, + SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedRoot, SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncAggregatorSelectionData, SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, @@ -1429,10 +1429,8 @@ impl ValidatorStore for LighthouseValidatorS validator_pubkey: PublicKeyBytes, data: PayloadAttestationData, ) -> Result { - let signing_context = self.signing_context( - Domain::PTCAttester, - data.slot.epoch(E::slots_per_epoch()), - ); + let signing_context = + self.signing_context(Domain::PTCAttester, data.slot.epoch(E::slots_per_epoch())); let validator_index = self .validator_index(&validator_pubkey) From 68bfb334303a598e76dd05fcfab90e3f13a80316 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 27 Apr 2026 09:46:09 +0200 Subject: [PATCH 29/34] Add tests and ssz suppport --- beacon_node/http_api/src/beacon/pool.rs | 83 ++++++++++++++++--- beacon_node/http_api/src/lib.rs | 20 ++++- beacon_node/http_api/tests/tests.rs | 69 +++++++++++++++ common/eth2/src/lib.rs | 31 ++++++- .../lighthouse_validator_store/src/lib.rs | 2 + .../src/payload_attestation_service.rs | 31 +++++-- 6 files changed, 215 insertions(+), 21 deletions(-) diff --git a/beacon_node/http_api/src/beacon/pool.rs b/beacon_node/http_api/src/beacon/pool.rs index 8c39814d35..56cc7c493c 100644 --- a/beacon_node/http_api/src/beacon/pool.rs +++ b/beacon_node/http_api/src/beacon/pool.rs @@ -1,5 +1,8 @@ use crate::task_spawner::{Priority, TaskSpawner}; -use crate::utils::{NetworkTxFilter, OptionalConsensusVersionHeaderFilter, ResponseFilter}; +use crate::utils::{ + ChainFilter, EthV1Filter, NetworkTxFilter, OptionalConsensusVersionHeaderFilter, + ResponseFilter, TaskSpawnerFilter, +}; use crate::version::{ ResponseIncludesVersion, V1, V2, add_consensus_version_header, beacon_response, unsupported_version_rejection, @@ -7,11 +10,13 @@ use crate::version::{ use crate::{sync_committees, utils}; use beacon_chain::observed_operations::ObservationOutcome; use beacon_chain::{BeaconChain, BeaconChainTypes}; +use bytes::Bytes; use eth2::types::{AttestationPoolQuery, EndpointVersion, Failure, GenericResponse}; use lighthouse_network::PubsubMessage; use network::NetworkMessage; use operation_pool::ReceivedPreCapella; use slot_clock::SlotClock; +use ssz::{Decode, Encode}; use std::collections::HashSet; use std::sync::Arc; use tokio::sync::mpsc::UnboundedSender; @@ -522,9 +527,10 @@ pub fn post_beacon_pool_attestations_v2( .boxed() } -/// POST beacon/pool/payload_attestations +/// POST beacon/pool/payload_attestations (JSON) pub fn post_beacon_pool_payload_attestations( network_tx_filter: &NetworkTxFilter, + optional_consensus_version_header_filter: OptionalConsensusVersionHeaderFilter, beacon_pool_path: &BeaconPoolPathFilter, ) -> ResponseFilter { beacon_pool_path @@ -532,23 +538,80 @@ pub fn post_beacon_pool_payload_attestations( .and(warp::path("payload_attestations")) .and(warp::path::end()) .and(warp_utils::json::json()) + .and(optional_consensus_version_header_filter) .and(network_tx_filter.clone()) .then( |task_spawner: TaskSpawner, _chain: Arc>, messages: Vec, + _fork_name: Option, network_tx: UnboundedSender>| { task_spawner.blocking_json_task(Priority::P0, move || { - // TODO(gloas): add proper verification once payload_attestation_verification is implemented - for message in messages { - utils::publish_pubsub_message( - &network_tx, - PubsubMessage::PayloadAttestation(Box::new(message)), - )?; - } - Ok(()) + publish_payload_attestation_messages(&network_tx, messages) }) }, ) .boxed() } + +/// POST beacon/pool/payload_attestations (SSZ) +pub fn post_beacon_pool_payload_attestations_ssz( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter, + chain_filter: ChainFilter, + network_tx_filter: NetworkTxFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("beacon")) + .and(warp::path("pool")) + .and(warp::path("payload_attestations")) + .and(warp::path::end()) + .and(warp::body::bytes()) + .and(task_spawner_filter) + .and(chain_filter) + .and(network_tx_filter) + .then( + |body_bytes: Bytes, + task_spawner: TaskSpawner, + _chain: Arc>, + network_tx: UnboundedSender>| { + task_spawner.blocking_json_task(Priority::P0, move || { + let item_len = ::ssz_fixed_len(); + if body_bytes.len() % item_len != 0 { + return Err(warp_utils::reject::custom_bad_request(format!( + "SSZ body length {} is not a multiple of PayloadAttestationMessage size {}", + body_bytes.len(), + item_len, + ))); + } + let messages: Vec = body_bytes + .chunks(item_len) + .map(|chunk| { + PayloadAttestationMessage::from_ssz_bytes(chunk).map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "invalid SSZ: {e:?}" + )) + }) + }) + .collect::>()?; + + publish_payload_attestation_messages(&network_tx, messages) + }) + }, + ) + .boxed() +} + +fn publish_payload_attestation_messages( + network_tx: &UnboundedSender>, + messages: Vec, +) -> Result<(), warp::Rejection> { + // TODO(gloas): add proper gossip verification and store in ptc op pool. + for message in messages { + utils::publish_pubsub_message( + network_tx, + PubsubMessage::PayloadAttestation(Box::new(message)), + )?; + } + Ok(()) +} diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index eafb978b38..b2d069f384 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1454,7 +1454,7 @@ pub fn serve( let post_beacon_pool_attestations_v2 = post_beacon_pool_attestations_v2( &network_tx_filter, - optional_consensus_version_header_filter, + optional_consensus_version_header_filter.clone(), &beacon_pool_path_v2, ); @@ -1488,8 +1488,19 @@ pub fn serve( post_beacon_pool_sync_committees(&network_tx_filter, &beacon_pool_path); // POST beacon/pool/payload_attestations - let post_beacon_pool_payload_attestations = - post_beacon_pool_payload_attestations(&network_tx_filter, &beacon_pool_path); + let post_beacon_pool_payload_attestations = post_beacon_pool_payload_attestations( + &network_tx_filter, + optional_consensus_version_header_filter, + &beacon_pool_path, + ); + + // POST beacon/pool/payload_attestations (SSZ) + let post_beacon_pool_payload_attestations_ssz = post_beacon_pool_payload_attestations_ssz( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); // GET beacon/pool/bls_to_execution_changes let get_beacon_pool_bls_to_execution_changes = @@ -3404,7 +3415,8 @@ pub fn serve( .uor(post_beacon_blocks_v2_ssz) .uor(post_beacon_blinded_blocks_ssz) .uor(post_beacon_blinded_blocks_v2_ssz) - .uor(post_beacon_execution_payload_envelope_ssz), + .uor(post_beacon_execution_payload_envelope_ssz) + .uor(post_beacon_pool_payload_attestations_ssz), ) .uor(post_beacon_blocks) .uor(post_beacon_blinded_blocks) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index aac3384fbd..1db9fc4e4f 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -2793,6 +2793,62 @@ impl ApiTester { self } + pub async fn test_post_beacon_pool_payload_attestations_valid(mut self) -> Self { + let slot = self.chain.slot().unwrap(); + let head_root = self.chain.head_beacon_block_root(); + + let message = PayloadAttestationMessage { + validator_index: 0, + data: PayloadAttestationData { + beacon_block_root: head_root, + slot, + payload_present: true, + blob_data_available: true, + }, + signature: Signature::empty(), + }; + + self.client + .post_beacon_pool_payload_attestations(&[message]) + .await + .unwrap(); + + assert!( + self.network_rx.network_recv.recv().await.is_some(), + "valid payload attestation should be sent to network" + ); + + self + } + + pub async fn test_post_beacon_pool_payload_attestations_valid_ssz(mut self) -> Self { + let slot = self.chain.slot().unwrap(); + let head_root = self.chain.head_beacon_block_root(); + + let message = PayloadAttestationMessage { + validator_index: 0, + data: PayloadAttestationData { + beacon_block_root: head_root, + slot, + payload_present: true, + blob_data_available: true, + }, + signature: Signature::empty(), + }; + + self.client + .post_beacon_pool_payload_attestations_ssz(&[message]) + .await + .unwrap(); + + assert!( + self.network_rx.network_recv.recv().await.is_some(), + "valid payload attestation (SSZ) should be sent to network" + ); + + self + } + pub async fn test_get_config_fork_schedule(self) -> Self { let result = self.client.get_config_fork_schedule().await.unwrap().data; @@ -8246,6 +8302,19 @@ async fn get_validator_payload_attestation_data_pre_gloas() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn post_beacon_pool_payload_attestations_valid() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + ApiTester::new() + .await + .test_post_beacon_pool_payload_attestations_valid() + .await + .test_post_beacon_pool_payload_attestations_valid_ssz() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_validator_aggregate_attestation_v1() { ApiTester::new() diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 25e3a8a3f4..bc02db1ad3 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -1789,7 +1789,7 @@ impl BeaconNodeHttpClient { Ok(()) } - /// `POST beacon/pool/payload_attestations` + /// `POST beacon/pool/payload_attestations` (JSON) pub async fn post_beacon_pool_payload_attestations( &self, messages: &[PayloadAttestationMessage], @@ -1802,7 +1802,34 @@ impl BeaconNodeHttpClient { .push("pool") .push("payload_attestations"); - self.post(path, &messages).await?; + self.post_generic_with_consensus_version(path, &messages, None, ForkName::Gloas) + .await?; + + Ok(()) + } + + /// `POST beacon/pool/payload_attestations` (SSZ) + pub async fn post_beacon_pool_payload_attestations_ssz( + &self, + messages: &[PayloadAttestationMessage], + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("pool") + .push("payload_attestations"); + + let ssz_body: Vec = messages.iter().flat_map(|m| m.as_ssz_bytes()).collect(); + + self.post_generic_with_consensus_version_and_ssz_body( + path, + ssz_body, + None, + ForkName::Gloas, + ) + .await?; Ok(()) } diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index dec65af242..1b32777678 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -1455,6 +1455,8 @@ impl ValidatorStore for LighthouseValidatorS }) } + /// Sign an `ExecutionPayloadEnvelope` for Gloas (local building). + /// The proposer acts as the builder and signs with the BeaconBuilder domain. async fn sign_execution_payload_envelope( &self, validator_pubkey: PublicKeyBytes, diff --git a/validator_client/validator_services/src/payload_attestation_service.rs b/validator_client/validator_services/src/payload_attestation_service.rs index 2ae089c762..420bac94e4 100644 --- a/validator_client/validator_services/src/payload_attestation_service.rs +++ b/validator_client/validator_services/src/payload_attestation_service.rs @@ -230,19 +230,40 @@ impl PayloadAttestationServ } let count = messages.len(); - match self + let result = self .beacon_nodes .first_success(|beacon_node| { let messages = messages.clone(); async move { beacon_node - .post_beacon_pool_payload_attestations(&messages) + .post_beacon_pool_payload_attestations_ssz(&messages) .await - .map_err(|e| format!("Failed to publish payload attestations: {e:?}")) + .map_err(|e| format!("Failed to publish payload attestations (SSZ): {e:?}")) } }) - .await - { + .await; + + let result = match result { + Ok(()) => Ok(()), + Err(_) => { + debug!(%slot, "SSZ publish failed, falling back to JSON"); + self.beacon_nodes + .first_success(|beacon_node| { + let messages = messages.clone(); + async move { + beacon_node + .post_beacon_pool_payload_attestations(&messages) + .await + .map_err(|e| { + format!("Failed to publish payload attestations (JSON): {e:?}") + }) + } + }) + .await + } + }; + + match result { Ok(()) => { info!( %slot, From 812dc31b159429f31583a97f3fe918217ca51bbe Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 27 Apr 2026 10:10:06 +0200 Subject: [PATCH 30/34] Fix --- beacon_node/http_api/src/beacon/pool.rs | 2 +- validator_client/src/lib.rs | 2 +- .../src/payload_attestation_service.rs | 26 +++++++++++++++---- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/beacon_node/http_api/src/beacon/pool.rs b/beacon_node/http_api/src/beacon/pool.rs index 56cc7c493c..ae9b50b279 100644 --- a/beacon_node/http_api/src/beacon/pool.rs +++ b/beacon_node/http_api/src/beacon/pool.rs @@ -577,7 +577,7 @@ pub fn post_beacon_pool_payload_attestations_ssz( network_tx: UnboundedSender>| { task_spawner.blocking_json_task(Priority::P0, move || { let item_len = ::ssz_fixed_len(); - if body_bytes.len() % item_len != 0 { + if !body_bytes.len().is_multiple_of(item_len) { return Err(warp_utils::reject::custom_bad_request(format!( "SSZ body length {} is not a multiple of PayloadAttestationMessage size {}", body_bytes.len(), diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 3fbb52fcff..d54a2f5ee8 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -643,7 +643,7 @@ impl ProductionValidatorClient { self.payload_attestation_service .clone() - .start_update_service(&self.context.eth2_config.spec) + .start_update_service() .map_err(|e| format!("Unable to start payload attestation service: {}", e))?; self.preparation_service diff --git a/validator_client/validator_services/src/payload_attestation_service.rs b/validator_client/validator_services/src/payload_attestation_service.rs index 420bac94e4..2558e219d1 100644 --- a/validator_client/validator_services/src/payload_attestation_service.rs +++ b/validator_client/validator_services/src/payload_attestation_service.rs @@ -5,8 +5,8 @@ use slot_clock::SlotClock; use std::ops::Deref; use std::sync::Arc; use task_executor::TaskExecutor; -use tokio::time::{Duration, sleep}; -use tracing::{debug, error, info, warn}; +use tokio::time::sleep; +use tracing::{debug, error, info}; use types::{ChainSpec, EthSpec}; use validator_store::ValidatorStore; @@ -19,6 +19,14 @@ pub struct PayloadAttestationServiceBuilder>, } +impl Default + for PayloadAttestationServiceBuilder +{ + fn default() -> Self { + Self::new() + } +} + impl PayloadAttestationServiceBuilder { pub fn new() -> Self { Self { @@ -117,9 +125,9 @@ impl Deref for PayloadAttestationService { } impl PayloadAttestationService { - pub fn start_update_service(self, spec: &ChainSpec) -> Result<(), String> { - let slot_duration = spec.get_slot_duration(); - let payload_attestation_due = spec.get_payload_attestation_due(); + pub fn start_update_service(self) -> Result<(), String> { + let slot_duration = self.chain_spec.get_slot_duration(); + let payload_attestation_due = self.chain_spec.get_payload_attestation_due(); info!( payload_attestation_due_ms = payload_attestation_due.as_millis(), @@ -143,6 +151,14 @@ impl PayloadAttestationServ continue; }; + if !self + .chain_spec + .fork_name_at_slot::(current_slot) + .gloas_enabled() + { + continue; + } + let duties = self.duties_service.get_ptc_duties_for_slot(current_slot); if duties.is_empty() { continue; From ea9664dc91729d8c3e30104e1d003b00601d3f44 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 27 Apr 2026 10:32:48 +0200 Subject: [PATCH 31/34] Drop read lock --- .../gossip_verified_payload_attestation.rs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs index c2ec8cfa4d..2d9fce812e 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs @@ -141,19 +141,21 @@ impl VerifiedPayloadAttestationMessage { signature: AggregateSignature::from(&payload_attestation_message.signature), }; - // [REJECT] The signature is valid with respect to the `validator_index`. - let pubkey_cache = ctx.validator_pubkey_cache.read(); - let signature_set = indexed_payload_attestation_signature_set( - state, - |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), - &indexed_payload_attestation.signature, - &indexed_payload_attestation, - ctx.spec, - ) - .map_err(|_| Error::UnknownValidatorIndex(validator_index))?; + { + // [REJECT] The signature is valid with respect to the `validator_index`. + let pubkey_cache = ctx.validator_pubkey_cache.read(); + let signature_set = indexed_payload_attestation_signature_set( + state, + |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), + &indexed_payload_attestation.signature, + &indexed_payload_attestation, + ctx.spec, + ) + .map_err(|_| Error::UnknownValidatorIndex(validator_index))?; - if !signature_set.verify() { - return Err(Error::InvalidSignature); + if !signature_set.verify() { + return Err(Error::InvalidSignature); + } } // Record that we have received a valid payload attestation message from this From 5c221e8a839c918be15decc11e63a16773dff746 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 27 Apr 2026 10:34:31 +0200 Subject: [PATCH 32/34] skip all --- .../network/src/network_beacon_processor/gossip_methods.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d06359117d..4083b1a3af 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4098,7 +4098,7 @@ impl NetworkBeaconProcessor { #[instrument( level = "trace", - skip(self, message_id, peer_id, payload_attestation_message), + skip_all, fields( peer_id = %peer_id, slot = %payload_attestation_message.data.slot, From 7d5a59cd071d379be82960dc1dca0249de3c01c5 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 27 Apr 2026 10:45:15 +0200 Subject: [PATCH 33/34] LOL --- .../validator_services/src/payload_attestation_service.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/validator_client/validator_services/src/payload_attestation_service.rs b/validator_client/validator_services/src/payload_attestation_service.rs index f4a8483e65..98b2936ddd 100644 --- a/validator_client/validator_services/src/payload_attestation_service.rs +++ b/validator_client/validator_services/src/payload_attestation_service.rs @@ -7,11 +7,7 @@ use std::sync::Arc; use task_executor::TaskExecutor; use tokio::time::sleep; use tracing::{debug, error, info}; -<<<<<<< HEAD use types::ChainSpec; -======= -use types::{ChainSpec, EthSpec}; ->>>>>>> gloas-ptc-validator-duty use validator_store::ValidatorStore; pub struct PayloadAttestationServiceBuilder { From 9debb1a30b764dc3cf041b10e5ddba3092ba2ea6 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 27 Apr 2026 22:47:19 +0200 Subject: [PATCH 34/34] merge conflicts --- .../payload_attestation_verification/tests.rs | 10 -- beacon_node/http_api/src/beacon/pool.rs | 31 ---- beacon_node/http_api/tests/tests.rs | 39 ----- common/eth2/src/lib.rs | 20 --- validator_client/src/lib.rs | 23 --- .../src/payload_attestation_service.rs | 133 +----------------- 6 files changed, 1 insertion(+), 255 deletions(-) diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs index 2456ad70e4..7faad98e55 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs @@ -7,10 +7,7 @@ use genesis::{generate_deterministic_keypairs, interop_genesis_state}; use parking_lot::RwLock; use proto_array::PayloadStatus; use slot_clock::{SlotClock, TestingSlotClock}; -<<<<<<< HEAD -======= use state_processing::AllCaches; ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 use state_processing::genesis::genesis_block; use store::{HotColdDB, StoreConfig}; use types::{ @@ -29,11 +26,7 @@ use crate::{ GossipVerificationContext, VerifiedPayloadAttestationMessage, }, }, -<<<<<<< HEAD - test_utils::{EphemeralHarnessType, fork_name_from_env, test_spec}, -======= test_utils::{BeaconChainHarness, EphemeralHarnessType, fork_name_from_env, test_spec}, ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 validator_pubkey_cache::ValidatorPubkeyCache, }; @@ -334,8 +327,6 @@ fn duplicate_after_valid() { Err(PayloadAttestationError::PriorPayloadAttestationMessageKnown { .. }) )); } -<<<<<<< HEAD -======= /// Exercises the `partial_state_advance` fallback in gossip verification when /// the head state is too stale to compute PTC membership (e.g., during a @@ -429,4 +420,3 @@ async fn stale_head_with_partial_advance() { result.unwrap_err() ); } ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 diff --git a/beacon_node/http_api/src/beacon/pool.rs b/beacon_node/http_api/src/beacon/pool.rs index ac998f0b8b..c6b8a69643 100644 --- a/beacon_node/http_api/src/beacon/pool.rs +++ b/beacon_node/http_api/src/beacon/pool.rs @@ -543,20 +543,12 @@ pub fn post_beacon_pool_payload_attestations( .and(network_tx_filter.clone()) .then( |task_spawner: TaskSpawner, -<<<<<<< HEAD - _chain: Arc>, -======= chain: Arc>, ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 messages: Vec, _fork_name: Option, network_tx: UnboundedSender>| { task_spawner.blocking_json_task(Priority::P0, move || { -<<<<<<< HEAD - publish_payload_attestation_messages(&network_tx, messages) -======= publish_payload_attestation_messages(&chain, &network_tx, messages) ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 }) }, ) @@ -582,11 +574,7 @@ pub fn post_beacon_pool_payload_attestations_ssz( .then( |body_bytes: Bytes, task_spawner: TaskSpawner, -<<<<<<< HEAD - _chain: Arc>, -======= chain: Arc>, ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 network_tx: UnboundedSender>| { task_spawner.blocking_json_task(Priority::P0, move || { let item_len = ::ssz_fixed_len(); @@ -608,31 +596,13 @@ pub fn post_beacon_pool_payload_attestations_ssz( }) .collect::>()?; -<<<<<<< HEAD - publish_payload_attestation_messages(&network_tx, messages) -======= publish_payload_attestation_messages(&chain, &network_tx, messages) ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 }) }, ) .boxed() } -<<<<<<< HEAD -fn publish_payload_attestation_messages( - network_tx: &UnboundedSender>, - messages: Vec, -) -> Result<(), warp::Rejection> { - // TODO(gloas): add proper gossip verification and store in ptc op pool. - for message in messages { - utils::publish_pubsub_message( - network_tx, - PubsubMessage::PayloadAttestation(Box::new(message)), - )?; - } - Ok(()) -======= fn publish_payload_attestation_messages( chain: &BeaconChain, network_tx: &UnboundedSender>, @@ -690,5 +660,4 @@ fn publish_payload_attestation_messages( failures, )) } ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 } diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 0354d1e8ab..b8326f4495 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -2793,25 +2793,6 @@ impl ApiTester { self } -<<<<<<< HEAD - pub async fn test_post_beacon_pool_payload_attestations_valid(mut self) -> Self { - let slot = self.chain.slot().unwrap(); - let head_root = self.chain.head_beacon_block_root(); - - let message = PayloadAttestationMessage { - validator_index: 0, - data: PayloadAttestationData { - beacon_block_root: head_root, - slot, - payload_present: true, - blob_data_available: true, - }, - signature: Signature::empty(), - }; - - self.client - .post_beacon_pool_payload_attestations(&[message]) -======= fn make_valid_payload_attestation_message( &self, ptc_offset: usize, @@ -2867,7 +2848,6 @@ impl ApiTester { self.client .post_beacon_pool_payload_attestations(&[message], fork_name) ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 .await .unwrap(); @@ -2880,30 +2860,11 @@ impl ApiTester { } pub async fn test_post_beacon_pool_payload_attestations_valid_ssz(mut self) -> Self { -<<<<<<< HEAD - let slot = self.chain.slot().unwrap(); - let head_root = self.chain.head_beacon_block_root(); - - let message = PayloadAttestationMessage { - validator_index: 0, - data: PayloadAttestationData { - beacon_block_root: head_root, - slot, - payload_present: true, - blob_data_available: true, - }, - signature: Signature::empty(), - }; - - self.client - .post_beacon_pool_payload_attestations_ssz(&[message]) -======= let message = self.make_valid_payload_attestation_message(1); let fork_name = self.chain.spec.fork_name_at_slot::(message.data.slot); self.client .post_beacon_pool_payload_attestations_ssz(&[message], fork_name) ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 .await .unwrap(); diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 436d3fd996..e866547b9f 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -1793,10 +1793,7 @@ impl BeaconNodeHttpClient { pub async fn post_beacon_pool_payload_attestations( &self, messages: &[PayloadAttestationMessage], -<<<<<<< HEAD -======= fork_name: ForkName, ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 ) -> Result<(), Error> { let mut path = self.eth_path(V1)?; @@ -1806,11 +1803,7 @@ impl BeaconNodeHttpClient { .push("pool") .push("payload_attestations"); -<<<<<<< HEAD - self.post_generic_with_consensus_version(path, &messages, None, ForkName::Gloas) -======= self.post_generic_with_consensus_version(path, &messages, None, fork_name) ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 .await?; Ok(()) @@ -1820,10 +1813,7 @@ impl BeaconNodeHttpClient { pub async fn post_beacon_pool_payload_attestations_ssz( &self, messages: &[PayloadAttestationMessage], -<<<<<<< HEAD -======= fork_name: ForkName, ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 ) -> Result<(), Error> { let mut path = self.eth_path(V1)?; @@ -1835,18 +1825,8 @@ impl BeaconNodeHttpClient { let ssz_body: Vec = messages.iter().flat_map(|m| m.as_ssz_bytes()).collect(); -<<<<<<< HEAD - self.post_generic_with_consensus_version_and_ssz_body( - path, - ssz_body, - None, - ForkName::Gloas, - ) - .await?; -======= self.post_generic_with_consensus_version_and_ssz_body(path, ssz_body, None, fork_name) .await?; ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 Ok(()) } diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 369bf8fa16..b1b81bc0ea 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -45,11 +45,7 @@ use validator_services::{ block_service::{BlockService, BlockServiceBuilder}, duties_service::{self, DutiesService, DutiesServiceBuilder}, latency_service, -<<<<<<< HEAD - payload_attestation_service::{PayloadAttestationService, PayloadAttestationServiceBuilder}, -======= payload_attestation_service::PayloadAttestationService, ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 preparation_service::{PreparationService, PreparationServiceBuilder}, sync_committee_service::SyncCommitteeService, }; @@ -557,17 +553,6 @@ impl ProductionValidatorClient { beacon_nodes.clone(), context.executor.clone(), ); - -<<<<<<< HEAD - let payload_attestation_service = PayloadAttestationServiceBuilder::new() - .duties_service(duties_service.clone()) - .validator_store(validator_store.clone()) - .slot_clock(slot_clock.clone()) - .beacon_nodes(beacon_nodes.clone()) - .executor(context.executor.clone()) - .chain_spec(context.eth2_config.spec.clone()) - .build()?; -======= let payload_attestation_service = PayloadAttestationService::new( duties_service.clone(), validator_store.clone(), @@ -576,7 +561,6 @@ impl ProductionValidatorClient { context.executor.clone(), context.eth2_config.spec.clone(), ); ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 Ok(Self { context, @@ -656,19 +640,12 @@ impl ProductionValidatorClient { .start_update_service(&self.context.eth2_config.spec) .map_err(|e| format!("Unable to start sync committee service: {}", e))?; -<<<<<<< HEAD - self.payload_attestation_service - .clone() - .start_update_service() - .map_err(|e| format!("Unable to start payload attestation service: {}", e))?; -======= if self.context.eth2_config.spec.is_gloas_scheduled() { self.payload_attestation_service .clone() .start_update_service() .map_err(|e| format!("Unable to start payload attestation service: {}", e))?; } ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 self.preparation_service .clone() diff --git a/validator_client/validator_services/src/payload_attestation_service.rs b/validator_client/validator_services/src/payload_attestation_service.rs index 4211d563bb..b3b0c0ee9c 100644 --- a/validator_client/validator_services/src/payload_attestation_service.rs +++ b/validator_client/validator_services/src/payload_attestation_service.rs @@ -7,100 +7,9 @@ use std::sync::Arc; use task_executor::TaskExecutor; use tokio::time::sleep; use tracing::{debug, error, info}; -<<<<<<< HEAD -use types::ChainSpec; -use validator_store::ValidatorStore; - -pub struct PayloadAttestationServiceBuilder { - duties_service: Option>>, - validator_store: Option>, - slot_clock: Option, - beacon_nodes: Option>>, - executor: Option, - chain_spec: Option>, -} - -impl Default - for PayloadAttestationServiceBuilder -{ - fn default() -> Self { - Self::new() - } -} - -impl PayloadAttestationServiceBuilder { - pub fn new() -> Self { - Self { - duties_service: None, - validator_store: None, - slot_clock: None, - beacon_nodes: None, - executor: None, - chain_spec: None, - } - } - - pub fn duties_service(mut self, service: Arc>) -> Self { - self.duties_service = Some(service); - self - } - - pub fn validator_store(mut self, store: Arc) -> Self { - self.validator_store = Some(store); - self - } - - pub fn slot_clock(mut self, slot_clock: T) -> Self { - self.slot_clock = Some(slot_clock); - self - } - - pub fn beacon_nodes(mut self, beacon_nodes: Arc>) -> Self { - self.beacon_nodes = Some(beacon_nodes); - self - } - - pub fn executor(mut self, executor: TaskExecutor) -> Self { - self.executor = Some(executor); - self - } - - pub fn chain_spec(mut self, chain_spec: Arc) -> Self { - self.chain_spec = Some(chain_spec); - self - } - - pub fn build(self) -> Result, String> { - Ok(PayloadAttestationService { - inner: Arc::new(Inner { - duties_service: self - .duties_service - .ok_or("Cannot build PayloadAttestationService without duties_service")?, - validator_store: self - .validator_store - .ok_or("Cannot build PayloadAttestationService without validator_store")?, - slot_clock: self - .slot_clock - .ok_or("Cannot build PayloadAttestationService without slot_clock")?, - beacon_nodes: self - .beacon_nodes - .ok_or("Cannot build PayloadAttestationService without beacon_nodes")?, - executor: self - .executor - .ok_or("Cannot build PayloadAttestationService without executor")?, - chain_spec: self - .chain_spec - .ok_or("Cannot build PayloadAttestationService without chain_spec")?, - }), - }) - } -} - -======= use types::{ChainSpec, EthSpec}; use validator_store::ValidatorStore; ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 pub struct Inner { duties_service: Arc>, validator_store: Arc, @@ -131,8 +40,6 @@ impl Deref for PayloadAttestationService { } impl PayloadAttestationService { -<<<<<<< HEAD -======= pub fn new( duties_service: Arc>, validator_store: Arc, @@ -153,7 +60,6 @@ impl PayloadAttestationServ } } ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 pub fn start_update_service(self) -> Result<(), String> { let slot_duration = self.chain_spec.get_slot_duration(); let payload_attestation_due = self.chain_spec.get_payload_attestation_due(); @@ -173,11 +79,6 @@ impl PayloadAttestationServ continue; }; -<<<<<<< HEAD - sleep(duration_to_next_slot + payload_attestation_due).await; - -======= ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 let Some(current_slot) = self.slot_clock.now() else { error!("Failed to read slot clock after trigger"); continue; @@ -188,21 +89,6 @@ impl PayloadAttestationServ .fork_name_at_slot::(current_slot) .gloas_enabled() { -<<<<<<< HEAD - continue; - } - - let duties = self.duties_service.get_ptc_duties_for_slot(current_slot); - if duties.is_empty() { - continue; - } - - debug!( - %current_slot, - duty_count = duties.len(), - "Producing payload attestations" - ); -======= let duration_to_next_epoch = self .slot_clock .duration_to_next_epoch(S::E::slots_per_epoch()) @@ -214,7 +100,6 @@ impl PayloadAttestationServ } sleep(duration_to_next_slot + payload_attestation_due).await; ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 let service = self.clone(); self.executor.spawn( @@ -232,23 +117,17 @@ impl PayloadAttestationServ async fn produce_and_publish(&self, slot: types::Slot) { let duties = self.duties_service.get_ptc_duties_for_slot(slot); -<<<<<<< HEAD -======= ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 if duties.is_empty() { return; } -<<<<<<< HEAD -======= debug!( %slot, duty_count = duties.len(), "Producing payload attestations" ); ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 let attestation_data = match self .beacon_nodes .first_success(|beacon_node| async move { @@ -305,21 +184,15 @@ impl PayloadAttestationServ } let count = messages.len(); -<<<<<<< HEAD -======= + let fork_name = self.chain_spec.fork_name_at_slot::(slot); ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 let result = self .beacon_nodes .first_success(|beacon_node| { let messages = messages.clone(); async move { beacon_node -<<<<<<< HEAD - .post_beacon_pool_payload_attestations_ssz(&messages) -======= .post_beacon_pool_payload_attestations_ssz(&messages, fork_name) ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 .await .map_err(|e| format!("Failed to publish payload attestations (SSZ): {e:?}")) } @@ -335,11 +208,7 @@ impl PayloadAttestationServ let messages = messages.clone(); async move { beacon_node -<<<<<<< HEAD - .post_beacon_pool_payload_attestations(&messages) -======= .post_beacon_pool_payload_attestations(&messages, fork_name) ->>>>>>> 028b5a42a9715c31f416d45db70add39d9934b12 .await .map_err(|e| { format!("Failed to publish payload attestations (JSON): {e:?}")