Fork choice modifications and cleanup (#3962)

## Issue Addressed

NA

## Proposed Changes

- Implements https://github.com/ethereum/consensus-specs/pull/3290/
- Bumps `ef-tests` to [v1.3.0-rc.4](https://github.com/ethereum/consensus-spec-tests/releases/tag/v1.3.0-rc.4).

The `CountRealizedFull` concept has been removed and the `--count-unrealized-full` and `--count-unrealized` BN flags now do nothing but log a `WARN` when used.

## Database Migration Debt

This PR removes the `best_justified_checkpoint` from fork choice. This field is persisted on-disk and the correct way to go about this would be to make a DB migration to remove the field. However, in this PR I've simply stubbed out the value with a junk value. I've taken this approach because if we're going to do a DB migration I'd love to remove the `Option`s around the justified and finalized checkpoints on `ProtoNode` whilst we're at it. Those options were added in #2822 which was included in Lighthouse v2.1.0. The options were only put there to handle the migration and they've been set to `Some` ever since v2.1.0. There's no reason to keep them as options anymore.

I started adding the DB migration to this branch but I started to feel like I was bloating this rather critical PR with nice-to-haves. I've kept the partially-complete migration [over in my repo](https://github.com/paulhauner/lighthouse/tree/fc-pr-18-migration) so we can pick it up after this PR is merged.
This commit is contained in:
Paul Hauner
2023-03-21 07:34:41 +00:00
parent 3ac5583cf9
commit 1f8c17b530
29 changed files with 217 additions and 414 deletions

View File

@@ -1,6 +1,6 @@
use crate::{ForkChoiceStore, InvalidationOperation};
use proto_array::{
Block as ProtoBlock, CountUnrealizedFull, ExecutionStatus, ProposerHeadError, ProposerHeadInfo,
Block as ProtoBlock, ExecutionStatus, ProposerHeadError, ProposerHeadInfo,
ProtoArrayForkChoice, ReOrgThreshold,
};
use slog::{crit, debug, warn, Logger};
@@ -187,51 +187,6 @@ impl CountUnrealized {
pub fn is_true(&self) -> bool {
matches!(self, CountUnrealized::True)
}
pub fn and(&self, other: CountUnrealized) -> CountUnrealized {
if self.is_true() && other.is_true() {
CountUnrealized::True
} else {
CountUnrealized::False
}
}
}
impl From<bool> for CountUnrealized {
fn from(count_unrealized: bool) -> Self {
if count_unrealized {
CountUnrealized::True
} else {
CountUnrealized::False
}
}
}
#[derive(Copy, Clone)]
enum UpdateJustifiedCheckpointSlots {
OnTick {
current_slot: Slot,
},
OnBlock {
state_slot: Slot,
current_slot: Slot,
},
}
impl UpdateJustifiedCheckpointSlots {
fn current_slot(&self) -> Slot {
match self {
UpdateJustifiedCheckpointSlots::OnTick { current_slot } => *current_slot,
UpdateJustifiedCheckpointSlots::OnBlock { current_slot, .. } => *current_slot,
}
}
fn state_slot(&self) -> Option<Slot> {
match self {
UpdateJustifiedCheckpointSlots::OnTick { .. } => None,
UpdateJustifiedCheckpointSlots::OnBlock { state_slot, .. } => Some(*state_slot),
}
}
}
/// Indicates if a block has been verified by an execution payload.
@@ -393,7 +348,6 @@ where
anchor_block: &SignedBeaconBlock<E>,
anchor_state: &BeaconState<E>,
current_slot: Option<Slot>,
count_unrealized_full_config: CountUnrealizedFull,
spec: &ChainSpec,
) -> Result<Self, Error<T::Error>> {
// Sanity check: the anchor must lie on an epoch boundary.
@@ -440,7 +394,6 @@ where
current_epoch_shuffling_id,
next_epoch_shuffling_id,
execution_status,
count_unrealized_full_config,
)?;
let mut fork_choice = Self {
@@ -533,7 +486,7 @@ where
// Provide the slot (as per the system clock) to the `fc_store` and then return its view of
// the current slot. The `fc_store` will ensure that the `current_slot` is never
// decreasing, a property which we must maintain.
let current_slot = self.update_time(system_time_current_slot, spec)?;
let current_slot = self.update_time(system_time_current_slot)?;
let store = &mut self.fc_store;
@@ -654,58 +607,6 @@ where
}
}
/// Returns `true` if the given `store` should be updated to set
/// `state.current_justified_checkpoint` its `justified_checkpoint`.
///
/// ## Specification
///
/// Is equivalent to:
///
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#should_update_justified_checkpoint
fn should_update_justified_checkpoint(
&mut self,
new_justified_checkpoint: Checkpoint,
slots: UpdateJustifiedCheckpointSlots,
spec: &ChainSpec,
) -> Result<bool, Error<T::Error>> {
self.update_time(slots.current_slot(), spec)?;
if compute_slots_since_epoch_start::<E>(self.fc_store.get_current_slot())
< spec.safe_slots_to_update_justified
{
return Ok(true);
}
let justified_slot =
compute_start_slot_at_epoch::<E>(self.fc_store.justified_checkpoint().epoch);
// This sanity check is not in the spec, but the invariant is implied.
if let Some(state_slot) = slots.state_slot() {
if justified_slot >= state_slot {
return Err(Error::AttemptToRevertJustification {
store: justified_slot,
state: state_slot,
});
}
}
// We know that the slot for `new_justified_checkpoint.root` is not greater than
// `state.slot`, since a state cannot justify its own slot.
//
// We know that `new_justified_checkpoint.root` is an ancestor of `state`, since a `state`
// only ever justifies ancestors.
//
// A prior `if` statement protects against a justified_slot that is greater than
// `state.slot`
let justified_ancestor =
self.get_ancestor(new_justified_checkpoint.root, justified_slot)?;
if justified_ancestor != Some(self.fc_store.justified_checkpoint().root) {
return Ok(false);
}
Ok(true)
}
/// See `ProtoArrayForkChoice::process_execution_payload_validation` for documentation.
pub fn on_valid_execution_payload(
&mut self,
@@ -759,7 +660,7 @@ where
// Provide the slot (as per the system clock) to the `fc_store` and then return its view of
// the current slot. The `fc_store` will ensure that the `current_slot` is never
// decreasing, a property which we must maintain.
let current_slot = self.update_time(system_time_current_slot, spec)?;
let current_slot = self.update_time(system_time_current_slot)?;
// Parent block must be known.
let parent_block = self
@@ -814,17 +715,10 @@ where
self.fc_store.set_proposer_boost_root(block_root);
}
let update_justified_checkpoint_slots = UpdateJustifiedCheckpointSlots::OnBlock {
state_slot: state.slot(),
current_slot,
};
// Update store with checkpoints if necessary
self.update_checkpoints(
state.current_justified_checkpoint(),
state.finalized_checkpoint(),
update_justified_checkpoint_slots,
spec,
)?;
// Update unrealized justified/finalized checkpoints.
@@ -905,12 +799,7 @@ where
// If block is from past epochs, try to update store's justified & finalized checkpoints right away
if block.slot().epoch(E::slots_per_epoch()) < current_slot.epoch(E::slots_per_epoch()) {
self.update_checkpoints(
unrealized_justified_checkpoint,
unrealized_finalized_checkpoint,
update_justified_checkpoint_slots,
spec,
)?;
self.pull_up_store_checkpoints()?;
}
(
@@ -1004,29 +893,19 @@ where
&mut self,
justified_checkpoint: Checkpoint,
finalized_checkpoint: Checkpoint,
slots: UpdateJustifiedCheckpointSlots,
spec: &ChainSpec,
) -> Result<(), Error<T::Error>> {
// Update justified checkpoint.
if justified_checkpoint.epoch > self.fc_store.justified_checkpoint().epoch {
if justified_checkpoint.epoch > self.fc_store.best_justified_checkpoint().epoch {
self.fc_store
.set_best_justified_checkpoint(justified_checkpoint);
}
if self.should_update_justified_checkpoint(justified_checkpoint, slots, spec)? {
self.fc_store
.set_justified_checkpoint(justified_checkpoint)
.map_err(Error::UnableToSetJustifiedCheckpoint)?;
}
self.fc_store
.set_justified_checkpoint(justified_checkpoint)
.map_err(Error::UnableToSetJustifiedCheckpoint)?;
}
// Update finalized checkpoint.
if finalized_checkpoint.epoch > self.fc_store.finalized_checkpoint().epoch {
self.fc_store.set_finalized_checkpoint(finalized_checkpoint);
self.fc_store
.set_justified_checkpoint(justified_checkpoint)
.map_err(Error::UnableToSetJustifiedCheckpoint)?;
}
Ok(())
}
@@ -1167,9 +1046,8 @@ where
system_time_current_slot: Slot,
attestation: &IndexedAttestation<E>,
is_from_block: AttestationFromBlock,
spec: &ChainSpec,
) -> Result<(), Error<T::Error>> {
self.update_time(system_time_current_slot, spec)?;
self.update_time(system_time_current_slot)?;
// Ignore any attestations to the zero hash.
//
@@ -1230,16 +1108,12 @@ where
/// Call `on_tick` for all slots between `fc_store.get_current_slot()` and the provided
/// `current_slot`. Returns the value of `self.fc_store.get_current_slot`.
pub fn update_time(
&mut self,
current_slot: Slot,
spec: &ChainSpec,
) -> Result<Slot, Error<T::Error>> {
pub fn update_time(&mut self, current_slot: Slot) -> Result<Slot, Error<T::Error>> {
while self.fc_store.get_current_slot() < current_slot {
let previous_slot = self.fc_store.get_current_slot();
// Note: we are relying upon `on_tick` to update `fc_store.time` to ensure we don't
// get stuck in a loop.
self.on_tick(previous_slot + 1, spec)?
self.on_tick(previous_slot + 1)?
}
// Process any attestations that might now be eligible.
@@ -1255,7 +1129,7 @@ where
/// Equivalent to:
///
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#on_tick
fn on_tick(&mut self, time: Slot, spec: &ChainSpec) -> Result<(), Error<T::Error>> {
fn on_tick(&mut self, time: Slot) -> Result<(), Error<T::Error>> {
let store = &mut self.fc_store;
let previous_slot = store.get_current_slot();
@@ -1283,26 +1157,21 @@ where
return Ok(());
}
if store.best_justified_checkpoint().epoch > store.justified_checkpoint().epoch {
let store = &self.fc_store;
if self.is_finalized_checkpoint_or_descendant(store.best_justified_checkpoint().root) {
let store = &mut self.fc_store;
store
.set_justified_checkpoint(*store.best_justified_checkpoint())
.map_err(Error::ForkChoiceStoreError)?;
}
}
// Update the justified/finalized checkpoints based upon the
// best-observed unrealized justification/finality.
self.pull_up_store_checkpoints()?;
Ok(())
}
fn pull_up_store_checkpoints(&mut self) -> Result<(), Error<T::Error>> {
// Update store.justified_checkpoint if a better unrealized justified checkpoint is known
let unrealized_justified_checkpoint = *self.fc_store.unrealized_justified_checkpoint();
let unrealized_finalized_checkpoint = *self.fc_store.unrealized_finalized_checkpoint();
self.update_checkpoints(
unrealized_justified_checkpoint,
unrealized_finalized_checkpoint,
UpdateJustifiedCheckpointSlots::OnTick { current_slot },
spec,
)?;
Ok(())
)
}
/// Processes and removes from the queue any queued attestations which may now be eligible for
@@ -1468,16 +1337,6 @@ where
*self.fc_store.justified_checkpoint()
}
/// Return the best justified checkpoint.
///
/// ## Warning
///
/// This is distinct to the "justified checkpoint" or the "current justified checkpoint". This
/// "best justified checkpoint" value should only be used internally or for testing.
pub fn best_justified_checkpoint(&self) -> Checkpoint {
*self.fc_store.best_justified_checkpoint()
}
pub fn unrealized_justified_checkpoint(&self) -> Checkpoint {
*self.fc_store.unrealized_justified_checkpoint()
}
@@ -1538,13 +1397,11 @@ where
pub fn proto_array_from_persisted(
persisted: &PersistedForkChoice,
reset_payload_statuses: ResetPayloadStatuses,
count_unrealized_full: CountUnrealizedFull,
spec: &ChainSpec,
log: &Logger,
) -> Result<ProtoArrayForkChoice, Error<T::Error>> {
let mut proto_array =
ProtoArrayForkChoice::from_bytes(&persisted.proto_array_bytes, count_unrealized_full)
.map_err(Error::InvalidProtoArrayBytes)?;
let mut proto_array = ProtoArrayForkChoice::from_bytes(&persisted.proto_array_bytes)
.map_err(Error::InvalidProtoArrayBytes)?;
let contains_invalid_payloads = proto_array.contains_invalid_payloads();
debug!(
@@ -1575,7 +1432,7 @@ where
"error" => e,
"info" => "please report this error",
);
ProtoArrayForkChoice::from_bytes(&persisted.proto_array_bytes, count_unrealized_full)
ProtoArrayForkChoice::from_bytes(&persisted.proto_array_bytes)
.map_err(Error::InvalidProtoArrayBytes)
} else {
debug!(
@@ -1592,17 +1449,11 @@ where
persisted: PersistedForkChoice,
reset_payload_statuses: ResetPayloadStatuses,
fc_store: T,
count_unrealized_full: CountUnrealizedFull,
spec: &ChainSpec,
log: &Logger,
) -> Result<Self, Error<T::Error>> {
let proto_array = Self::proto_array_from_persisted(
&persisted,
reset_payload_statuses,
count_unrealized_full,
spec,
log,
)?;
let proto_array =
Self::proto_array_from_persisted(&persisted, reset_payload_statuses, spec, log)?;
let current_slot = fc_store.get_current_slot();

View File

@@ -47,9 +47,6 @@ pub trait ForkChoiceStore<T: EthSpec>: Sized {
/// Returns balances from the `state` identified by `justified_checkpoint.root`.
fn justified_balances(&self) -> &JustifiedBalances;
/// Returns the `best_justified_checkpoint`.
fn best_justified_checkpoint(&self) -> &Checkpoint;
/// Returns the `finalized_checkpoint`.
fn finalized_checkpoint(&self) -> &Checkpoint;
@@ -68,9 +65,6 @@ pub trait ForkChoiceStore<T: EthSpec>: Sized {
/// Sets the `justified_checkpoint`.
fn set_justified_checkpoint(&mut self, checkpoint: Checkpoint) -> Result<(), Self::Error>;
/// Sets the `best_justified_checkpoint`.
fn set_best_justified_checkpoint(&mut self, checkpoint: Checkpoint);
/// Sets the `unrealized_justified_checkpoint`.
fn set_unrealized_justified_checkpoint(&mut self, checkpoint: Checkpoint);

View File

@@ -7,6 +7,4 @@ pub use crate::fork_choice::{
PersistedForkChoice, QueuedAttestation, ResetPayloadStatuses,
};
pub use fork_choice_store::ForkChoiceStore;
pub use proto_array::{
Block as ProtoBlock, CountUnrealizedFull, ExecutionStatus, InvalidationOperation,
};
pub use proto_array::{Block as ProtoBlock, ExecutionStatus, InvalidationOperation};

View File

@@ -104,16 +104,6 @@ impl ForkChoiceTest {
self
}
/// Assert the epochs match.
pub fn assert_best_justified_epoch(self, epoch: u64) -> Self {
assert_eq!(
self.get(|fc_store| fc_store.best_justified_checkpoint().epoch),
Epoch::new(epoch),
"best_justified_epoch"
);
self
}
/// Assert the given slot is greater than the head slot.
pub fn assert_finalized_epoch_is_less_than(self, epoch: Epoch) -> Self {
assert!(self.harness.finalized_checkpoint().epoch < epoch);
@@ -151,7 +141,7 @@ impl ForkChoiceTest {
.chain
.canonical_head
.fork_choice_write_lock()
.update_time(self.harness.chain.slot().unwrap(), &self.harness.spec)
.update_time(self.harness.chain.slot().unwrap())
.unwrap();
func(
self.harness
@@ -241,6 +231,11 @@ impl ForkChoiceTest {
///
/// If the chain is presently in an unsafe period, transition through it and the following safe
/// period.
///
/// Note: the `SAFE_SLOTS_TO_UPDATE_JUSTIFIED` variable has been removed
/// from the fork choice spec in Q1 2023. We're still leaving references to
/// it in our tests because (a) it's easier and (b) it allows us to easily
/// test for the absence of that parameter.
pub fn move_to_next_unsafe_period(self) -> Self {
self.move_inside_safe_to_update()
.move_outside_safe_to_update()
@@ -534,7 +529,6 @@ async fn justified_checkpoint_updates_with_descendent_outside_safe_slots() {
.unwrap()
.move_outside_safe_to_update()
.assert_justified_epoch(2)
.assert_best_justified_epoch(2)
.apply_blocks(1)
.await
.assert_justified_epoch(3);
@@ -551,11 +545,9 @@ async fn justified_checkpoint_updates_first_justification_outside_safe_to_update
.unwrap()
.move_to_next_unsafe_period()
.assert_justified_epoch(0)
.assert_best_justified_epoch(0)
.apply_blocks(1)
.await
.assert_justified_epoch(2)
.assert_best_justified_epoch(2);
.assert_justified_epoch(2);
}
/// - The new justified checkpoint **does not** descend from the current.
@@ -583,8 +575,7 @@ async fn justified_checkpoint_updates_with_non_descendent_inside_safe_slots_with
.unwrap();
})
.await
.assert_justified_epoch(3)
.assert_best_justified_epoch(3);
.assert_justified_epoch(3);
}
/// - The new justified checkpoint **does not** descend from the current.
@@ -612,8 +603,9 @@ async fn justified_checkpoint_updates_with_non_descendent_outside_safe_slots_wit
.unwrap();
})
.await
.assert_justified_epoch(2)
.assert_best_justified_epoch(3);
// Now that `SAFE_SLOTS_TO_UPDATE_JUSTIFIED` has been removed, the new
// block should have updated the justified checkpoint.
.assert_justified_epoch(3);
}
/// - The new justified checkpoint **does not** descend from the current.
@@ -641,8 +633,7 @@ async fn justified_checkpoint_updates_with_non_descendent_outside_safe_slots_wit
.unwrap();
})
.await
.assert_justified_epoch(3)
.assert_best_justified_epoch(3);
.assert_justified_epoch(3);
}
/// Check that the balances are obtained correctly.