Fix starting-epoch check in doppelganger (#2491)

## Issue Addressed

NA

## Proposed Changes

Fixes a bug in Doppelganger Protection which would cause false-positives when a VC is restarted in the same epoch where it has already produced a signed message.

It could also cause a false-negative in the scenario where time skips forward (perhaps due to host suspend/wake). The new `time_skips_forward_with_doppelgangers` test covers this case.

This was a simple (and embarrassing, on my behalf) `>=` instead of `<=` bug that was missed by my tests but detected during manual testing by @michaelsproul (🙏). Regression tests have been added.

## Additional Info

NA

## TODO

- [x] Add test for doppelganger in epoch > next_check_epoch
This commit is contained in:
Paul Hauner 2021-08-04 00:03:47 +00:00
parent 187425cdc1
commit 6a620a31da

View File

@ -540,7 +540,7 @@ impl DoppelgangerService {
continue; continue;
}; };
if response.is_live && next_check_epoch >= response.epoch { if response.is_live && next_check_epoch <= response.epoch {
violators.push(response.index); violators.push(response.index);
} }
} }
@ -1033,20 +1033,23 @@ mod test {
where where
F: Fn(&mut LivenessResponses), F: Fn(&mut LivenessResponses),
{ {
let epoch = genesis_epoch() + 1; let starting_epoch = genesis_epoch() + 1;
let slot = epoch.start_slot(E::slots_per_epoch()); let starting_slot = starting_epoch.start_slot(E::slots_per_epoch());
let checking_epoch = starting_epoch + 2;
let checking_slot = checking_epoch.start_slot(E::slots_per_epoch());
TestBuilder::default() TestBuilder::default()
.build() .build()
.set_slot(slot) .set_slot(starting_slot)
.register_all_in_doppelganger_protection_if_enabled() .register_all_in_doppelganger_protection_if_enabled()
.assert_all_disabled() .assert_all_disabled()
// First, simulate a check where there are no doppelgangers. // First, simulate a check where there are no doppelgangers.
.simulate_detect_doppelgangers( .simulate_detect_doppelgangers(
slot, checking_slot,
ShouldShutdown::No, ShouldShutdown::No,
|current_epoch, detection_indices: Vec<_>| { |current_epoch, detection_indices: Vec<_>| {
assert_eq!(current_epoch, epoch); assert_eq!(current_epoch, checking_epoch);
check_detection_indices(&detection_indices); check_detection_indices(&detection_indices);
let liveness_responses = get_false_responses(current_epoch, &detection_indices); let liveness_responses = get_false_responses(current_epoch, &detection_indices);
@ -1059,11 +1062,10 @@ mod test {
// Now, simulate a check where we apply `mutate_responses` which *must* create some // Now, simulate a check where we apply `mutate_responses` which *must* create some
// doppelgangers. // doppelgangers.
.simulate_detect_doppelgangers( .simulate_detect_doppelgangers(
// Perform this check in the next slot. checking_slot,
slot + 1,
ShouldShutdown::Yes, ShouldShutdown::Yes,
|current_epoch, detection_indices: Vec<_>| { |current_epoch, detection_indices: Vec<_>| {
assert_eq!(current_epoch, epoch); assert_eq!(current_epoch, checking_epoch);
check_detection_indices(&detection_indices); check_detection_indices(&detection_indices);
let mut liveness_responses = let mut liveness_responses =
@ -1078,7 +1080,7 @@ mod test {
.assert_all_disabled() .assert_all_disabled()
// The states of all validators should be jammed with `u64::max_value()`. // The states of all validators should be jammed with `u64::max_value()`.
.assert_all_states(&DoppelgangerState { .assert_all_states(&DoppelgangerState {
next_check_epoch: epoch + 1, next_check_epoch: starting_epoch + 1,
remaining_epochs: u64::MAX, remaining_epochs: u64::MAX,
}); });
} }
@ -1097,6 +1099,43 @@ mod test {
}) })
} }
#[test]
fn detect_doppelganger_in_starting_epoch() {
let epoch = genesis_epoch() + 1;
let slot = epoch.start_slot(E::slots_per_epoch());
TestBuilder::default()
.build()
.set_slot(slot)
.register_all_in_doppelganger_protection_if_enabled()
.assert_all_disabled()
// First, simulate a check where there is a doppelganger in the starting epoch.
//
// This should *not* cause a shutdown since we don't declare a doppelganger in the
// start-up epoch to be a *real* doppelganger. Doing a fast ctrl+c and restart can cause
// this behaviour.
.simulate_detect_doppelgangers(
slot,
ShouldShutdown::No,
|current_epoch, detection_indices: Vec<_>| {
assert_eq!(current_epoch, epoch);
check_detection_indices(&detection_indices);
let mut liveness_responses =
get_false_responses(current_epoch, &detection_indices);
liveness_responses.previous_epoch_responses[0].is_live = true;
future::ready(liveness_responses)
},
)
.assert_all_disabled()
.assert_all_states(&DoppelgangerState {
next_check_epoch: epoch + 1,
remaining_epochs: DEFAULT_REMAINING_DETECTION_EPOCHS,
});
}
#[test] #[test]
fn no_doppelgangers_for_adequate_time() { fn no_doppelgangers_for_adequate_time() {
let initial_epoch = genesis_epoch() + 42; let initial_epoch = genesis_epoch() + 42;
@ -1169,7 +1208,7 @@ mod test {
} }
#[test] #[test]
fn time_skips_forward() { fn time_skips_forward_no_doppelgangers() {
let initial_epoch = genesis_epoch() + 1; let initial_epoch = genesis_epoch() + 1;
let initial_slot = initial_epoch.start_slot(E::slots_per_epoch()); let initial_slot = initial_epoch.start_slot(E::slots_per_epoch());
let skipped_forward_epoch = initial_epoch + 42; let skipped_forward_epoch = initial_epoch + 42;
@ -1202,6 +1241,7 @@ mod test {
ShouldShutdown::No, ShouldShutdown::No,
|current_epoch, detection_indices: Vec<_>| { |current_epoch, detection_indices: Vec<_>| {
assert_eq!(current_epoch, skipped_forward_epoch); assert_eq!(current_epoch, skipped_forward_epoch);
assert!(!detection_indices.is_empty());
check_detection_indices(&detection_indices); check_detection_indices(&detection_indices);
future::ready(get_false_responses(current_epoch, &detection_indices)) future::ready(get_false_responses(current_epoch, &detection_indices))
@ -1213,6 +1253,56 @@ mod test {
}); });
} }
#[test]
fn time_skips_forward_with_doppelgangers() {
let initial_epoch = genesis_epoch() + 1;
let initial_slot = initial_epoch.start_slot(E::slots_per_epoch());
let skipped_forward_epoch = initial_epoch + 42;
let skipped_forward_slot = skipped_forward_epoch.end_slot(E::slots_per_epoch());
TestBuilder::default()
.build()
.set_slot(initial_slot)
.register_all_in_doppelganger_protection_if_enabled()
.assert_all_disabled()
// First, simulate a check in the initialization epoch.
.simulate_detect_doppelgangers(
initial_slot,
ShouldShutdown::No,
|current_epoch, detection_indices: Vec<_>| {
assert_eq!(current_epoch, initial_epoch);
check_detection_indices(&detection_indices);
future::ready(get_false_responses(current_epoch, &detection_indices))
},
)
.assert_all_disabled()
.assert_all_states(&DoppelgangerState {
next_check_epoch: initial_epoch + 1,
remaining_epochs: DEFAULT_REMAINING_DETECTION_EPOCHS,
})
// Simulate a check in the skipped forward slot
.simulate_detect_doppelgangers(
skipped_forward_slot,
ShouldShutdown::Yes,
|current_epoch, detection_indices: Vec<_>| {
assert_eq!(current_epoch, skipped_forward_epoch);
assert!(!detection_indices.is_empty());
let mut liveness_responses =
get_false_responses(current_epoch, &detection_indices);
liveness_responses.previous_epoch_responses[1].is_live = true;
future::ready(liveness_responses)
},
)
.assert_all_states(&DoppelgangerState {
next_check_epoch: initial_epoch + 1,
remaining_epochs: u64::max_value(),
});
}
#[test] #[test]
fn time_skips_backward() { fn time_skips_backward() {
let initial_epoch = genesis_epoch() + 42; let initial_epoch = genesis_epoch() + 42;