mirror of
https://github.com/sigp/lighthouse.git
synced 2026-06-10 01:26:44 +00:00
EIP=8045 impl with tests for the slashing cache
This commit is contained in:
@@ -299,6 +299,67 @@ async fn proposer_slashing_duplicate_in_state() {
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slashings_cache_matches_state_after_block_import() {
|
||||
let db_path = tempdir().unwrap();
|
||||
let store = get_store(&db_path);
|
||||
let harness = get_harness(store.clone(), VALIDATOR_COUNT);
|
||||
|
||||
// Slash a spread of validators by importing proposer slashings into the op pool, exactly as
|
||||
// they would arrive over gossip.
|
||||
let slashed_validators = [0u64, 7, VALIDATOR_COUNT as u64 - 1];
|
||||
for &validator_index in &slashed_validators {
|
||||
let slashing = harness.make_proposer_slashing(validator_index);
|
||||
let ObservationOutcome::New(verified_slashing) = harness
|
||||
.chain
|
||||
.verify_proposer_slashing_for_gossip(slashing)
|
||||
.unwrap()
|
||||
else {
|
||||
panic!("slashing should verify");
|
||||
};
|
||||
harness.chain.import_proposer_slashing(verified_slashing);
|
||||
}
|
||||
|
||||
// Produce and import a block that includes the slashings. This drives the production flow:
|
||||
// `per_block_processing` -> `slash_validator` -> `SlashingsCache::record_validator_slashing`.
|
||||
harness
|
||||
.extend_chain(
|
||||
1,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
|
||||
let state = harness.get_current_state();
|
||||
|
||||
// The block processing above should have left the slashings cache initialized for the head.
|
||||
assert!(
|
||||
state.slashings_cache_is_initialized(),
|
||||
"slashings cache should be initialized after block import"
|
||||
);
|
||||
|
||||
// The targeted validators must actually be slashed in the state (i.e. the slashings were
|
||||
// included and applied, not silently dropped).
|
||||
for &validator_index in &slashed_validators {
|
||||
assert!(
|
||||
state
|
||||
.get_validator(validator_index as usize)
|
||||
.unwrap()
|
||||
.slashed,
|
||||
"validator {validator_index} should be slashed in the state"
|
||||
);
|
||||
}
|
||||
|
||||
// The cache must agree with the `slashed` flag of *every* validator in the state.
|
||||
for index in 0..state.validators().len() {
|
||||
assert_eq!(
|
||||
state.slashings_cache().is_slashed(index),
|
||||
state.get_validator(index).unwrap().slashed,
|
||||
"slashings cache disagrees with state at validator {index}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attester_slashing() {
|
||||
let db_path = tempdir().unwrap();
|
||||
|
||||
@@ -1394,7 +1394,20 @@ 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() {
|
||||
if self.slashings_cache_is_initialized() {
|
||||
let slashings_cache = self.slashings_cache();
|
||||
indices.retain(|&index| !slashings_cache.is_slashed(index));
|
||||
} else {
|
||||
// Fallback incase the slashing cache isnt initialized for the current slot.
|
||||
// The slashing cache may be cold during block replay or state reconstruction.
|
||||
// This fallback makes the slashing checks infallible.
|
||||
indices.retain(|&index| self.validators().get(index).is_some_and(|v| !v.slashed));
|
||||
}
|
||||
}
|
||||
|
||||
let preimage = self.get_seed(epoch, Domain::BeaconProposer, spec)?;
|
||||
self.compute_proposer_indices(epoch, preimage.as_slice(), &indices, spec)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user