Skip to content

Commit 2b3084f

Browse files
committed
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.~~
1 parent 56dba96 commit 2b3084f

File tree

8 files changed

+106
-23
lines changed

8 files changed

+106
-23
lines changed

beacon_node/beacon_chain/src/beacon_chain.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2206,12 +2206,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
22062206
&self,
22072207
exit: SignedVoluntaryExit,
22082208
) -> Result<ObservationOutcome<SignedVoluntaryExit, T::EthSpec>, Error> {
2209-
// NOTE: this could be more efficient if it avoided cloning the head state
2210-
let wall_clock_state = self.wall_clock_state()?;
2209+
let head_snapshot = self.head().snapshot;
2210+
let head_state = &head_snapshot.beacon_state;
2211+
let wall_clock_epoch = self.epoch()?;
2212+
22112213
Ok(self
22122214
.observed_voluntary_exits
22132215
.lock()
2214-
.verify_and_observe(exit, &wall_clock_state, &self.spec)
2216+
.verify_and_observe_at(exit, wall_clock_epoch, head_state, &self.spec)
22152217
.map(|exit| {
22162218
// this method is called for both API and gossip exits, so this covers all exit events
22172219
if let Some(event_handler) = self.event_handler.as_ref() {

beacon_node/beacon_chain/src/observed_operations.rs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use derivative::Derivative;
22
use smallvec::{smallvec, SmallVec};
33
use ssz::{Decode, Encode};
4-
use state_processing::{SigVerifiedOp, VerifyOperation};
4+
use state_processing::{SigVerifiedOp, VerifyOperation, VerifyOperationAt};
55
use std::collections::HashSet;
66
use std::marker::PhantomData;
77
use types::{
8-
AttesterSlashing, BeaconState, ChainSpec, EthSpec, ForkName, ProposerSlashing,
8+
AttesterSlashing, BeaconState, ChainSpec, Epoch, EthSpec, ForkName, ProposerSlashing,
99
SignedBlsToExecutionChange, SignedVoluntaryExit, Slot,
1010
};
1111

@@ -87,12 +87,16 @@ impl<E: EthSpec> ObservableOperation<E> for SignedBlsToExecutionChange {
8787
}
8888

8989
impl<T: ObservableOperation<E>, E: EthSpec> ObservedOperations<T, E> {
90-
pub fn verify_and_observe(
90+
pub fn verify_and_observe_parametric<F>(
9191
&mut self,
9292
op: T,
93+
validate: F,
9394
head_state: &BeaconState<E>,
9495
spec: &ChainSpec,
95-
) -> Result<ObservationOutcome<T, E>, T::Error> {
96+
) -> Result<ObservationOutcome<T, E>, T::Error>
97+
where
98+
F: Fn(T) -> Result<SigVerifiedOp<T, E>, T::Error>,
99+
{
96100
self.reset_at_fork_boundary(head_state.slot(), spec);
97101

98102
let observed_validator_indices = &mut self.observed_validator_indices;
@@ -112,7 +116,7 @@ impl<T: ObservableOperation<E>, E: EthSpec> ObservedOperations<T, E> {
112116
}
113117

114118
// Validate the op using operation-specific logic (`verify_attester_slashing`, etc).
115-
let verified_op = op.validate(head_state, spec)?;
119+
let verified_op = validate(op)?;
116120

117121
// Add the relevant indices to the set of known indices to prevent processing of duplicates
118122
// in the future.
@@ -121,6 +125,16 @@ impl<T: ObservableOperation<E>, E: EthSpec> ObservedOperations<T, E> {
121125
Ok(ObservationOutcome::New(verified_op))
122126
}
123127

128+
pub fn verify_and_observe(
129+
&mut self,
130+
op: T,
131+
head_state: &BeaconState<E>,
132+
spec: &ChainSpec,
133+
) -> Result<ObservationOutcome<T, E>, T::Error> {
134+
let validate = |op: T| op.validate(head_state, spec);
135+
self.verify_and_observe_parametric(op, validate, head_state, spec)
136+
}
137+
124138
/// Reset the cache when crossing a fork boundary.
125139
///
126140
/// 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> {
140154
}
141155
}
142156
}
157+
158+
impl<T: ObservableOperation<E> + VerifyOperationAt<E>, E: EthSpec> ObservedOperations<T, E> {
159+
pub fn verify_and_observe_at(
160+
&mut self,
161+
op: T,
162+
verify_at_epoch: Epoch,
163+
head_state: &BeaconState<E>,
164+
spec: &ChainSpec,
165+
) -> Result<ObservationOutcome<T, E>, T::Error> {
166+
let validate = |op: T| op.validate_at(head_state, verify_at_epoch, spec);
167+
self.verify_and_observe_parametric(op, validate, head_state, spec)
168+
}
169+
}

beacon_node/operation_pool/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,8 @@ impl<T: EthSpec> OperationPool<T> {
497497
|exit| {
498498
filter(exit.as_inner())
499499
&& exit.signature_is_still_valid(&state.fork())
500-
&& verify_exit(state, exit.as_inner(), VerifySignatures::False, spec).is_ok()
500+
&& verify_exit(state, None, exit.as_inner(), VerifySignatures::False, spec)
501+
.is_ok()
501502
},
502503
|exit| exit.as_inner().clone(),
503504
T::MaxVoluntaryExits::to_usize(),

consensus/state_processing/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,4 @@ pub use per_epoch_processing::{
4141
errors::EpochProcessingError, process_epoch as per_epoch_processing,
4242
};
4343
pub use per_slot_processing::{per_slot_processing, Error as SlotProcessingError};
44-
pub use verify_operation::{SigVerifiedOp, VerifyOperation};
44+
pub use verify_operation::{SigVerifiedOp, VerifyOperation, VerifyOperationAt};

consensus/state_processing/src/per_block_processing/process_operations.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,8 @@ pub fn process_exits<T: EthSpec>(
282282
// Verify and apply each exit in series. We iterate in series because higher-index exits may
283283
// become invalid due to the application of lower-index ones.
284284
for (i, exit) in voluntary_exits.iter().enumerate() {
285-
verify_exit(state, exit, verify_signatures, spec).map_err(|e| e.into_with_index(i))?;
285+
verify_exit(state, None, exit, verify_signatures, spec)
286+
.map_err(|e| e.into_with_index(i))?;
286287

287288
initiate_validator_exit(state, exit.message.validator_index as usize, spec)?;
288289
}

consensus/state_processing/src/per_block_processing/tests.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -978,8 +978,14 @@ async fn fork_spanning_exit() {
978978
let head = harness.chain.canonical_head.cached_head();
979979
let head_state = &head.snapshot.beacon_state;
980980
assert!(head_state.current_epoch() < spec.altair_fork_epoch.unwrap());
981-
verify_exit(head_state, &signed_exit, VerifySignatures::True, &spec)
982-
.expect("phase0 exit verifies against phase0 state");
981+
verify_exit(
982+
head_state,
983+
None,
984+
&signed_exit,
985+
VerifySignatures::True,
986+
&spec,
987+
)
988+
.expect("phase0 exit verifies against phase0 state");
983989

984990
/*
985991
* Ensure the exit verifies after Altair.
@@ -992,8 +998,14 @@ async fn fork_spanning_exit() {
992998
let head_state = &head.snapshot.beacon_state;
993999
assert!(head_state.current_epoch() >= spec.altair_fork_epoch.unwrap());
9941000
assert!(head_state.current_epoch() < spec.bellatrix_fork_epoch.unwrap());
995-
verify_exit(head_state, &signed_exit, VerifySignatures::True, &spec)
996-
.expect("phase0 exit verifies against altair state");
1001+
verify_exit(
1002+
head_state,
1003+
None,
1004+
&signed_exit,
1005+
VerifySignatures::True,
1006+
&spec,
1007+
)
1008+
.expect("phase0 exit verifies against altair state");
9971009

9981010
/*
9991011
* Ensure the exit no longer verifies after Bellatrix.
@@ -1009,6 +1021,12 @@ async fn fork_spanning_exit() {
10091021
let head = harness.chain.canonical_head.cached_head();
10101022
let head_state = &head.snapshot.beacon_state;
10111023
assert!(head_state.current_epoch() >= spec.bellatrix_fork_epoch.unwrap());
1012-
verify_exit(head_state, &signed_exit, VerifySignatures::True, &spec)
1013-
.expect_err("phase0 exit does not verify against bellatrix state");
1024+
verify_exit(
1025+
head_state,
1026+
None,
1027+
&signed_exit,
1028+
VerifySignatures::True,
1029+
&spec,
1030+
)
1031+
.expect_err("phase0 exit does not verify against bellatrix state");
10141032
}

consensus/state_processing/src/per_block_processing/verify_exit.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ fn error(reason: ExitInvalid) -> BlockOperationError<ExitInvalid> {
2020
/// Spec v0.12.1
2121
pub fn verify_exit<T: EthSpec>(
2222
state: &BeaconState<T>,
23+
current_epoch: Option<Epoch>,
2324
signed_exit: &SignedVoluntaryExit,
2425
verify_signatures: VerifySignatures,
2526
spec: &ChainSpec,
2627
) -> Result<()> {
28+
let current_epoch = current_epoch.unwrap_or(state.current_epoch());
2729
let exit = &signed_exit.message;
2830

2931
let validator = state
@@ -33,7 +35,7 @@ pub fn verify_exit<T: EthSpec>(
3335

3436
// Verify the validator is active.
3537
verify!(
36-
validator.is_active_at(state.current_epoch()),
38+
validator.is_active_at(current_epoch),
3739
ExitInvalid::NotActive(exit.validator_index)
3840
);
3941

@@ -45,9 +47,9 @@ pub fn verify_exit<T: EthSpec>(
4547

4648
// Exits must specify an epoch when they become valid; they are not valid before then.
4749
verify!(
48-
state.current_epoch() >= exit.epoch,
50+
current_epoch >= exit.epoch,
4951
ExitInvalid::FutureEpoch {
50-
state: state.current_epoch(),
52+
state: current_epoch,
5153
exit: exit.epoch
5254
}
5355
);
@@ -57,9 +59,9 @@ pub fn verify_exit<T: EthSpec>(
5759
.activation_epoch
5860
.safe_add(spec.shard_committee_period)?;
5961
verify!(
60-
state.current_epoch() >= earliest_exit_epoch,
62+
current_epoch >= earliest_exit_epoch,
6163
ExitInvalid::TooYoungToExit {
62-
current_epoch: state.current_epoch(),
64+
current_epoch,
6365
earliest_exit_epoch,
6466
}
6567
);

consensus/state_processing/src/verify_operation.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ impl<E: EthSpec> VerifyOperation<E> for SignedVoluntaryExit {
134134
state: &BeaconState<E>,
135135
spec: &ChainSpec,
136136
) -> Result<SigVerifiedOp<Self, E>, Self::Error> {
137-
verify_exit(state, &self, VerifySignatures::True, spec)?;
137+
verify_exit(state, None, &self, VerifySignatures::True, spec)?;
138138
Ok(SigVerifiedOp::new(self, state))
139139
}
140140

@@ -205,3 +205,35 @@ impl<E: EthSpec> VerifyOperation<E> for SignedBlsToExecutionChange {
205205
smallvec![]
206206
}
207207
}
208+
209+
/// Trait for operations that can be verified and transformed into a
210+
/// `SigVerifiedOp`.
211+
///
212+
/// The `At` suffix indicates that we can specify a particular epoch at which to
213+
/// verify the operation.
214+
pub trait VerifyOperationAt<E: EthSpec>: VerifyOperation<E> + Sized {
215+
fn validate_at(
216+
self,
217+
state: &BeaconState<E>,
218+
validate_at_epoch: Epoch,
219+
spec: &ChainSpec,
220+
) -> Result<SigVerifiedOp<Self, E>, Self::Error>;
221+
}
222+
223+
impl<E: EthSpec> VerifyOperationAt<E> for SignedVoluntaryExit {
224+
fn validate_at(
225+
self,
226+
state: &BeaconState<E>,
227+
validate_at_epoch: Epoch,
228+
spec: &ChainSpec,
229+
) -> Result<SigVerifiedOp<Self, E>, Self::Error> {
230+
verify_exit(
231+
state,
232+
Some(validate_at_epoch),
233+
&self,
234+
VerifySignatures::True,
235+
spec,
236+
)?;
237+
Ok(SigVerifiedOp::new(self, state))
238+
}
239+
}

0 commit comments

Comments
 (0)