mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-23 06:44:35 +00:00
Fix bugs in proposer calculation post-Fulu (#8101)
As identified by a researcher during the Fusaka security competition, we were computing the proposer index incorrectly in some places by computing without lookahead. - [x] Add "low level" checks to computation functions in `consensus/types` to ensure they error cleanly - [x] Re-work the determination of proposer shuffling decision roots, which are now fork aware. - [x] Re-work and simplify the beacon proposer cache to be fork-aware. - [x] Optimise `with_proposer_cache` to use `OnceCell`. - [x] All tests passing. - [x] Resolve all remaining `FIXME(sproul)`s. - [x] Unit tests for `ProtoBlock::proposer_shuffling_root_for_child_block`. - [x] End-to-end regression test. - [x] Test on pre-Fulu network. - [x] Test on post-Fulu network. Co-Authored-By: Michael Sproul <michael@sigmaprime.io>
This commit is contained in:
@@ -12,9 +12,9 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes};
|
||||
use fork_choice::ExecutionStatus;
|
||||
use lru::LruCache;
|
||||
use once_cell::sync::OnceCell;
|
||||
use safe_arith::SafeArith;
|
||||
use smallvec::SmallVec;
|
||||
use state_processing::state_advance::partial_state_advance;
|
||||
use std::cmp::Ordering;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
use types::non_zero_usize::new_non_zero_usize;
|
||||
@@ -51,6 +51,34 @@ pub struct EpochBlockProposers {
|
||||
pub(crate) proposers: SmallVec<[usize; TYPICAL_SLOTS_PER_EPOCH]>,
|
||||
}
|
||||
|
||||
impl EpochBlockProposers {
|
||||
pub fn new(epoch: Epoch, fork: Fork, proposers: Vec<usize>) -> Self {
|
||||
Self {
|
||||
epoch,
|
||||
fork,
|
||||
proposers: proposers.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_slot<E: EthSpec>(&self, slot: Slot) -> Result<Proposer, BeaconChainError> {
|
||||
let epoch = slot.epoch(E::slots_per_epoch());
|
||||
if epoch == self.epoch {
|
||||
self.proposers
|
||||
.get(slot.as_usize() % E::SlotsPerEpoch::to_usize())
|
||||
.map(|&index| Proposer {
|
||||
index,
|
||||
fork: self.fork,
|
||||
})
|
||||
.ok_or(BeaconChainError::ProposerCacheOutOfBounds { slot, epoch })
|
||||
} else {
|
||||
Err(BeaconChainError::ProposerCacheWrongEpoch {
|
||||
request_epoch: epoch,
|
||||
cache_epoch: self.epoch,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A cache to store the proposers for some epoch.
|
||||
///
|
||||
/// See the module-level documentation for more information.
|
||||
@@ -76,23 +104,8 @@ impl BeaconProposerCache {
|
||||
) -> Option<Proposer> {
|
||||
let epoch = slot.epoch(E::slots_per_epoch());
|
||||
let key = (epoch, shuffling_decision_block);
|
||||
let cache_opt = self.cache.get(&key).and_then(|cell| cell.get());
|
||||
if let Some(cache) = cache_opt {
|
||||
// This `if` statement is likely unnecessary, but it feels like good practice.
|
||||
if epoch == cache.epoch {
|
||||
cache
|
||||
.proposers
|
||||
.get(slot.as_usize() % E::SlotsPerEpoch::to_usize())
|
||||
.map(|&index| Proposer {
|
||||
index,
|
||||
fork: cache.fork,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let cache = self.cache.get(&key)?.get()?;
|
||||
cache.get_slot::<E>(slot).ok()
|
||||
}
|
||||
|
||||
/// As per `Self::get_slot`, but returns all proposers in all slots for the given `epoch`.
|
||||
@@ -142,11 +155,7 @@ impl BeaconProposerCache {
|
||||
) -> Result<(), BeaconStateError> {
|
||||
let key = (epoch, shuffling_decision_block);
|
||||
if !self.cache.contains(&key) {
|
||||
let epoch_proposers = EpochBlockProposers {
|
||||
epoch,
|
||||
fork,
|
||||
proposers: proposers.into(),
|
||||
};
|
||||
let epoch_proposers = EpochBlockProposers::new(epoch, fork, proposers);
|
||||
self.cache
|
||||
.put(key, Arc::new(OnceCell::with_value(epoch_proposers)));
|
||||
}
|
||||
@@ -178,7 +187,12 @@ pub fn compute_proposer_duties_from_head<T: BeaconChainTypes>(
|
||||
.ok_or(BeaconChainError::HeadMissingFromForkChoice(head_block_root))?;
|
||||
|
||||
// Advance the state into the requested epoch.
|
||||
ensure_state_is_in_epoch(&mut state, head_state_root, request_epoch, &chain.spec)?;
|
||||
ensure_state_can_determine_proposers_for_epoch(
|
||||
&mut state,
|
||||
head_state_root,
|
||||
request_epoch,
|
||||
&chain.spec,
|
||||
)?;
|
||||
|
||||
let indices = state
|
||||
.get_beacon_proposer_indices(request_epoch, &chain.spec)
|
||||
@@ -186,13 +200,13 @@ pub fn compute_proposer_duties_from_head<T: BeaconChainTypes>(
|
||||
|
||||
let dependent_root = state
|
||||
// The only block which decides its own shuffling is the genesis block.
|
||||
.proposer_shuffling_decision_root(chain.genesis_block_root)
|
||||
.proposer_shuffling_decision_root(chain.genesis_block_root, &chain.spec)
|
||||
.map_err(BeaconChainError::from)?;
|
||||
|
||||
Ok((indices, dependent_root, execution_status, state.fork()))
|
||||
}
|
||||
|
||||
/// If required, advance `state` to `target_epoch`.
|
||||
/// If required, advance `state` to the epoch required to determine proposer indices in `target_epoch`.
|
||||
///
|
||||
/// ## Details
|
||||
///
|
||||
@@ -200,22 +214,33 @@ pub fn compute_proposer_duties_from_head<T: BeaconChainTypes>(
|
||||
/// - No-op if `state.current_epoch() == target_epoch`.
|
||||
/// - It must be the case that `state.canonical_root() == state_root`, but this function will not
|
||||
/// check that.
|
||||
pub fn ensure_state_is_in_epoch<E: EthSpec>(
|
||||
pub fn ensure_state_can_determine_proposers_for_epoch<E: EthSpec>(
|
||||
state: &mut BeaconState<E>,
|
||||
state_root: Hash256,
|
||||
target_epoch: Epoch,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<(), BeaconChainError> {
|
||||
match state.current_epoch().cmp(&target_epoch) {
|
||||
// Protects against an inconsistent slot clock.
|
||||
Ordering::Greater => Err(BeaconStateError::SlotOutOfBounds.into()),
|
||||
// The state needs to be advanced.
|
||||
Ordering::Less => {
|
||||
let target_slot = target_epoch.start_slot(E::slots_per_epoch());
|
||||
partial_state_advance(state, Some(state_root), target_slot, spec)
|
||||
.map_err(BeaconChainError::from)
|
||||
}
|
||||
// The state is suitable, nothing to do.
|
||||
Ordering::Equal => Ok(()),
|
||||
// The decision slot is the end of an epoch, so we add 1 to reach the first slot of the epoch
|
||||
// at which the shuffling is determined.
|
||||
let minimum_slot = spec
|
||||
.proposer_shuffling_decision_slot::<E>(target_epoch)
|
||||
.safe_add(1)?;
|
||||
let minimum_epoch = minimum_slot.epoch(E::slots_per_epoch());
|
||||
|
||||
// Before and after Fulu, the oldest epoch reachable from a state at epoch N is epoch N itself,
|
||||
// i.e. we can never "look back".
|
||||
let maximum_epoch = target_epoch;
|
||||
|
||||
if state.current_epoch() > maximum_epoch {
|
||||
Err(BeaconStateError::SlotOutOfBounds.into())
|
||||
} else if state.current_epoch() >= minimum_epoch {
|
||||
// Fulu allows us to access shufflings in multiple epochs (thanks to lookahead).
|
||||
// Pre-Fulu we expect `minimum_epoch == maximum_epoch`, and this branch covers that case.
|
||||
Ok(())
|
||||
} else {
|
||||
// State's current epoch is less than the minimum epoch.
|
||||
// Advance the state up to the minimum epoch.
|
||||
partial_state_advance(state, Some(state_root), minimum_slot, spec)
|
||||
.map_err(BeaconChainError::from)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user