Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use integer_sqrt::IntegerSquareRoot;
use safe_arith::SafeArith;
use smallvec::SmallVec;
use types::{AttestationData, BeaconState, ChainSpec, EthSpec};
use types::{
BeaconStateError as Error,
AttestationData, BeaconState, BeaconStateError as Error, ChainSpec, EthSpec,
consts::altair::{
NUM_FLAG_INDICES, TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX,
TIMELY_TARGET_FLAG_INDEX,
Expand All @@ -16,6 +16,8 @@ use types::{
///
/// This function will return an error if the source of the attestation doesn't match the
/// state's relevant justified checkpoint.
///
/// This function has been abstracted to work for all forks from Altair to Gloas.
pub fn get_attestation_participation_flag_indices<E: EthSpec>(
state: &BeaconState<E>,
data: &AttestationData,
Expand All @@ -27,13 +29,43 @@ pub fn get_attestation_participation_flag_indices<E: EthSpec>(
} else {
state.previous_justified_checkpoint()
};

// Matching roots.
let is_matching_source = data.source == justified_checkpoint;

// Matching target.
let is_matching_target = is_matching_source
&& data.target.root == *state.get_block_root_at_epoch(data.target.epoch)?;
let is_matching_head =
is_matching_target && data.beacon_block_root == *state.get_block_root(data.slot)?;

// [New in Gloas:EIP7732]
let payload_matches = if state.fork_name_unchecked().gloas_enabled() {
if state.is_attestation_same_slot(data)? {
// For same-slot attestations, data.index must be 0
if data.index != 0 {
return Err(Error::BadOverloadedDataIndex(data.index));
}
true
} else {
// For non same-slot attestations, check execution payload availability
let slot_index = data
.slot
.as_usize()
.safe_rem(E::slots_per_historical_root())?;
let payload_index = state
.execution_payload_availability()?
.get(slot_index)
.map(|avail| if avail { 1 } else { 0 })
.map_err(|_| Error::InvalidExecutionPayloadAvailabilityIndex(slot_index))?;
data.index == payload_index
}
} else {
// Essentially `payload_matches` is always true pre-Gloas (it is not considered for matching
// head).
true
};

// Matching head.
let is_matching_head = is_matching_target
&& data.beacon_block_root == *state.get_block_root(data.slot)?
&& payload_matches;

if !is_matching_source {
return Err(Error::IncorrectAttestationSource);
Expand Down
4 changes: 4 additions & 0 deletions consensus/state_processing/src/per_block_processing/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ pub enum BlockProcessingError {
IncorrectExpectedWithdrawalsVariant,
MissingLastWithdrawal,
PendingAttestationInElectra,
/// Builder payment index out of bounds (Gloas)
BuilderPaymentIndexOutOfBounds(usize),
}

impl From<BeaconStateError> for BlockProcessingError {
Expand Down Expand Up @@ -372,6 +374,8 @@ pub enum AttestationInvalid {
BadSignature,
/// The indexed attestation created from this attestation was found to be invalid.
BadIndexedAttestation(IndexedAttestationInvalid),
/// The overloaded "data.index" field is invalid (post-Gloas).
BadOverloadedDataIndex,
}

impl From<BlockOperationError<IndexedAttestationInvalid>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,148 @@ pub mod altair_deneb {
}
}

pub mod gloas {
use super::*;
use crate::common::update_progressive_balances_cache::update_progressive_balances_on_attestation;

pub fn process_attestations<'a, E: EthSpec, I>(
state: &mut BeaconState<E>,
attestations: I,
verify_signatures: VerifySignatures,
ctxt: &mut ConsensusContext<E>,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError>
where
I: Iterator<Item = AttestationRef<'a, E>>,
{
attestations.enumerate().try_for_each(|(i, attestation)| {
process_attestation(state, attestation, i, ctxt, verify_signatures, spec)
})
}

pub fn process_attestation<E: EthSpec>(
state: &mut BeaconState<E>,
attestation: AttestationRef<E>,
att_index: usize,
ctxt: &mut ConsensusContext<E>,
verify_signatures: VerifySignatures,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError> {
let proposer_index = ctxt.get_proposer_index(state, spec)?;
let previous_epoch = ctxt.previous_epoch;
let current_epoch = ctxt.current_epoch;

let indexed_att = verify_attestation_for_block_inclusion(
state,
attestation,
ctxt,
verify_signatures,
spec,
)
.map_err(|e| e.into_with_index(att_index))?;

// Matching roots, participation flag indices
let data = attestation.data();
let inclusion_delay = state.slot().safe_sub(data.slot)?.as_u64();
let participation_flag_indices =
get_attestation_participation_flag_indices(state, data, inclusion_delay, spec)?;

// [New in EIP-7732]
let current_epoch_target = data.target.epoch == state.current_epoch();
let slot_mod = data
.slot
.as_usize()
.safe_rem(E::slots_per_epoch() as usize)?;
let payment_index = if current_epoch_target {
(E::slots_per_epoch() as usize).safe_add(slot_mod)?
} else {
slot_mod
};
// Cached here to avoid repeat lookups. The withdrawal amount is immutable throughout
// this whole function.
let payment_withdrawal_amount = state
.builder_pending_payments()?
.get(payment_index)
.ok_or(BlockProcessingError::BuilderPaymentIndexOutOfBounds(
payment_index,
))?
.withdrawal
.amount;

// Update epoch participation flags.
let mut proposer_reward_numerator = 0;
for index in indexed_att.attesting_indices_iter() {
let index = *index as usize;

let validator_effective_balance = state.epoch_cache().get_effective_balance(index)?;
let validator_slashed = state.slashings_cache().is_slashed(index);

// [New in Gloas:EIP7732]
// For same-slot attestations, check if we're setting any new flags
// If we are, this validator hasn't contributed to this slot's quorum yet
let mut will_set_new_flag = false;

for (flag_index, &weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() {
let epoch_participation = state.get_epoch_participation_mut(
data.target.epoch,
previous_epoch,
current_epoch,
)?;

if participation_flag_indices.contains(&flag_index) {
let validator_participation = epoch_participation
.get_mut(index)
.ok_or(BeaconStateError::ParticipationOutOfBounds(index))?;

if !validator_participation.has_flag(flag_index)? {
validator_participation.add_flag(flag_index)?;
proposer_reward_numerator
.safe_add_assign(state.get_base_reward(index)?.safe_mul(weight)?)?;
will_set_new_flag = true;

update_progressive_balances_on_attestation(
state,
data.target.epoch,
flag_index,
validator_effective_balance,
validator_slashed,
)?;
}
}
}

// [New in Gloas:EIP7732]
// Add weight for same-slot attestations when any new flag is set.
// This ensures each validator contributes exactly once per slot.
if will_set_new_flag
&& state.is_attestation_same_slot(data)?
&& payment_withdrawal_amount > 0
{
let builder_payments = state.builder_pending_payments_mut()?;
let payment = builder_payments.get_mut(payment_index).ok_or(
BlockProcessingError::BuilderPaymentIndexOutOfBounds(payment_index),
)?;
payment
.weight
.safe_add_assign(validator_effective_balance)?;
}
}

let proposer_reward_denominator = WEIGHT_DENOMINATOR
.safe_sub(PROPOSER_WEIGHT)?
.safe_mul(WEIGHT_DENOMINATOR)?
.safe_div(PROPOSER_WEIGHT)?;
let proposer_reward = proposer_reward_numerator.safe_div(proposer_reward_denominator)?;
increase_balance(state, proposer_index as usize, proposer_reward)?;

// [New in Gloas:EIP7732]
// Update builder payment weight
// No-op, this is done inline above.

Ok(())
}
}

/// Validates each `ProposerSlashing` and updates the state, short-circuiting on an invalid object.
///
/// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns
Expand Down Expand Up @@ -285,7 +427,15 @@ pub fn process_attestations<E: EthSpec, Payload: AbstractExecPayload<E>>(
ctxt: &mut ConsensusContext<E>,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError> {
if state.fork_name_unchecked().altair_enabled() {
if state.fork_name_unchecked().gloas_enabled() {
gloas::process_attestations(
state,
block_body.attestations(),
verify_signatures,
ctxt,
spec,
)?;
} else if state.fork_name_unchecked().altair_enabled() {
altair_deneb::process_attestations(
state,
block_body.attestations(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,12 @@ pub fn verify_attestation_for_state<'ctxt, E: EthSpec>(
);
}
AttestationRef::Electra(_) => {
verify!(data.index == 0, Invalid::BadCommitteeIndex);
let fork_at_attestation_slot = spec.fork_name_at_slot::<E>(data.slot);
if fork_at_attestation_slot.gloas_enabled() {
verify!(data.index < 2, Invalid::BadOverloadedDataIndex);
} else {
verify!(data.index == 0, Invalid::BadCommitteeIndex);
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions consensus/types/src/state/beacon_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ pub enum BeaconStateError {
NonExecutionAddressWithdrawalCredential,
NoCommitteeFound(CommitteeIndex),
InvalidCommitteeIndex(CommitteeIndex),
/// `Attestation.data.index` field is invalid in overloaded data index scenario.
BadOverloadedDataIndex(u64),
InvalidSelectionProof {
aggregator_index: u64,
},
Expand All @@ -198,6 +200,7 @@ pub enum BeaconStateError {
i: usize,
},
InvalidIndicesCount,
InvalidExecutionPayloadAvailabilityIndex(usize),
}

/// Control whether an epoch-indexed field can be indexed at the next epoch or not.
Expand Down
1 change: 0 additions & 1 deletion testing/ef_tests/check_all_files_accessed.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
"tests/.*/eip7732",
"tests/.*/eip7805",
# TODO(gloas): remove these ignores as more Gloas operations are implemented
"tests/.*/gloas/operations/attestation/.*",
"tests/.*/gloas/operations/attester_slashing/.*",
"tests/.*/gloas/operations/block_header/.*",
"tests/.*/gloas/operations/bls_to_execution_change/.*",
Expand Down
18 changes: 14 additions & 4 deletions testing/ef_tests/src/cases/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ use state_processing::{
errors::BlockProcessingError,
process_block_header, process_execution_payload,
process_operations::{
altair_deneb, base, process_attester_slashings, process_bls_to_execution_changes,
process_deposits, process_exits, process_proposer_slashings,
altair_deneb, base, gloas, process_attester_slashings,
process_bls_to_execution_changes, process_deposits, process_exits,
process_proposer_slashings,
},
process_sync_aggregate, withdrawals,
},
Expand Down Expand Up @@ -98,9 +99,18 @@ impl<E: EthSpec> Operation<E> for Attestation<E> {
_: &Operations<E, Self>,
) -> Result<(), BlockProcessingError> {
initialize_epoch_cache(state, spec)?;
initialize_progressive_balances_cache(state, spec)?;
let mut ctxt = ConsensusContext::new(state.slot());
if state.fork_name_unchecked().altair_enabled() {
initialize_progressive_balances_cache(state, spec)?;
if state.fork_name_unchecked().gloas_enabled() {
gloas::process_attestation(
state,
self.to_ref(),
0,
&mut ctxt,
VerifySignatures::True,
spec,
)
} else if state.fork_name_unchecked().altair_enabled() {
altair_deneb::process_attestation(
state,
self.to_ref(),
Expand Down
4 changes: 3 additions & 1 deletion testing/ef_tests/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,9 @@ impl<E: EthSpec + TypeName, O: Operation<E>> Handler for OperationsHandler<E, O>
fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool {
// TODO(gloas): So far only withdrawals tests are enabled for Gloas.
Self::Case::is_enabled_for_fork(fork_name)
&& (!fork_name.gloas_enabled() || self.handler_name() == "withdrawals")
&& (!fork_name.gloas_enabled()
|| self.handler_name() == "withdrawals"
|| self.handler_name() == "attestation")
}
}

Expand Down
Loading