Gloas alpha spec 9 (#9393)

Changes implemented

Ensure bids are for a higher slot than their parent (https://github.com/ethereum/consensus-specs/pull/5302)
Ignore PTC attestations for empty assigned slots (https://github.com/ethereum/consensus-specs/pull/5281)
Limit should_build_on_full checks to the previous slot (https://github.com/ethereum/consensus-specs/pull/5309)
Apply proposer boost if dependent roots match (https://github.com/ethereum/consensus-specs/pull/5306)
Exclude slashed validators from proposing (EIP-8045) (https://github.com/ethereum/consensus-specs/pull/5115)
Force the proposer to reorg late payloads (https://github.com/ethereum/consensus-specs/pull/5210)
Remove support for old deposit mechanism in Fulu (https://github.com/ethereum/consensus-specs/pull/4704)


  


Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu>

Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com>

Co-Authored-By: Eitan Seri-Levi <eserilev@gmail.com>

Co-Authored-By: Michael Sproul <michael@sigmaprime.io>

Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com>
This commit is contained in:
Eitan Seri-Levi
2026-06-15 16:56:09 -07:00
committed by GitHub
parent d8e406b6ac
commit 58e35bc96f
33 changed files with 785 additions and 247 deletions

View File

@@ -1127,21 +1127,33 @@ impl<E: EthSpec> BeaconState<E> {
// Post-Fulu we must never compute proposer indices using insufficient lookahead. This
// would be very dangerous as it would lead to conflicts between the *true* proposer as
// defined by `self.proposer_lookahead` and the output of this function.
// With MIN_SEED_LOOKAHEAD=1 (common config), this is equivalent to checking that the
// requested epoch is not the current epoch.
//
// We do not run this check if this function is called from `upgrade_to_fulu`,
// which runs *after* the slot is incremented, and needs to compute the proposer
// shuffling for the epoch that was just transitioned into.
if self.fork_name_unchecked().fulu_enabled()
&& epoch < current_epoch.safe_add(spec.min_seed_lookahead)?
{
return Err(
BeaconStateError::ComputeProposerIndicesInsufficientLookahead {
current_epoch,
request_epoch: epoch,
},
);
// Furthermore, post-Gloas, we must never compute proposers at any slot other than the
// dependent root slot itself, as slashings at subsequent slots have the ability to
// change the shuffling.
//
// For simplicity these two checks are combined into a single check on the dependent
// slot, which is safe for Fulu and Gloas. This function is always called from
// `get_beacon_proposer_indices`, which uses the cached lookahead for `current_epoch` and
// `next_epoch`. The only epoch's shuffling that should ordinarily be computed therefore
// is `next_epoch + 1`, which for Fulu and Gloas is computed during the epoch transition
// in the last slot of `current_epoch` (before the slot is incremented into
// `next_epoch`).
//
// The only case where computation of proposers in `current_epoch` and `next_epoch` is
// directly required is during the fork to Fulu itself
// (`upgrade_to_fulu`/`initialize_proposer_lookahead`), in which case the state is not
// yet the Fulu variant, and we omit the check.
if self.fork_name_unchecked().fulu_enabled() {
let dependent_slot = spec.proposer_shuffling_decision_slot::<E>(epoch);
if self.slot() != dependent_slot {
return Err(
BeaconStateError::ComputeProposerIndicesInsufficientLookahead {
current_epoch,
request_epoch: epoch,
},
);
}
}
} else {
// Pre-Fulu the situation is reversed, we *should not* compute proposer indices using
@@ -1375,7 +1387,7 @@ impl<E: EthSpec> BeaconState<E> {
spec: &ChainSpec,
) -> Result<Vec<usize>, BeaconStateError> {
// This isn't in the spec, but we remove the footgun that is requesting the current epoch
// for a Fulu state.
// or next epoch for a Fulu state.
if let Ok(proposer_lookahead) = self.proposer_lookahead()
&& epoch >= self.current_epoch()
&& epoch <= self.next_epoch()?
@@ -1394,7 +1406,15 @@ impl<E: EthSpec> BeaconState<E> {
}
// Not using the cached validator indices since they are shuffled.
let indices = self.get_active_validator_indices(epoch, spec)?;
let mut indices = self.get_active_validator_indices(epoch, spec)?;
// Post-Gloas, slashed validators are excluded from proposer selection
if self.fork_name_unchecked().gloas_enabled() {
let latest_block_slot = self.latest_block_header().slot;
let slashings_cache = self.slashings_cache();
slashings_cache.check_initialized(latest_block_slot)?;
indices.retain(|&index| !slashings_cache.is_slashed(index));
}
let preimage = self.get_seed(epoch, Domain::BeaconProposer, spec)?;
self.compute_proposer_indices(epoch, preimage.as_slice(), &indices, spec)

View File

@@ -64,3 +64,127 @@ impl SlashingsCache {
self.latest_block_slot = Some(latest_block_slot);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Epoch, Hash256};
use bls::PublicKeyBytes;
/// Build a minimal validator with the given `slashed` flag. The other fields are irrelevant to
/// the slashings cache.
fn validator(slashed: bool) -> Validator {
Validator {
pubkey: PublicKeyBytes::empty(),
withdrawal_credentials: Hash256::ZERO,
effective_balance: 0,
slashed,
activation_eligibility_epoch: Epoch::new(0),
activation_epoch: Epoch::new(0),
exit_epoch: Epoch::new(0),
withdrawable_epoch: Epoch::new(0),
}
}
/// Validators 1 and 3 are slashed, the rest are not.
fn validators() -> Vec<Validator> {
vec![
validator(false),
validator(true),
validator(false),
validator(true),
validator(false),
]
}
#[test]
fn new_captures_slashed_indices() {
let validators = validators();
let cache = SlashingsCache::new(Slot::new(7), validators.iter());
// The cache is initialized at the block slot it was built for.
assert!(cache.is_initialized(Slot::new(7)));
assert!(!cache.is_initialized(Slot::new(8)));
// Each index reports the same `slashed` status as the source validator.
for (index, validator) in validators.iter().enumerate() {
assert_eq!(
cache.is_slashed(index),
validator.slashed,
"validator {index} slashed status mismatch"
);
}
// An out-of-bounds index is not slashed.
assert!(!cache.is_slashed(validators.len()));
}
#[test]
fn default_is_uninitialized() {
let cache = SlashingsCache::default();
// A default cache is not initialized at any slot.
assert!(!cache.is_initialized(Slot::new(0)));
assert_eq!(
cache.check_initialized(Slot::new(0)),
Err(BeaconStateError::SlashingsCacheUninitialized {
initialized_slot: None,
latest_block_slot: Slot::new(0),
})
);
// It reports nothing as slashed. This is exactly why callers must check initialization
// before trusting `is_slashed`.
assert!(!cache.is_slashed(0));
}
#[test]
fn check_initialized_matches_block_slot() {
let cache = SlashingsCache::new(Slot::new(3), validators().iter());
assert_eq!(cache.check_initialized(Slot::new(3)), Ok(()));
assert_eq!(
cache.check_initialized(Slot::new(4)),
Err(BeaconStateError::SlashingsCacheUninitialized {
initialized_slot: Some(Slot::new(3)),
latest_block_slot: Slot::new(4),
})
);
}
#[test]
fn record_validator_slashing_requires_matching_slot() {
let mut cache = SlashingsCache::new(Slot::new(3), validators().iter());
// Index 0 starts unslashed.
assert!(!cache.is_slashed(0));
// Recording at the initialized slot succeeds and marks the validator slashed.
cache.record_validator_slashing(Slot::new(3), 0).unwrap();
assert!(cache.is_slashed(0));
// Recording at a slot the cache is not initialized for errors and leaves the set unchanged.
assert_eq!(
cache.record_validator_slashing(Slot::new(4), 2),
Err(BeaconStateError::SlashingsCacheUninitialized {
initialized_slot: Some(Slot::new(3)),
latest_block_slot: Slot::new(4),
})
);
assert!(!cache.is_slashed(2));
}
#[test]
fn update_latest_block_slot_preserves_slashed_set() {
let mut cache = SlashingsCache::new(Slot::new(3), validators().iter());
cache.update_latest_block_slot(Slot::new(4));
// The initialized slot moves forward without clearing the recorded slashings.
assert!(!cache.is_initialized(Slot::new(3)));
assert!(cache.is_initialized(Slot::new(4)));
assert!(cache.is_slashed(1));
assert!(cache.is_slashed(3));
assert!(!cache.is_slashed(0));
}
}