Use head state for exit verification (#4183)

## Issue Addressed

NA

## Proposed Changes

Similar to #4181 but without the version bump and a more nuanced fix.

Patches the high CPU usage seen after the Capella fork which was caused by processing exits when there are skip slots.

## Additional Info

~~This is an imperfect solution that will cause us to drop some exits at the fork boundary. This is tracked at #4184.~~
This commit is contained in:
Paul Hauner
2023-04-14 01:11:46 +00:00
parent 56dba96319
commit 2b3084f578
8 changed files with 106 additions and 23 deletions

View File

@@ -2206,12 +2206,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
&self,
exit: SignedVoluntaryExit,
) -> Result<ObservationOutcome<SignedVoluntaryExit, T::EthSpec>, Error> {
// NOTE: this could be more efficient if it avoided cloning the head state
let wall_clock_state = self.wall_clock_state()?;
let head_snapshot = self.head().snapshot;
let head_state = &head_snapshot.beacon_state;
let wall_clock_epoch = self.epoch()?;
Ok(self
.observed_voluntary_exits
.lock()
.verify_and_observe(exit, &wall_clock_state, &self.spec)
.verify_and_observe_at(exit, wall_clock_epoch, head_state, &self.spec)
.map(|exit| {
// this method is called for both API and gossip exits, so this covers all exit events
if let Some(event_handler) = self.event_handler.as_ref() {

View File

@@ -1,11 +1,11 @@
use derivative::Derivative;
use smallvec::{smallvec, SmallVec};
use ssz::{Decode, Encode};
use state_processing::{SigVerifiedOp, VerifyOperation};
use state_processing::{SigVerifiedOp, VerifyOperation, VerifyOperationAt};
use std::collections::HashSet;
use std::marker::PhantomData;
use types::{
AttesterSlashing, BeaconState, ChainSpec, EthSpec, ForkName, ProposerSlashing,
AttesterSlashing, BeaconState, ChainSpec, Epoch, EthSpec, ForkName, ProposerSlashing,
SignedBlsToExecutionChange, SignedVoluntaryExit, Slot,
};
@@ -87,12 +87,16 @@ impl<E: EthSpec> ObservableOperation<E> for SignedBlsToExecutionChange {
}
impl<T: ObservableOperation<E>, E: EthSpec> ObservedOperations<T, E> {
pub fn verify_and_observe(
pub fn verify_and_observe_parametric<F>(
&mut self,
op: T,
validate: F,
head_state: &BeaconState<E>,
spec: &ChainSpec,
) -> Result<ObservationOutcome<T, E>, T::Error> {
) -> Result<ObservationOutcome<T, E>, T::Error>
where
F: Fn(T) -> Result<SigVerifiedOp<T, E>, T::Error>,
{
self.reset_at_fork_boundary(head_state.slot(), spec);
let observed_validator_indices = &mut self.observed_validator_indices;
@@ -112,7 +116,7 @@ impl<T: ObservableOperation<E>, E: EthSpec> ObservedOperations<T, E> {
}
// Validate the op using operation-specific logic (`verify_attester_slashing`, etc).
let verified_op = op.validate(head_state, spec)?;
let verified_op = validate(op)?;
// Add the relevant indices to the set of known indices to prevent processing of duplicates
// in the future.
@@ -121,6 +125,16 @@ impl<T: ObservableOperation<E>, E: EthSpec> ObservedOperations<T, E> {
Ok(ObservationOutcome::New(verified_op))
}
pub fn verify_and_observe(
&mut self,
op: T,
head_state: &BeaconState<E>,
spec: &ChainSpec,
) -> Result<ObservationOutcome<T, E>, T::Error> {
let validate = |op: T| op.validate(head_state, spec);
self.verify_and_observe_parametric(op, validate, head_state, spec)
}
/// Reset the cache when crossing a fork boundary.
///
/// This prevents an attacker from crafting a self-slashing which is only valid before the fork
@@ -140,3 +154,16 @@ impl<T: ObservableOperation<E>, E: EthSpec> ObservedOperations<T, E> {
}
}
}
impl<T: ObservableOperation<E> + VerifyOperationAt<E>, E: EthSpec> ObservedOperations<T, E> {
pub fn verify_and_observe_at(
&mut self,
op: T,
verify_at_epoch: Epoch,
head_state: &BeaconState<E>,
spec: &ChainSpec,
) -> Result<ObservationOutcome<T, E>, T::Error> {
let validate = |op: T| op.validate_at(head_state, verify_at_epoch, spec);
self.verify_and_observe_parametric(op, validate, head_state, spec)
}
}