From 242b7a0057fcb66ad57fd8cabfd2d86315ac58a4 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Sun, 17 Aug 2025 11:38:18 -0700 Subject: [PATCH 1/6] modify process_attestations per eip-7732 --- .../common/get_attestation_participation.rs | 27 +++++++- .../src/common/is_attestation_same_slot.rs | 21 ++++++ consensus/state_processing/src/common/mod.rs | 2 + .../src/per_block_processing/errors.rs | 6 +- .../process_operations.rs | 64 ++++++++++++++++++- .../verify_attestation.rs | 17 +++-- consensus/types/src/beacon_state.rs | 1 + 7 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 consensus/state_processing/src/common/is_attestation_same_slot.rs diff --git a/consensus/state_processing/src/common/get_attestation_participation.rs b/consensus/state_processing/src/common/get_attestation_participation.rs index 71bf6329f11..0fc0b0c1370 100644 --- a/consensus/state_processing/src/common/get_attestation_participation.rs +++ b/consensus/state_processing/src/common/get_attestation_participation.rs @@ -9,6 +9,8 @@ use types::{ }, }; +use crate::common::is_attestation_same_slot; + /// Get the participation flags for a valid attestation. /// /// You should have called `verify_attestation_for_block_inclusion` or similar before @@ -32,9 +34,32 @@ pub fn get_attestation_participation_flag_indices( let is_matching_source = data.source == justified_checkpoint; let is_matching_target = is_matching_source && data.target.root == *state.get_block_root_at_epoch(data.target.epoch)?; - let is_matching_head = + + let is_matching_blockroot = is_matching_target && data.beacon_block_root == *state.get_block_root(data.slot)?; + let is_matching_head = if state.fork_name_unchecked().gloas_enabled() { + let is_matching_payload = if is_attestation_same_slot(state, data)? { + // For same-slot attestations, data.index must be 0 + if data.index != 0 { + // TODO(EIP7732): consider if we want to use a different error type, since this is more of an overloaded data index scenario as opposed to the InvalidCommitteeIndex previous error. It's more like `AttestationInvalid::BadOverloadedDataIndex` + return Err(Error::InvalidCommitteeIndex(data.index)); + } + true + } else { + // For non same-slot attestations, check execution payload availability + // TODO(EIP7732) Discuss if we want to return new error BeaconStateError::InvalidExecutionPayloadAvailabilityIndex here for bit out of bounds or use something like BeaconStateError::InvalidBitfield + let slot_index = data.slot.as_usize() % E::slots_per_historical_root(); + state + .execution_payload_availability()? + .get(slot_index) + .map_err(|_| Error::InvalidExecutionPayloadAvailabilityIndex(slot_index))? + }; + is_matching_blockroot && is_matching_payload + } else { + is_matching_blockroot + }; + if !is_matching_source { return Err(Error::IncorrectAttestationSource); } diff --git a/consensus/state_processing/src/common/is_attestation_same_slot.rs b/consensus/state_processing/src/common/is_attestation_same_slot.rs new file mode 100644 index 00000000000..bb72d0c5ded --- /dev/null +++ b/consensus/state_processing/src/common/is_attestation_same_slot.rs @@ -0,0 +1,21 @@ +use types::{AttestationData, BeaconState, BeaconStateError, EthSpec}; + +/// Checks if the attestation was for the block proposed at the attestation slot. +/// +/// Returns true if: +/// - The attestation is for slot 0 (genesis), OR +/// - The attestation's beacon_block_root matches the block actually proposed at that slot +/// AND it's different from the previous slot's block (indicating no skip) +pub fn is_attestation_same_slot( + state: &BeaconState, + data: &AttestationData, +) -> Result { + if data.slot == 0 { + return Ok(true); + } + + let is_matching_block_root = data.beacon_block_root == *state.get_block_root(data.slot)?; + let is_current_block_root = data.beacon_block_root != *state.get_block_root(data.slot - 1)?; + + Ok(is_matching_block_root && is_current_block_root) +} diff --git a/consensus/state_processing/src/common/mod.rs b/consensus/state_processing/src/common/mod.rs index e550a6c48b1..51b28ce2456 100644 --- a/consensus/state_processing/src/common/mod.rs +++ b/consensus/state_processing/src/common/mod.rs @@ -3,6 +3,7 @@ mod get_attestation_participation; mod get_attesting_indices; mod get_payload_attesting_indices; mod initiate_validator_exit; +mod is_attestation_same_slot; mod slash_validator; pub mod altair; @@ -18,6 +19,7 @@ pub use get_payload_attesting_indices::{ get_indexed_payload_attestation, get_payload_attesting_indices, }; pub use initiate_validator_exit::initiate_validator_exit; +pub use is_attestation_same_slot::is_attestation_same_slot; pub use slash_validator::slash_validator; use safe_arith::SafeArith; diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index f3f5ef96af9..174b525978c 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -345,7 +345,10 @@ pub enum AttestationInvalid { attestation: Slot, }, /// Attestation slot is too far in the past to be included in a block. - IncludedTooLate { state: Slot, attestation: Slot }, + IncludedTooLate { + state: Slot, + attestation: Slot, + }, /// Attestation target epoch does not match attestation slot. TargetEpochSlotMismatch { target_epoch: Epoch, @@ -378,6 +381,7 @@ pub enum AttestationInvalid { BadSignature, /// The indexed attestation created from this attestation was found to be invalid. BadIndexedAttestation(IndexedAttestationInvalid), + BadOverloadedDataIndex, } impl From> diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index c0007424ef2..37160e7dcc2 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -2,10 +2,11 @@ use super::*; use crate::VerifySignatures; use crate::common::{ get_attestation_participation_flag_indices, increase_balance, initiate_validator_exit, - slash_validator, + is_attestation_same_slot, slash_validator, }; use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; use crate::per_block_processing::verify_payload_attestation::verify_payload_attestation; +use safe_arith::ArithError; use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}; use types::typenum::U33; @@ -175,12 +176,26 @@ pub mod altair_deneb { // Update epoch participation flags. let mut proposer_reward_numerator = 0; + + // Track validators for Gloas pending builder payment logic + let mut validators_setting_new_flags_effective_balances = + if state.fork_name_unchecked().gloas_enabled() { + // TODO(EIP7732): Discuss if want to set capacity to full list of attesters from the attestation + Some(Vec::with_capacity(indexed_att.attesting_indices_len())) + } else { + None + }; + for index in indexed_att.attesting_indices_iter() { let index = *index as usize; - let validator_effective_balance = state.epoch_cache().get_effective_balance(index)?; let validator_slashed = state.slashings_cache().is_slashed(index); + // [New in EIP7732] + // For same-slot attestations, check if we're setting any new flags + // If we are, this validator hasn't contributed to this slot's quorum yet + let mut will_set_new_flag = false; + for (flag_index, &weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() { let epoch_participation = state.get_epoch_participation_mut( data.target.epoch, @@ -205,9 +220,22 @@ pub mod altair_deneb { validator_effective_balance, validator_slashed, )?; + + will_set_new_flag = true; } } } + + // Collect validators for Gloas builder payment processing + if let Some(ref mut effective_balances) = + validators_setting_new_flags_effective_balances + { + // We will only add weight for same-slot attestations when any new flag is set + // This ensures each validator contributes exactly once per slot + if will_set_new_flag && is_attestation_same_slot(state, data)? { + effective_balances.push(validator_effective_balance); + } + } } let proposer_reward_denominator = WEIGHT_DENOMINATOR @@ -216,6 +244,38 @@ pub mod altair_deneb { .safe_div(PROPOSER_WEIGHT)?; let proposer_reward = proposer_reward_numerator.safe_div(proposer_reward_denominator)?; increase_balance(state, proposer_index as usize, proposer_reward)?; + + // [New in EIP-7732] Process builder payments + if let Some(effective_balances) = validators_setting_new_flags_effective_balances { + process_builder_payments_for_attestation(state, effective_balances, data, spec)?; + } + + Ok(()) + } + + /// Process builder payments for validators who set new participation flags in Gloas fork + fn process_builder_payments_for_attestation( + state: &mut BeaconState, + effective_balances: Vec, + data: &AttestationData, + _spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> { + let current_epoch_target = data.target.epoch == state.current_epoch(); + let payment_index = if current_epoch_target { + E::slots_per_epoch() + data.slot.as_u64() % E::slots_per_epoch() + } else { + data.slot.as_u64() % E::slots_per_epoch() + }; + + if let Ok(builder_payments) = state.builder_pending_payments_mut() { + if let Some(payment) = builder_payments.get_mut(payment_index as usize) { + effective_balances.iter().try_for_each(|&balance| { + payment.weight = payment.weight.safe_add(balance)?; + Ok::<(), ArithError>(()) + })?; + } + } + Ok(()) } } diff --git a/consensus/state_processing/src/per_block_processing/verify_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_attestation.rs index 0d1fd17768e..868b21c715d 100644 --- a/consensus/state_processing/src/per_block_processing/verify_attestation.rs +++ b/consensus/state_processing/src/per_block_processing/verify_attestation.rs @@ -63,19 +63,24 @@ pub fn verify_attestation_for_state<'ctxt, E: EthSpec>( ) -> Result> { let data = attestation.data(); - // NOTE: choosing a validation based on the attestation's fork - // rather than the state's fork makes this simple, but technically the spec - // defines this verification based on the state's fork. - match attestation { - AttestationRef::Base(_) => { + // TODO(EIP7732): modifying this to verify data.index based on fork as opposed to Attestation variant since we haven't modified Attestation superstruct since Electra. just checking if this is ok + match state.fork_name_unchecked() { + ForkName::Base + | ForkName::Altair + | ForkName::Bellatrix + | ForkName::Capella + | ForkName::Deneb => { verify!( data.index < state.get_committee_count_at_slot(data.slot)?, Invalid::BadCommitteeIndex ); } - AttestationRef::Electra(_) => { + ForkName::Electra | ForkName::Fulu => { verify!(data.index == 0, Invalid::BadCommitteeIndex); } + ForkName::Gloas => { + verify!(data.index < 2, Invalid::BadOverloadedDataIndex); + } } // Verify the Casper FFG vote. diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 550f0d67705..a9d63ea5348 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -190,6 +190,7 @@ pub enum Error { }, InvalidIndicesCount, PleaseNotifyTheDevs(String), + InvalidExecutionPayloadAvailabilityIndex(usize), } /// Control whether an epoch-indexed field can be indexed at the next epoch or not. From 30c70111b278d12898266d4805c6674e295b0941 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Thu, 23 Oct 2025 19:55:37 -0400 Subject: [PATCH 2/6] refactor process_attestations --- .../common/get_attestation_participation.rs | 6 +- .../src/common/is_attestation_same_slot.rs | 11 +- .../src/per_block_processing.rs | 1 + .../src/per_block_processing/errors.rs | 2 + .../process_attestations.rs | 322 ++++++++++++++++++ .../process_operations.rs | 230 +------------ .../verify_attestation.rs | 28 +- testing/ef_tests/src/cases/operations.rs | 7 +- 8 files changed, 357 insertions(+), 250 deletions(-) create mode 100644 consensus/state_processing/src/per_block_processing/process_attestations.rs diff --git a/consensus/state_processing/src/common/get_attestation_participation.rs b/consensus/state_processing/src/common/get_attestation_participation.rs index 0fc0b0c1370..a12748907f2 100644 --- a/consensus/state_processing/src/common/get_attestation_participation.rs +++ b/consensus/state_processing/src/common/get_attestation_participation.rs @@ -1,4 +1,5 @@ use integer_sqrt::IntegerSquareRoot; +use safe_arith::SafeArith; use smallvec::SmallVec; use types::{AttestationData, BeaconState, ChainSpec, EthSpec}; use types::{ @@ -49,7 +50,10 @@ pub fn get_attestation_participation_flag_indices( } else { // For non same-slot attestations, check execution payload availability // TODO(EIP7732) Discuss if we want to return new error BeaconStateError::InvalidExecutionPayloadAvailabilityIndex here for bit out of bounds or use something like BeaconStateError::InvalidBitfield - let slot_index = data.slot.as_usize() % E::slots_per_historical_root(); + let slot_index = data + .slot + .as_usize() + .safe_rem(E::slots_per_historical_root())?; state .execution_payload_availability()? .get(slot_index) diff --git a/consensus/state_processing/src/common/is_attestation_same_slot.rs b/consensus/state_processing/src/common/is_attestation_same_slot.rs index bb72d0c5ded..90becd71435 100644 --- a/consensus/state_processing/src/common/is_attestation_same_slot.rs +++ b/consensus/state_processing/src/common/is_attestation_same_slot.rs @@ -1,11 +1,7 @@ +use safe_arith::SafeArith; use types::{AttestationData, BeaconState, BeaconStateError, EthSpec}; /// Checks if the attestation was for the block proposed at the attestation slot. -/// -/// Returns true if: -/// - The attestation is for slot 0 (genesis), OR -/// - The attestation's beacon_block_root matches the block actually proposed at that slot -/// AND it's different from the previous slot's block (indicating no skip) pub fn is_attestation_same_slot( state: &BeaconState, data: &AttestationData, @@ -14,8 +10,9 @@ pub fn is_attestation_same_slot( return Ok(true); } - let is_matching_block_root = data.beacon_block_root == *state.get_block_root(data.slot)?; - let is_current_block_root = data.beacon_block_root != *state.get_block_root(data.slot - 1)?; + let is_matching_block_root = &data.beacon_block_root == state.get_block_root(data.slot)?; + let is_current_block_root = + &data.beacon_block_root != state.get_block_root(data.slot.safe_sub(1)?)?; Ok(is_matching_block_root && is_current_block_root) } diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index ab644f8ba66..19f856b8892 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -35,6 +35,7 @@ pub mod deneb; pub mod errors; mod is_valid_indexed_attestation; mod is_valid_indexed_payload_attestation; +pub mod process_attestations; pub mod process_operations; pub mod process_withdrawals; pub mod signature_sets; diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index 174b525978c..9c04b56e399 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -98,6 +98,8 @@ pub enum BlockProcessingError { ExecutionPayloadBidInvalid { reason: ExecutionPayloadBidInvalid, }, + /// Builder payment index out of bounds (Gloas) + BuilderPaymentIndexOutOfBounds(usize), } impl From for BlockProcessingError { diff --git a/consensus/state_processing/src/per_block_processing/process_attestations.rs b/consensus/state_processing/src/per_block_processing/process_attestations.rs new file mode 100644 index 00000000000..4538c7064be --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/process_attestations.rs @@ -0,0 +1,322 @@ +use super::*; +use crate::common::{ + get_attestation_participation_flag_indices, increase_balance, is_attestation_same_slot, +}; +use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; +use safe_arith::SafeArith; +use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}; + +pub mod base { + use super::*; + + /// Validates each `Attestation` and updates the state, short-circuiting on an invalid object. + /// + /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns + /// an `Err` describing the invalid object or cause of failure. + pub fn process_attestations<'a, E: EthSpec, I>( + state: &mut BeaconState, + attestations: I, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> + where + I: Iterator>, + { + // Ensure required caches are all built. These should be no-ops during regular operation. + state.build_committee_cache(RelativeEpoch::Current, spec)?; + state.build_committee_cache(RelativeEpoch::Previous, spec)?; + initialize_epoch_cache(state, spec)?; + initialize_progressive_balances_cache(state, spec)?; + state.build_slashings_cache()?; + + let proposer_index = ctxt.get_proposer_index(state, spec)?; + + // Verify and apply each attestation. + for (i, attestation) in attestations.enumerate() { + verify_attestation_for_block_inclusion( + state, + attestation, + ctxt, + verify_signatures, + spec, + ) + .map_err(|e| e.into_with_index(i))?; + + let AttestationRef::Base(attestation) = attestation else { + // Pending attestations have been deprecated in a altair, this branch should + // never happen + return Err(BlockProcessingError::PendingAttestationInElectra); + }; + + let pending_attestation = PendingAttestation { + aggregation_bits: attestation.aggregation_bits.clone(), + data: attestation.data.clone(), + inclusion_delay: state.slot().safe_sub(attestation.data.slot)?.as_u64(), + proposer_index, + }; + + if attestation.data.target.epoch == state.current_epoch() { + state + .as_base_mut()? + .current_epoch_attestations + .push(pending_attestation)?; + } else { + state + .as_base_mut()? + .previous_epoch_attestations + .push(pending_attestation)?; + } + } + + Ok(()) + } +} + +pub mod altair_gloas { + use super::*; + use crate::common::update_progressive_balances_cache::update_progressive_balances_on_attestation; + + pub fn process_attestation( + state: &mut BeaconState, + attestation: AttestationRef, + att_index: usize, + ctxt: &mut ConsensusContext, + verify_signatures: VerifySignatures, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> { + if !state.fork_name_unchecked().gloas_enabled() { + return altair::process_attestation( + state, + attestation, + att_index, + ctxt, + verify_signatures, + spec, + ); + } + + gloas::process_attestation(state, attestation, att_index, ctxt, verify_signatures, spec) + } + + pub fn process_attestations<'a, E: EthSpec, I>( + state: &mut BeaconState, + attestations: I, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> + where + I: Iterator>, + { + attestations.enumerate().try_for_each(|(i, attestation)| { + process_attestation(state, attestation, i, ctxt, verify_signatures, spec) + }) + } + + pub mod altair { + use super::*; + + pub fn process_attestation( + state: &mut BeaconState, + attestation: AttestationRef, + att_index: usize, + ctxt: &mut ConsensusContext, + verify_signatures: VerifySignatures, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> { + let proposer_index = ctxt.get_proposer_index(state, spec)?; + let previous_epoch = ctxt.previous_epoch; + let current_epoch = ctxt.current_epoch; + + let indexed_att = verify_attestation_for_block_inclusion( + state, + attestation, + ctxt, + verify_signatures, + spec, + ) + .map_err(|e| e.into_with_index(att_index))?; + + // Matching roots, participation flag indices + let data = attestation.data(); + let inclusion_delay = state.slot().safe_sub(data.slot)?.as_u64(); + let participation_flag_indices = + get_attestation_participation_flag_indices(state, data, inclusion_delay, spec)?; + + // Update epoch participation flags. + let mut proposer_reward_numerator = 0; + for index in indexed_att.attesting_indices_iter() { + let index = *index as usize; + + let validator_effective_balance = + state.epoch_cache().get_effective_balance(index)?; + let validator_slashed = state.slashings_cache().is_slashed(index); + + for (flag_index, &weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() { + let epoch_participation = state.get_epoch_participation_mut( + data.target.epoch, + previous_epoch, + current_epoch, + )?; + + if participation_flag_indices.contains(&flag_index) { + let validator_participation = epoch_participation + .get_mut(index) + .ok_or(BeaconStateError::ParticipationOutOfBounds(index))?; + + if !validator_participation.has_flag(flag_index)? { + validator_participation.add_flag(flag_index)?; + proposer_reward_numerator + .safe_add_assign(state.get_base_reward(index)?.safe_mul(weight)?)?; + + update_progressive_balances_on_attestation( + state, + data.target.epoch, + flag_index, + validator_effective_balance, + validator_slashed, + )?; + } + } + } + } + + let proposer_reward_denominator = WEIGHT_DENOMINATOR + .safe_sub(PROPOSER_WEIGHT)? + .safe_mul(WEIGHT_DENOMINATOR)? + .safe_div(PROPOSER_WEIGHT)?; + let proposer_reward = + proposer_reward_numerator.safe_div(proposer_reward_denominator)?; + increase_balance(state, proposer_index as usize, proposer_reward)?; + Ok(()) + } + } + + pub mod gloas { + use super::*; + + pub fn process_attestation( + state: &mut BeaconState, + attestation: AttestationRef, + att_index: usize, + ctxt: &mut ConsensusContext, + verify_signatures: VerifySignatures, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> { + let proposer_index = ctxt.get_proposer_index(state, spec)?; + let previous_epoch = ctxt.previous_epoch; + let current_epoch = ctxt.current_epoch; + + let indexed_att = verify_attestation_for_block_inclusion( + state, + attestation, + ctxt, + verify_signatures, + spec, + ) + .map_err(|e| e.into_with_index(att_index))?; + + // Matching roots, participation flag indices + let data = attestation.data(); + let inclusion_delay = state.slot().safe_sub(data.slot)?.as_u64(); + let participation_flag_indices = + get_attestation_participation_flag_indices(state, data, inclusion_delay, spec)?; + + // [New in EIP-7732] + let current_epoch_target = data.target.epoch == state.current_epoch(); + let slot_mod = data + .slot + .as_usize() + .safe_rem(E::slots_per_epoch() as usize)?; + let payment_index = if current_epoch_target { + (E::slots_per_epoch() as usize).safe_add(slot_mod)? + } else { + slot_mod + }; + // Accumulate weight for same-slot attestations + let mut accumulated_weight = 0; + + // Update epoch participation flags. + let mut proposer_reward_numerator = 0; + for index in indexed_att.attesting_indices_iter() { + let index = *index as usize; + + let validator_effective_balance = + state.epoch_cache().get_effective_balance(index)?; + let validator_slashed = state.slashings_cache().is_slashed(index); + + // [New in EIP7732] + // For same-slot attestations, check if we're setting any new flags + // If we are, this validator hasn't contributed to this slot's quorum yet + let mut will_set_new_flag = false; + + for (flag_index, &weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() { + let epoch_participation = state.get_epoch_participation_mut( + data.target.epoch, + previous_epoch, + current_epoch, + )?; + + if participation_flag_indices.contains(&flag_index) { + let validator_participation = epoch_participation + .get_mut(index) + .ok_or(BeaconStateError::ParticipationOutOfBounds(index))?; + + if !validator_participation.has_flag(flag_index)? { + validator_participation.add_flag(flag_index)?; + proposer_reward_numerator + .safe_add_assign(state.get_base_reward(index)?.safe_mul(weight)?)?; + will_set_new_flag = true; + + update_progressive_balances_on_attestation( + state, + data.target.epoch, + flag_index, + validator_effective_balance, + validator_slashed, + )?; + } + } + } + + // Check that payment_index is valid and get payment amount + let builder_payments = state.builder_pending_payments_mut()?; + let payment_amount = builder_payments + .get(payment_index) + .ok_or(BlockProcessingError::BuilderPaymentIndexOutOfBounds( + payment_index, + ))? + .withdrawal + .amount; + + // Collect validators for Gloas builder payment processing + // We will only add weight for same-slot attestations when any new flag is set + // This ensures each validator contributes exactly once per slot + if will_set_new_flag && is_attestation_same_slot(state, data)? && payment_amount > 0 + { + accumulated_weight.safe_add_assign(validator_effective_balance)?; + } + } + + let proposer_reward_denominator = WEIGHT_DENOMINATOR + .safe_sub(PROPOSER_WEIGHT)? + .safe_mul(WEIGHT_DENOMINATOR)? + .safe_div(PROPOSER_WEIGHT)?; + let proposer_reward = + proposer_reward_numerator.safe_div(proposer_reward_denominator)?; + increase_balance(state, proposer_index as usize, proposer_reward)?; + + // Update builder payment weight + if accumulated_weight > 0 { + let builder_payments = state.builder_pending_payments_mut()?; + let payment = builder_payments.get_mut(payment_index).ok_or( + BlockProcessingError::BuilderPaymentIndexOutOfBounds(payment_index), + )?; + payment.weight.safe_add_assign(accumulated_weight)?; + } + + Ok(()) + } + } +} 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 37160e7dcc2..ccebb5d740d 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -1,13 +1,8 @@ use super::*; -use crate::VerifySignatures; -use crate::common::{ - get_attestation_participation_flag_indices, increase_balance, initiate_validator_exit, - is_attestation_same_slot, slash_validator, -}; +use crate::common::{increase_balance, initiate_validator_exit, slash_validator}; use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; +use crate::per_block_processing::process_attestations::{altair_gloas, base}; use crate::per_block_processing::verify_payload_attestation::verify_payload_attestation; -use safe_arith::ArithError; -use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}; use types::typenum::U33; pub fn process_operations>( @@ -61,225 +56,6 @@ pub fn process_operations>( Ok(()) } -pub mod base { - use super::*; - - /// Validates each `Attestation` and updates the state, short-circuiting on an invalid object. - /// - /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns - /// an `Err` describing the invalid object or cause of failure. - pub fn process_attestations<'a, E: EthSpec, I>( - state: &mut BeaconState, - attestations: I, - verify_signatures: VerifySignatures, - ctxt: &mut ConsensusContext, - spec: &ChainSpec, - ) -> Result<(), BlockProcessingError> - where - I: Iterator>, - { - // Ensure required caches are all built. These should be no-ops during regular operation. - state.build_committee_cache(RelativeEpoch::Current, spec)?; - state.build_committee_cache(RelativeEpoch::Previous, spec)?; - initialize_epoch_cache(state, spec)?; - initialize_progressive_balances_cache(state, spec)?; - state.build_slashings_cache()?; - - let proposer_index = ctxt.get_proposer_index(state, spec)?; - - // Verify and apply each attestation. - for (i, attestation) in attestations.enumerate() { - verify_attestation_for_block_inclusion( - state, - attestation, - ctxt, - verify_signatures, - spec, - ) - .map_err(|e| e.into_with_index(i))?; - - let AttestationRef::Base(attestation) = attestation else { - // Pending attestations have been deprecated in a altair, this branch should - // never happen - return Err(BlockProcessingError::PendingAttestationInElectra); - }; - - let pending_attestation = PendingAttestation { - aggregation_bits: attestation.aggregation_bits.clone(), - data: attestation.data.clone(), - inclusion_delay: state.slot().safe_sub(attestation.data.slot)?.as_u64(), - proposer_index, - }; - - if attestation.data.target.epoch == state.current_epoch() { - state - .as_base_mut()? - .current_epoch_attestations - .push(pending_attestation)?; - } else { - state - .as_base_mut()? - .previous_epoch_attestations - .push(pending_attestation)?; - } - } - - Ok(()) - } -} - -pub mod altair_deneb { - use super::*; - use crate::common::update_progressive_balances_cache::update_progressive_balances_on_attestation; - - pub fn process_attestations<'a, E: EthSpec, I>( - state: &mut BeaconState, - attestations: I, - verify_signatures: VerifySignatures, - ctxt: &mut ConsensusContext, - spec: &ChainSpec, - ) -> Result<(), BlockProcessingError> - where - I: Iterator>, - { - attestations.enumerate().try_for_each(|(i, attestation)| { - process_attestation(state, attestation, i, ctxt, verify_signatures, spec) - }) - } - - pub fn process_attestation( - state: &mut BeaconState, - attestation: AttestationRef, - att_index: usize, - ctxt: &mut ConsensusContext, - verify_signatures: VerifySignatures, - spec: &ChainSpec, - ) -> Result<(), BlockProcessingError> { - let proposer_index = ctxt.get_proposer_index(state, spec)?; - let previous_epoch = ctxt.previous_epoch; - let current_epoch = ctxt.current_epoch; - - let indexed_att = verify_attestation_for_block_inclusion( - state, - attestation, - ctxt, - verify_signatures, - spec, - ) - .map_err(|e| e.into_with_index(att_index))?; - - // Matching roots, participation flag indices - let data = attestation.data(); - let inclusion_delay = state.slot().safe_sub(data.slot)?.as_u64(); - let participation_flag_indices = - get_attestation_participation_flag_indices(state, data, inclusion_delay, spec)?; - - // Update epoch participation flags. - let mut proposer_reward_numerator = 0; - - // Track validators for Gloas pending builder payment logic - let mut validators_setting_new_flags_effective_balances = - if state.fork_name_unchecked().gloas_enabled() { - // TODO(EIP7732): Discuss if want to set capacity to full list of attesters from the attestation - Some(Vec::with_capacity(indexed_att.attesting_indices_len())) - } else { - None - }; - - for index in indexed_att.attesting_indices_iter() { - let index = *index as usize; - let validator_effective_balance = state.epoch_cache().get_effective_balance(index)?; - let validator_slashed = state.slashings_cache().is_slashed(index); - - // [New in EIP7732] - // For same-slot attestations, check if we're setting any new flags - // If we are, this validator hasn't contributed to this slot's quorum yet - let mut will_set_new_flag = false; - - for (flag_index, &weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() { - let epoch_participation = state.get_epoch_participation_mut( - data.target.epoch, - previous_epoch, - current_epoch, - )?; - - if participation_flag_indices.contains(&flag_index) { - let validator_participation = epoch_participation - .get_mut(index) - .ok_or(BeaconStateError::ParticipationOutOfBounds(index))?; - - if !validator_participation.has_flag(flag_index)? { - validator_participation.add_flag(flag_index)?; - proposer_reward_numerator - .safe_add_assign(state.get_base_reward(index)?.safe_mul(weight)?)?; - - update_progressive_balances_on_attestation( - state, - data.target.epoch, - flag_index, - validator_effective_balance, - validator_slashed, - )?; - - will_set_new_flag = true; - } - } - } - - // Collect validators for Gloas builder payment processing - if let Some(ref mut effective_balances) = - validators_setting_new_flags_effective_balances - { - // We will only add weight for same-slot attestations when any new flag is set - // This ensures each validator contributes exactly once per slot - if will_set_new_flag && is_attestation_same_slot(state, data)? { - effective_balances.push(validator_effective_balance); - } - } - } - - let proposer_reward_denominator = WEIGHT_DENOMINATOR - .safe_sub(PROPOSER_WEIGHT)? - .safe_mul(WEIGHT_DENOMINATOR)? - .safe_div(PROPOSER_WEIGHT)?; - let proposer_reward = proposer_reward_numerator.safe_div(proposer_reward_denominator)?; - increase_balance(state, proposer_index as usize, proposer_reward)?; - - // [New in EIP-7732] Process builder payments - if let Some(effective_balances) = validators_setting_new_flags_effective_balances { - process_builder_payments_for_attestation(state, effective_balances, data, spec)?; - } - - Ok(()) - } - - /// Process builder payments for validators who set new participation flags in Gloas fork - fn process_builder_payments_for_attestation( - state: &mut BeaconState, - effective_balances: Vec, - data: &AttestationData, - _spec: &ChainSpec, - ) -> Result<(), BlockProcessingError> { - let current_epoch_target = data.target.epoch == state.current_epoch(); - let payment_index = if current_epoch_target { - E::slots_per_epoch() + data.slot.as_u64() % E::slots_per_epoch() - } else { - data.slot.as_u64() % E::slots_per_epoch() - }; - - if let Ok(builder_payments) = state.builder_pending_payments_mut() { - if let Some(payment) = builder_payments.get_mut(payment_index as usize) { - effective_balances.iter().try_for_each(|&balance| { - payment.weight = payment.weight.safe_add(balance)?; - Ok::<(), ArithError>(()) - })?; - } - } - - Ok(()) - } -} - /// Validates each `ProposerSlashing` and updates the state, short-circuiting on an invalid object. /// /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns @@ -354,7 +130,7 @@ pub fn process_attestations>( spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { if state.fork_name_unchecked().altair_enabled() { - altair_deneb::process_attestations( + altair_gloas::process_attestations( state, block_body.attestations(), verify_signatures, diff --git a/consensus/state_processing/src/per_block_processing/verify_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_attestation.rs index 868b21c715d..68672773cf8 100644 --- a/consensus/state_processing/src/per_block_processing/verify_attestation.rs +++ b/consensus/state_processing/src/per_block_processing/verify_attestation.rs @@ -63,23 +63,27 @@ pub fn verify_attestation_for_state<'ctxt, E: EthSpec>( ) -> Result> { let data = attestation.data(); - // TODO(EIP7732): modifying this to verify data.index based on fork as opposed to Attestation variant since we haven't modified Attestation superstruct since Electra. just checking if this is ok - match state.fork_name_unchecked() { - ForkName::Base - | ForkName::Altair - | ForkName::Bellatrix - | ForkName::Capella - | ForkName::Deneb => { + // NOTE: choosing a validation based on the attestation's fork + // rather than the state's fork makes this simple, but technically the spec + // defines this verification based on the state's fork. + // Verify data.index based on attestation variant. + // The attestation variant is determined by the block body variant, which matches the fork. + + // TODO(EIP-7732): discuss if it makes more sense to match on `ForkName` instead of attestation type. A reason against is an edge case like at the Gloas fork boundary, the first gloas block will contain attestations for a Fulu block, so I would think we would want this validation to still be with respect to fulu rules. But perhaps I'm wrong? + match attestation { + AttestationRef::Base(_) => { verify!( data.index < state.get_committee_count_at_slot(data.slot)?, Invalid::BadCommitteeIndex ); } - ForkName::Electra | ForkName::Fulu => { - verify!(data.index == 0, Invalid::BadCommitteeIndex); - } - ForkName::Gloas => { - verify!(data.index < 2, Invalid::BadOverloadedDataIndex); + AttestationRef::Electra(_) => { + let fork_at_attestation_slot = spec.fork_name_at_slot::(data.slot); + if fork_at_attestation_slot.gloas_enabled() { + verify!(data.index < 2, Invalid::BadOverloadedDataIndex); + } else { + verify!(data.index == 0, Invalid::BadCommitteeIndex); + } } } diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 63b46945c2f..121871f043c 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -14,10 +14,11 @@ use state_processing::{ per_block_processing::{ VerifyBlockRoot, VerifySignatures, errors::BlockProcessingError, + process_attestations::{altair_gloas, base}, process_block_header, process_execution_payload, process_operations::{ - altair_deneb, base, process_attester_slashings, process_bls_to_execution_changes, - process_deposits, process_exits, process_proposer_slashings, + process_attester_slashings, process_bls_to_execution_changes, process_deposits, + process_exits, process_proposer_slashings, }, process_sync_aggregate, process_withdrawals, }, @@ -101,7 +102,7 @@ impl Operation for Attestation { let mut ctxt = ConsensusContext::new(state.slot()); if state.fork_name_unchecked().altair_enabled() { initialize_progressive_balances_cache(state, spec)?; - altair_deneb::process_attestation( + altair_gloas::altair::process_attestation( state, self.to_ref(), 0, From 8d433274e8c8b898eecd44c45b20fb2d01add31a Mon Sep 17 00:00:00 2001 From: shane-moore Date: Fri, 24 Oct 2025 18:23:59 -0400 Subject: [PATCH 3/6] add process_attestations to process_operations --- .../common/get_attestation_participation.rs | 3 +- .../src/per_block_processing.rs | 1 - .../process_attestations.rs | 322 ----------------- .../process_operations.rs | 323 +++++++++++++++++- consensus/types/src/beacon_state.rs | 2 + testing/ef_tests/src/cases/operations.rs | 18 +- 6 files changed, 336 insertions(+), 333 deletions(-) delete mode 100644 consensus/state_processing/src/per_block_processing/process_attestations.rs diff --git a/consensus/state_processing/src/common/get_attestation_participation.rs b/consensus/state_processing/src/common/get_attestation_participation.rs index a12748907f2..833f522a928 100644 --- a/consensus/state_processing/src/common/get_attestation_participation.rs +++ b/consensus/state_processing/src/common/get_attestation_participation.rs @@ -43,8 +43,7 @@ pub fn get_attestation_participation_flag_indices( let is_matching_payload = if is_attestation_same_slot(state, data)? { // For same-slot attestations, data.index must be 0 if data.index != 0 { - // TODO(EIP7732): consider if we want to use a different error type, since this is more of an overloaded data index scenario as opposed to the InvalidCommitteeIndex previous error. It's more like `AttestationInvalid::BadOverloadedDataIndex` - return Err(Error::InvalidCommitteeIndex(data.index)); + return Err(Error::BadOverloadedDataIndex(data.index)); } true } else { diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 19f856b8892..ab644f8ba66 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -35,7 +35,6 @@ pub mod deneb; pub mod errors; mod is_valid_indexed_attestation; mod is_valid_indexed_payload_attestation; -pub mod process_attestations; pub mod process_operations; pub mod process_withdrawals; pub mod signature_sets; diff --git a/consensus/state_processing/src/per_block_processing/process_attestations.rs b/consensus/state_processing/src/per_block_processing/process_attestations.rs deleted file mode 100644 index 4538c7064be..00000000000 --- a/consensus/state_processing/src/per_block_processing/process_attestations.rs +++ /dev/null @@ -1,322 +0,0 @@ -use super::*; -use crate::common::{ - get_attestation_participation_flag_indices, increase_balance, is_attestation_same_slot, -}; -use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; -use safe_arith::SafeArith; -use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}; - -pub mod base { - use super::*; - - /// Validates each `Attestation` and updates the state, short-circuiting on an invalid object. - /// - /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns - /// an `Err` describing the invalid object or cause of failure. - pub fn process_attestations<'a, E: EthSpec, I>( - state: &mut BeaconState, - attestations: I, - verify_signatures: VerifySignatures, - ctxt: &mut ConsensusContext, - spec: &ChainSpec, - ) -> Result<(), BlockProcessingError> - where - I: Iterator>, - { - // Ensure required caches are all built. These should be no-ops during regular operation. - state.build_committee_cache(RelativeEpoch::Current, spec)?; - state.build_committee_cache(RelativeEpoch::Previous, spec)?; - initialize_epoch_cache(state, spec)?; - initialize_progressive_balances_cache(state, spec)?; - state.build_slashings_cache()?; - - let proposer_index = ctxt.get_proposer_index(state, spec)?; - - // Verify and apply each attestation. - for (i, attestation) in attestations.enumerate() { - verify_attestation_for_block_inclusion( - state, - attestation, - ctxt, - verify_signatures, - spec, - ) - .map_err(|e| e.into_with_index(i))?; - - let AttestationRef::Base(attestation) = attestation else { - // Pending attestations have been deprecated in a altair, this branch should - // never happen - return Err(BlockProcessingError::PendingAttestationInElectra); - }; - - let pending_attestation = PendingAttestation { - aggregation_bits: attestation.aggregation_bits.clone(), - data: attestation.data.clone(), - inclusion_delay: state.slot().safe_sub(attestation.data.slot)?.as_u64(), - proposer_index, - }; - - if attestation.data.target.epoch == state.current_epoch() { - state - .as_base_mut()? - .current_epoch_attestations - .push(pending_attestation)?; - } else { - state - .as_base_mut()? - .previous_epoch_attestations - .push(pending_attestation)?; - } - } - - Ok(()) - } -} - -pub mod altair_gloas { - use super::*; - use crate::common::update_progressive_balances_cache::update_progressive_balances_on_attestation; - - pub fn process_attestation( - state: &mut BeaconState, - attestation: AttestationRef, - att_index: usize, - ctxt: &mut ConsensusContext, - verify_signatures: VerifySignatures, - spec: &ChainSpec, - ) -> Result<(), BlockProcessingError> { - if !state.fork_name_unchecked().gloas_enabled() { - return altair::process_attestation( - state, - attestation, - att_index, - ctxt, - verify_signatures, - spec, - ); - } - - gloas::process_attestation(state, attestation, att_index, ctxt, verify_signatures, spec) - } - - pub fn process_attestations<'a, E: EthSpec, I>( - state: &mut BeaconState, - attestations: I, - verify_signatures: VerifySignatures, - ctxt: &mut ConsensusContext, - spec: &ChainSpec, - ) -> Result<(), BlockProcessingError> - where - I: Iterator>, - { - attestations.enumerate().try_for_each(|(i, attestation)| { - process_attestation(state, attestation, i, ctxt, verify_signatures, spec) - }) - } - - pub mod altair { - use super::*; - - pub fn process_attestation( - state: &mut BeaconState, - attestation: AttestationRef, - att_index: usize, - ctxt: &mut ConsensusContext, - verify_signatures: VerifySignatures, - spec: &ChainSpec, - ) -> Result<(), BlockProcessingError> { - let proposer_index = ctxt.get_proposer_index(state, spec)?; - let previous_epoch = ctxt.previous_epoch; - let current_epoch = ctxt.current_epoch; - - let indexed_att = verify_attestation_for_block_inclusion( - state, - attestation, - ctxt, - verify_signatures, - spec, - ) - .map_err(|e| e.into_with_index(att_index))?; - - // Matching roots, participation flag indices - let data = attestation.data(); - let inclusion_delay = state.slot().safe_sub(data.slot)?.as_u64(); - let participation_flag_indices = - get_attestation_participation_flag_indices(state, data, inclusion_delay, spec)?; - - // Update epoch participation flags. - let mut proposer_reward_numerator = 0; - for index in indexed_att.attesting_indices_iter() { - let index = *index as usize; - - let validator_effective_balance = - state.epoch_cache().get_effective_balance(index)?; - let validator_slashed = state.slashings_cache().is_slashed(index); - - for (flag_index, &weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() { - let epoch_participation = state.get_epoch_participation_mut( - data.target.epoch, - previous_epoch, - current_epoch, - )?; - - if participation_flag_indices.contains(&flag_index) { - let validator_participation = epoch_participation - .get_mut(index) - .ok_or(BeaconStateError::ParticipationOutOfBounds(index))?; - - if !validator_participation.has_flag(flag_index)? { - validator_participation.add_flag(flag_index)?; - proposer_reward_numerator - .safe_add_assign(state.get_base_reward(index)?.safe_mul(weight)?)?; - - update_progressive_balances_on_attestation( - state, - data.target.epoch, - flag_index, - validator_effective_balance, - validator_slashed, - )?; - } - } - } - } - - let proposer_reward_denominator = WEIGHT_DENOMINATOR - .safe_sub(PROPOSER_WEIGHT)? - .safe_mul(WEIGHT_DENOMINATOR)? - .safe_div(PROPOSER_WEIGHT)?; - let proposer_reward = - proposer_reward_numerator.safe_div(proposer_reward_denominator)?; - increase_balance(state, proposer_index as usize, proposer_reward)?; - Ok(()) - } - } - - pub mod gloas { - use super::*; - - pub fn process_attestation( - state: &mut BeaconState, - attestation: AttestationRef, - att_index: usize, - ctxt: &mut ConsensusContext, - verify_signatures: VerifySignatures, - spec: &ChainSpec, - ) -> Result<(), BlockProcessingError> { - let proposer_index = ctxt.get_proposer_index(state, spec)?; - let previous_epoch = ctxt.previous_epoch; - let current_epoch = ctxt.current_epoch; - - let indexed_att = verify_attestation_for_block_inclusion( - state, - attestation, - ctxt, - verify_signatures, - spec, - ) - .map_err(|e| e.into_with_index(att_index))?; - - // Matching roots, participation flag indices - let data = attestation.data(); - let inclusion_delay = state.slot().safe_sub(data.slot)?.as_u64(); - let participation_flag_indices = - get_attestation_participation_flag_indices(state, data, inclusion_delay, spec)?; - - // [New in EIP-7732] - let current_epoch_target = data.target.epoch == state.current_epoch(); - let slot_mod = data - .slot - .as_usize() - .safe_rem(E::slots_per_epoch() as usize)?; - let payment_index = if current_epoch_target { - (E::slots_per_epoch() as usize).safe_add(slot_mod)? - } else { - slot_mod - }; - // Accumulate weight for same-slot attestations - let mut accumulated_weight = 0; - - // Update epoch participation flags. - let mut proposer_reward_numerator = 0; - for index in indexed_att.attesting_indices_iter() { - let index = *index as usize; - - let validator_effective_balance = - state.epoch_cache().get_effective_balance(index)?; - let validator_slashed = state.slashings_cache().is_slashed(index); - - // [New in EIP7732] - // For same-slot attestations, check if we're setting any new flags - // If we are, this validator hasn't contributed to this slot's quorum yet - let mut will_set_new_flag = false; - - for (flag_index, &weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() { - let epoch_participation = state.get_epoch_participation_mut( - data.target.epoch, - previous_epoch, - current_epoch, - )?; - - if participation_flag_indices.contains(&flag_index) { - let validator_participation = epoch_participation - .get_mut(index) - .ok_or(BeaconStateError::ParticipationOutOfBounds(index))?; - - if !validator_participation.has_flag(flag_index)? { - validator_participation.add_flag(flag_index)?; - proposer_reward_numerator - .safe_add_assign(state.get_base_reward(index)?.safe_mul(weight)?)?; - will_set_new_flag = true; - - update_progressive_balances_on_attestation( - state, - data.target.epoch, - flag_index, - validator_effective_balance, - validator_slashed, - )?; - } - } - } - - // Check that payment_index is valid and get payment amount - let builder_payments = state.builder_pending_payments_mut()?; - let payment_amount = builder_payments - .get(payment_index) - .ok_or(BlockProcessingError::BuilderPaymentIndexOutOfBounds( - payment_index, - ))? - .withdrawal - .amount; - - // Collect validators for Gloas builder payment processing - // We will only add weight for same-slot attestations when any new flag is set - // This ensures each validator contributes exactly once per slot - if will_set_new_flag && is_attestation_same_slot(state, data)? && payment_amount > 0 - { - accumulated_weight.safe_add_assign(validator_effective_balance)?; - } - } - - let proposer_reward_denominator = WEIGHT_DENOMINATOR - .safe_sub(PROPOSER_WEIGHT)? - .safe_mul(WEIGHT_DENOMINATOR)? - .safe_div(PROPOSER_WEIGHT)?; - let proposer_reward = - proposer_reward_numerator.safe_div(proposer_reward_denominator)?; - increase_balance(state, proposer_index as usize, proposer_reward)?; - - // Update builder payment weight - if accumulated_weight > 0 { - let builder_payments = state.builder_pending_payments_mut()?; - let payment = builder_payments.get_mut(payment_index).ok_or( - BlockProcessingError::BuilderPaymentIndexOutOfBounds(payment_index), - )?; - payment.weight.safe_add_assign(accumulated_weight)?; - } - - Ok(()) - } - } -} 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 ccebb5d740d..2d7a32babef 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -1,8 +1,12 @@ use super::*; -use crate::common::{increase_balance, initiate_validator_exit, slash_validator}; +use crate::common::{ + get_attestation_participation_flag_indices, increase_balance, initiate_validator_exit, + is_attestation_same_slot, slash_validator, +}; use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; -use crate::per_block_processing::process_attestations::{altair_gloas, base}; use crate::per_block_processing::verify_payload_attestation::verify_payload_attestation; +use safe_arith::SafeArith; +use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}; use types::typenum::U33; pub fn process_operations>( @@ -56,6 +60,309 @@ pub fn process_operations>( Ok(()) } +pub mod base { + use super::*; + + /// Validates each `Attestation` and updates the state, short-circuiting on an invalid object. + /// + /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns + /// an `Err` describing the invalid object or cause of failure. + pub fn process_attestations<'a, E: EthSpec, I>( + state: &mut BeaconState, + attestations: I, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> + where + I: Iterator>, + { + // Ensure required caches are all built. These should be no-ops during regular operation. + state.build_committee_cache(RelativeEpoch::Current, spec)?; + state.build_committee_cache(RelativeEpoch::Previous, spec)?; + initialize_epoch_cache(state, spec)?; + initialize_progressive_balances_cache(state, spec)?; + state.build_slashings_cache()?; + + let proposer_index = ctxt.get_proposer_index(state, spec)?; + + // Verify and apply each attestation. + for (i, attestation) in attestations.enumerate() { + verify_attestation_for_block_inclusion( + state, + attestation, + ctxt, + verify_signatures, + spec, + ) + .map_err(|e| e.into_with_index(i))?; + + let AttestationRef::Base(attestation) = attestation else { + // Pending attestations have been deprecated in a altair, this branch should + // never happen + return Err(BlockProcessingError::PendingAttestationInElectra); + }; + + let pending_attestation = PendingAttestation { + aggregation_bits: attestation.aggregation_bits.clone(), + data: attestation.data.clone(), + inclusion_delay: state.slot().safe_sub(attestation.data.slot)?.as_u64(), + proposer_index, + }; + + if attestation.data.target.epoch == state.current_epoch() { + state + .as_base_mut()? + .current_epoch_attestations + .push(pending_attestation)?; + } else { + state + .as_base_mut()? + .previous_epoch_attestations + .push(pending_attestation)?; + } + } + + Ok(()) + } +} + +pub mod altair_deneb { + use super::*; + use crate::common::update_progressive_balances_cache::update_progressive_balances_on_attestation; + + pub fn process_attestations<'a, E: EthSpec, I>( + state: &mut BeaconState, + attestations: I, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> + where + I: Iterator>, + { + attestations.enumerate().try_for_each(|(i, attestation)| { + process_attestation(state, attestation, i, ctxt, verify_signatures, spec) + }) + } + + pub fn process_attestation( + state: &mut BeaconState, + attestation: AttestationRef, + att_index: usize, + ctxt: &mut ConsensusContext, + verify_signatures: VerifySignatures, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> { + let proposer_index = ctxt.get_proposer_index(state, spec)?; + let previous_epoch = ctxt.previous_epoch; + let current_epoch = ctxt.current_epoch; + + let indexed_att = verify_attestation_for_block_inclusion( + state, + attestation, + ctxt, + verify_signatures, + spec, + ) + .map_err(|e| e.into_with_index(att_index))?; + + // Matching roots, participation flag indices + let data = attestation.data(); + let inclusion_delay = state.slot().safe_sub(data.slot)?.as_u64(); + let participation_flag_indices = + get_attestation_participation_flag_indices(state, data, inclusion_delay, spec)?; + + // Update epoch participation flags. + let mut proposer_reward_numerator = 0; + for index in indexed_att.attesting_indices_iter() { + let index = *index as usize; + + let validator_effective_balance = state.epoch_cache().get_effective_balance(index)?; + let validator_slashed = state.slashings_cache().is_slashed(index); + + for (flag_index, &weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() { + let epoch_participation = state.get_epoch_participation_mut( + data.target.epoch, + previous_epoch, + current_epoch, + )?; + + if participation_flag_indices.contains(&flag_index) { + let validator_participation = epoch_participation + .get_mut(index) + .ok_or(BeaconStateError::ParticipationOutOfBounds(index))?; + + if !validator_participation.has_flag(flag_index)? { + validator_participation.add_flag(flag_index)?; + proposer_reward_numerator + .safe_add_assign(state.get_base_reward(index)?.safe_mul(weight)?)?; + + update_progressive_balances_on_attestation( + state, + data.target.epoch, + flag_index, + validator_effective_balance, + validator_slashed, + )?; + } + } + } + } + + let proposer_reward_denominator = WEIGHT_DENOMINATOR + .safe_sub(PROPOSER_WEIGHT)? + .safe_mul(WEIGHT_DENOMINATOR)? + .safe_div(PROPOSER_WEIGHT)?; + let proposer_reward = proposer_reward_numerator.safe_div(proposer_reward_denominator)?; + increase_balance(state, proposer_index as usize, proposer_reward)?; + Ok(()) + } +} + +// TODO(EIP-7732): add test cases to `consensus/state_processing/src/per_block_processing/tests.rs` to handle gloas. +// The tests will require being able to build gloas blocks, which currently fails due to errors as mentioned here. +// https://github.com/sigp/lighthouse/pull/8273 +pub mod gloas { + use super::*; + use crate::common::update_progressive_balances_cache::update_progressive_balances_on_attestation; + + pub fn process_attestations<'a, E: EthSpec, I>( + state: &mut BeaconState, + attestations: I, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> + where + I: Iterator>, + { + attestations.enumerate().try_for_each(|(i, attestation)| { + process_attestation(state, attestation, i, ctxt, verify_signatures, spec) + }) + } + + pub fn process_attestation( + state: &mut BeaconState, + attestation: AttestationRef, + att_index: usize, + ctxt: &mut ConsensusContext, + verify_signatures: VerifySignatures, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> { + let proposer_index = ctxt.get_proposer_index(state, spec)?; + let previous_epoch = ctxt.previous_epoch; + let current_epoch = ctxt.current_epoch; + + let indexed_att = verify_attestation_for_block_inclusion( + state, + attestation, + ctxt, + verify_signatures, + spec, + ) + .map_err(|e| e.into_with_index(att_index))?; + + // Matching roots, participation flag indices + let data = attestation.data(); + let inclusion_delay = state.slot().safe_sub(data.slot)?.as_u64(); + let participation_flag_indices = + get_attestation_participation_flag_indices(state, data, inclusion_delay, spec)?; + + // [New in EIP-7732] + let current_epoch_target = data.target.epoch == state.current_epoch(); + let slot_mod = data + .slot + .as_usize() + .safe_rem(E::slots_per_epoch() as usize)?; + let payment_index = if current_epoch_target { + (E::slots_per_epoch() as usize).safe_add(slot_mod)? + } else { + slot_mod + }; + // Accumulate weight for same-slot attestations + let mut accumulated_weight = 0; + + // Update epoch participation flags. + let mut proposer_reward_numerator = 0; + for index in indexed_att.attesting_indices_iter() { + let index = *index as usize; + + let validator_effective_balance = state.epoch_cache().get_effective_balance(index)?; + let validator_slashed = state.slashings_cache().is_slashed(index); + + // [New in EIP7732] + // For same-slot attestations, check if we're setting any new flags + // If we are, this validator hasn't contributed to this slot's quorum yet + let mut will_set_new_flag = false; + + for (flag_index, &weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() { + let epoch_participation = state.get_epoch_participation_mut( + data.target.epoch, + previous_epoch, + current_epoch, + )?; + + if participation_flag_indices.contains(&flag_index) { + let validator_participation = epoch_participation + .get_mut(index) + .ok_or(BeaconStateError::ParticipationOutOfBounds(index))?; + + if !validator_participation.has_flag(flag_index)? { + validator_participation.add_flag(flag_index)?; + proposer_reward_numerator + .safe_add_assign(state.get_base_reward(index)?.safe_mul(weight)?)?; + will_set_new_flag = true; + + update_progressive_balances_on_attestation( + state, + data.target.epoch, + flag_index, + validator_effective_balance, + validator_slashed, + )?; + } + } + } + + // Check that payment_index is valid and get payment amount + let builder_payments = state.builder_pending_payments_mut()?; + let payment_amount = builder_payments + .get(payment_index) + .ok_or(BlockProcessingError::BuilderPaymentIndexOutOfBounds( + payment_index, + ))? + .withdrawal + .amount; + + // Collect validators for Gloas builder payment processing + // We will only add weight for same-slot attestations when any new flag is set + // This ensures each validator contributes exactly once per slot + if will_set_new_flag && is_attestation_same_slot(state, data)? && payment_amount > 0 { + accumulated_weight.safe_add_assign(validator_effective_balance)?; + } + } + + let proposer_reward_denominator = WEIGHT_DENOMINATOR + .safe_sub(PROPOSER_WEIGHT)? + .safe_mul(WEIGHT_DENOMINATOR)? + .safe_div(PROPOSER_WEIGHT)?; + let proposer_reward = proposer_reward_numerator.safe_div(proposer_reward_denominator)?; + increase_balance(state, proposer_index as usize, proposer_reward)?; + + // Update builder payment weight + if accumulated_weight > 0 { + let builder_payments = state.builder_pending_payments_mut()?; + let payment = builder_payments.get_mut(payment_index).ok_or( + BlockProcessingError::BuilderPaymentIndexOutOfBounds(payment_index), + )?; + payment.weight.safe_add_assign(accumulated_weight)?; + } + + Ok(()) + } +} + /// Validates each `ProposerSlashing` and updates the state, short-circuiting on an invalid object. /// /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns @@ -129,8 +436,16 @@ pub fn process_attestations>( ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - if state.fork_name_unchecked().altair_enabled() { - altair_gloas::process_attestations( + if state.fork_name_unchecked().gloas_enabled() { + gloas::process_attestations( + state, + block_body.attestations(), + verify_signatures, + ctxt, + spec, + )?; + } else if state.fork_name_unchecked().altair_enabled() { + altair_deneb::process_attestations( state, block_body.attestations(), verify_signatures, diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index a9d63ea5348..78d7c4dca00 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -167,6 +167,8 @@ pub enum Error { NonExecutionAddressWithdrawalCredential, NoCommitteeFound(CommitteeIndex), InvalidCommitteeIndex(CommitteeIndex), + /// `Attestation.data.index` field is invalid in overloaded data index scenario. + BadOverloadedDataIndex(u64), InvalidSelectionProof { aggregator_index: u64, }, diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 121871f043c..86e63ca363c 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -7,14 +7,14 @@ use ssz::Decode; use state_processing::common::update_progressive_balances_cache::initialize_progressive_balances_cache; use state_processing::epoch_cache::initialize_epoch_cache; use state_processing::per_block_processing::process_operations::{ - process_consolidation_requests, process_deposit_requests, process_withdrawal_requests, + altair_deneb, base, gloas, process_consolidation_requests, process_deposit_requests, + process_withdrawal_requests, }; use state_processing::{ ConsensusContext, per_block_processing::{ VerifyBlockRoot, VerifySignatures, errors::BlockProcessingError, - process_attestations::{altair_gloas, base}, process_block_header, process_execution_payload, process_operations::{ process_attester_slashings, process_bls_to_execution_changes, process_deposits, @@ -100,9 +100,19 @@ impl Operation for Attestation { ) -> Result<(), BlockProcessingError> { initialize_epoch_cache(state, spec)?; let mut ctxt = ConsensusContext::new(state.slot()); - if state.fork_name_unchecked().altair_enabled() { + if state.fork_name_unchecked().gloas_enabled() { + initialize_progressive_balances_cache(state, spec)?; + gloas::process_attestation( + state, + self.to_ref(), + 0, + &mut ctxt, + VerifySignatures::True, + spec, + ) + } else if state.fork_name_unchecked().altair_enabled() { initialize_progressive_balances_cache(state, spec)?; - altair_gloas::altair::process_attestation( + altair_deneb::process_attestation( state, self.to_ref(), 0, From 4d199a54b36bd33ae1961390afa7709cfa75a50a Mon Sep 17 00:00:00 2001 From: shane-moore Date: Tue, 11 Nov 2025 13:39:41 -0300 Subject: [PATCH 4/6] add is_attestation_same_slot to beacon state --- .../common/get_attestation_participation.rs | 4 +--- .../src/common/is_attestation_same_slot.rs | 18 ------------------ consensus/state_processing/src/common/mod.rs | 2 -- .../process_operations.rs | 4 ++-- consensus/types/src/beacon_state.rs | 19 +++++++++++++++++++ 5 files changed, 22 insertions(+), 25 deletions(-) delete mode 100644 consensus/state_processing/src/common/is_attestation_same_slot.rs diff --git a/consensus/state_processing/src/common/get_attestation_participation.rs b/consensus/state_processing/src/common/get_attestation_participation.rs index 833f522a928..56b8efaf47a 100644 --- a/consensus/state_processing/src/common/get_attestation_participation.rs +++ b/consensus/state_processing/src/common/get_attestation_participation.rs @@ -10,8 +10,6 @@ use types::{ }, }; -use crate::common::is_attestation_same_slot; - /// Get the participation flags for a valid attestation. /// /// You should have called `verify_attestation_for_block_inclusion` or similar before @@ -40,7 +38,7 @@ pub fn get_attestation_participation_flag_indices( is_matching_target && data.beacon_block_root == *state.get_block_root(data.slot)?; let is_matching_head = if state.fork_name_unchecked().gloas_enabled() { - let is_matching_payload = if is_attestation_same_slot(state, data)? { + let is_matching_payload = if state.is_attestation_same_slot(data)? { // For same-slot attestations, data.index must be 0 if data.index != 0 { return Err(Error::BadOverloadedDataIndex(data.index)); diff --git a/consensus/state_processing/src/common/is_attestation_same_slot.rs b/consensus/state_processing/src/common/is_attestation_same_slot.rs deleted file mode 100644 index 90becd71435..00000000000 --- a/consensus/state_processing/src/common/is_attestation_same_slot.rs +++ /dev/null @@ -1,18 +0,0 @@ -use safe_arith::SafeArith; -use types::{AttestationData, BeaconState, BeaconStateError, EthSpec}; - -/// Checks if the attestation was for the block proposed at the attestation slot. -pub fn is_attestation_same_slot( - state: &BeaconState, - data: &AttestationData, -) -> Result { - if data.slot == 0 { - return Ok(true); - } - - let is_matching_block_root = &data.beacon_block_root == state.get_block_root(data.slot)?; - let is_current_block_root = - &data.beacon_block_root != state.get_block_root(data.slot.safe_sub(1)?)?; - - Ok(is_matching_block_root && is_current_block_root) -} diff --git a/consensus/state_processing/src/common/mod.rs b/consensus/state_processing/src/common/mod.rs index 51b28ce2456..e550a6c48b1 100644 --- a/consensus/state_processing/src/common/mod.rs +++ b/consensus/state_processing/src/common/mod.rs @@ -3,7 +3,6 @@ mod get_attestation_participation; mod get_attesting_indices; mod get_payload_attesting_indices; mod initiate_validator_exit; -mod is_attestation_same_slot; mod slash_validator; pub mod altair; @@ -19,7 +18,6 @@ pub use get_payload_attesting_indices::{ get_indexed_payload_attestation, get_payload_attesting_indices, }; pub use initiate_validator_exit::initiate_validator_exit; -pub use is_attestation_same_slot::is_attestation_same_slot; pub use slash_validator::slash_validator; use safe_arith::SafeArith; 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 2d7a32babef..880af9f4475 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -1,7 +1,7 @@ use super::*; use crate::common::{ get_attestation_participation_flag_indices, increase_balance, initiate_validator_exit, - is_attestation_same_slot, slash_validator, + slash_validator, }; use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; use crate::per_block_processing::verify_payload_attestation::verify_payload_attestation; @@ -338,7 +338,7 @@ pub mod gloas { // Collect validators for Gloas builder payment processing // We will only add weight for same-slot attestations when any new flag is set // This ensures each validator contributes exactly once per slot - if will_set_new_flag && is_attestation_same_slot(state, data)? && payment_amount > 0 { + if will_set_new_flag && state.is_attestation_same_slot(data)? && payment_amount > 0 { accumulated_weight.safe_add_assign(validator_effective_balance)?; } } diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 78d7c4dca00..4174f38eba4 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -1991,6 +1991,25 @@ impl BeaconState { Ok(cache.get_attestation_duties(validator_index)) } + /// Check if an attestation is for the same slot as the block it is attesting to. + /// + /// Returns `true` if the attestation's block root matches the block root at the + /// attestation's slot, and the block root differs from the previous slot's root. + pub fn is_attestation_same_slot( + &self, + data: &AttestationData, + ) -> Result { + if data.slot == 0 { + return Ok(true); + } + + let is_matching_block_root = &data.beacon_block_root == self.get_block_root(data.slot)?; + let is_current_block_root = + &data.beacon_block_root != self.get_block_root(data.slot.safe_sub(1)?)?; + + Ok(is_matching_block_root && is_current_block_root) + } + /// Compute the total active balance cache from scratch. /// /// This method should rarely be invoked because single-pass epoch processing keeps the total From c6973fa503003afe2ec156721a6ae7b850ac0ff4 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 24 Nov 2025 14:24:13 -0300 Subject: [PATCH 5/6] updates per latest consensus specs --- .../common/get_attestation_participation.rs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/consensus/state_processing/src/common/get_attestation_participation.rs b/consensus/state_processing/src/common/get_attestation_participation.rs index 56b8efaf47a..aabd5003f45 100644 --- a/consensus/state_processing/src/common/get_attestation_participation.rs +++ b/consensus/state_processing/src/common/get_attestation_participation.rs @@ -23,22 +23,25 @@ pub fn get_attestation_participation_flag_indices( inclusion_delay: u64, spec: &ChainSpec, ) -> Result, Error> { + // Matching source let justified_checkpoint = if data.target.epoch == state.current_epoch() { state.current_justified_checkpoint() } else { state.previous_justified_checkpoint() }; - - // Matching roots. let is_matching_source = data.source == justified_checkpoint; - let is_matching_target = is_matching_source - && data.target.root == *state.get_block_root_at_epoch(data.target.epoch)?; - let is_matching_blockroot = - is_matching_target && data.beacon_block_root == *state.get_block_root(data.slot)?; + // Matching target + let target_root = *state.get_block_root_at_epoch(data.target.epoch)?; + let target_root_matches = data.target.root == target_root; + let is_matching_target = is_matching_source && target_root_matches; + + // Matching head + let head_root = *state.get_block_root(data.slot)?; + let head_root_matches = data.beacon_block_root == head_root; let is_matching_head = if state.fork_name_unchecked().gloas_enabled() { - let is_matching_payload = if state.is_attestation_same_slot(data)? { + let payload_matches = if state.is_attestation_same_slot(data)? { // For same-slot attestations, data.index must be 0 if data.index != 0 { return Err(Error::BadOverloadedDataIndex(data.index)); @@ -56,9 +59,9 @@ pub fn get_attestation_participation_flag_indices( .get(slot_index) .map_err(|_| Error::InvalidExecutionPayloadAvailabilityIndex(slot_index))? }; - is_matching_blockroot && is_matching_payload + is_matching_target && head_root_matches && payload_matches } else { - is_matching_blockroot + is_matching_target && head_root_matches }; if !is_matching_source { From 2d02cb45814e308dd58eb2e4c2cc3a6aa3fd2cf1 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Tue, 25 Nov 2025 13:54:48 -0300 Subject: [PATCH 6/6] updates per spec pr 4693 --- consensus/types/src/beacon_state.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 4174f38eba4..f24d34489dc 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -1991,7 +1991,7 @@ impl BeaconState { Ok(cache.get_attestation_duties(validator_index)) } - /// Check if an attestation is for the same slot as the block it is attesting to. + /// Check if the attestation is for the block proposed at the attestation slot. /// /// Returns `true` if the attestation's block root matches the block root at the /// attestation's slot, and the block root differs from the previous slot's root. @@ -2003,11 +2003,11 @@ impl BeaconState { return Ok(true); } - let is_matching_block_root = &data.beacon_block_root == self.get_block_root(data.slot)?; - let is_current_block_root = - &data.beacon_block_root != self.get_block_root(data.slot.safe_sub(1)?)?; + let blockroot = data.beacon_block_root; + let slot_blockroot = *self.get_block_root(data.slot)?; + let prev_blockroot = *self.get_block_root(data.slot.safe_sub(1)?)?; - Ok(is_matching_block_root && is_current_block_root) + Ok(blockroot == slot_blockroot && blockroot != prev_blockroot) } /// Compute the total active balance cache from scratch.