mirror of
https://github.com/sigp/lighthouse.git
synced 2026-07-04 13:24:39 +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:
@@ -5,8 +5,9 @@ use crate::attestation_verification::{
|
||||
};
|
||||
use crate::attester_cache::{AttesterCache, AttesterCacheKey};
|
||||
use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckCaches};
|
||||
use crate::beacon_proposer_cache::BeaconProposerCache;
|
||||
use crate::beacon_proposer_cache::compute_proposer_duties_from_head;
|
||||
use crate::beacon_proposer_cache::{
|
||||
BeaconProposerCache, EpochBlockProposers, ensure_state_can_determine_proposers_for_epoch,
|
||||
};
|
||||
use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob};
|
||||
use crate::block_times_cache::BlockTimesCache;
|
||||
use crate::block_verification::POS_PANDA_BANNER;
|
||||
@@ -4698,65 +4699,54 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
|
||||
// Compute the proposer index.
|
||||
let head_epoch = cached_head.head_slot().epoch(T::EthSpec::slots_per_epoch());
|
||||
let shuffling_decision_root = if head_epoch == proposal_epoch {
|
||||
cached_head
|
||||
let shuffling_decision_root = cached_head
|
||||
.snapshot
|
||||
.beacon_state
|
||||
.proposer_shuffling_decision_root(proposer_head)?
|
||||
} else {
|
||||
proposer_head
|
||||
};
|
||||
let cached_proposer = self
|
||||
.beacon_proposer_cache
|
||||
.lock()
|
||||
.get_slot::<T::EthSpec>(shuffling_decision_root, proposal_slot);
|
||||
let proposer_index = if let Some(proposer) = cached_proposer {
|
||||
proposer.index as u64
|
||||
} else {
|
||||
.proposer_shuffling_decision_root_at_epoch(proposal_epoch, proposer_head, &self.spec)?;
|
||||
|
||||
let Some(proposer_index) = self.with_proposer_cache(
|
||||
shuffling_decision_root,
|
||||
proposal_epoch,
|
||||
|proposers| proposers.get_slot::<T::EthSpec>(proposal_slot).map(|p| p.index as u64),
|
||||
|| {
|
||||
if head_epoch + self.config.sync_tolerance_epochs < proposal_epoch {
|
||||
warn!(
|
||||
msg = "this is a non-critical issue that can happen on unhealthy nodes or \
|
||||
networks.",
|
||||
networks",
|
||||
%proposal_epoch,
|
||||
%head_epoch,
|
||||
"Skipping proposer preparation"
|
||||
);
|
||||
|
||||
// Don't skip the head forward more than two epochs. This avoids burdening an
|
||||
// Don't skip the head forward too many epochs. This avoids burdening an
|
||||
// unhealthy node.
|
||||
//
|
||||
// Although this node might miss out on preparing for a proposal, they should still
|
||||
// be able to propose. This will prioritise beacon chain health over efficient
|
||||
// packing of execution blocks.
|
||||
return Ok(None);
|
||||
// Although this node might miss out on preparing for a proposal, they should
|
||||
// still be able to propose. This will prioritise beacon chain health over
|
||||
// efficient packing of execution blocks.
|
||||
Err(Error::SkipProposerPreparation)
|
||||
} else {
|
||||
let head = self.canonical_head.cached_head();
|
||||
Ok((
|
||||
head.head_state_root(),
|
||||
head.snapshot.beacon_state.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
let (proposers, decision_root, _, fork) =
|
||||
compute_proposer_duties_from_head(proposal_epoch, self)?;
|
||||
|
||||
let proposer_offset = (proposal_slot % T::EthSpec::slots_per_epoch()).as_usize();
|
||||
let proposer = *proposers
|
||||
.get(proposer_offset)
|
||||
.ok_or(BeaconChainError::NoProposerForSlot(proposal_slot))?;
|
||||
|
||||
self.beacon_proposer_cache.lock().insert(
|
||||
proposal_epoch,
|
||||
decision_root,
|
||||
proposers,
|
||||
fork,
|
||||
)?;
|
||||
|
||||
// It's possible that the head changes whilst computing these duties. If so, abandon
|
||||
// this routine since the change of head would have also spawned another instance of
|
||||
// this routine.
|
||||
//
|
||||
// Exit now, after updating the cache.
|
||||
if decision_root != shuffling_decision_root {
|
||||
},
|
||||
).map_or_else(|e| {
|
||||
match e {
|
||||
Error::ProposerCacheIncorrectState { .. } => {
|
||||
warn!("Head changed during proposer preparation");
|
||||
return Ok(None);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
proposer as u64
|
||||
Error::SkipProposerPreparation => {
|
||||
// Warning logged for this above.
|
||||
Ok(None)
|
||||
}
|
||||
e => Err(e)
|
||||
}
|
||||
}, |value| Ok(Some(value)))? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Get the `prev_randao` and parent block number.
|
||||
@@ -4916,13 +4906,18 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
|
||||
// Only attempt a re-org if we have a proposer registered for the re-org slot.
|
||||
let proposing_at_re_org_slot = {
|
||||
// The proposer shuffling has the same decision root as the next epoch attestation
|
||||
// shuffling. We know our re-org block is not on the epoch boundary, so it has the
|
||||
// same proposer shuffling as the head (but not necessarily the parent which may lie
|
||||
// in the previous epoch).
|
||||
let shuffling_decision_root = info
|
||||
.head_node
|
||||
.next_epoch_shuffling_id
|
||||
// We know our re-org block is not on the epoch boundary, so it has the same proposer
|
||||
// shuffling as the head (but not necessarily the parent which may lie in the previous
|
||||
// epoch).
|
||||
let shuffling_decision_root = if self
|
||||
.spec
|
||||
.fork_name_at_slot::<T::EthSpec>(re_org_block_slot)
|
||||
.fulu_enabled()
|
||||
{
|
||||
info.head_node.current_epoch_shuffling_id
|
||||
} else {
|
||||
info.head_node.next_epoch_shuffling_id
|
||||
}
|
||||
.shuffling_decision_block;
|
||||
let proposer_index = self
|
||||
.beacon_proposer_cache
|
||||
@@ -6558,6 +6553,70 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_proposer_cache<V, E: From<BeaconChainError> + From<BeaconStateError>>(
|
||||
&self,
|
||||
shuffling_decision_block: Hash256,
|
||||
proposal_epoch: Epoch,
|
||||
accessor: impl Fn(&EpochBlockProposers) -> Result<V, BeaconChainError>,
|
||||
state_provider: impl FnOnce() -> Result<(Hash256, BeaconState<T::EthSpec>), E>,
|
||||
) -> Result<V, E> {
|
||||
let cache_entry = self
|
||||
.beacon_proposer_cache
|
||||
.lock()
|
||||
.get_or_insert_key(proposal_epoch, shuffling_decision_block);
|
||||
|
||||
// If the cache entry is not initialised, run the code to initialise it inside a OnceCell.
|
||||
// This prevents duplication of work across multiple threads.
|
||||
//
|
||||
// If it is already initialised, then `get_or_try_init` will return immediately without
|
||||
// executing the initialisation code at all.
|
||||
let epoch_block_proposers = cache_entry.get_or_try_init(|| {
|
||||
debug!(
|
||||
?shuffling_decision_block,
|
||||
%proposal_epoch,
|
||||
"Proposer shuffling cache miss"
|
||||
);
|
||||
|
||||
// Fetch the state on-demand if the required epoch was missing from the cache.
|
||||
// If the caller wants to not compute the state they must return an error here and then
|
||||
// catch it at the call site.
|
||||
let (state_root, mut state) = state_provider()?;
|
||||
|
||||
// Ensure the state can compute proposer duties for `epoch`.
|
||||
ensure_state_can_determine_proposers_for_epoch(
|
||||
&mut state,
|
||||
state_root,
|
||||
proposal_epoch,
|
||||
&self.spec,
|
||||
)?;
|
||||
|
||||
// Sanity check the state.
|
||||
let latest_block_root = state.get_latest_block_root(state_root);
|
||||
let state_decision_block_root = state.proposer_shuffling_decision_root_at_epoch(
|
||||
proposal_epoch,
|
||||
latest_block_root,
|
||||
&self.spec,
|
||||
)?;
|
||||
if state_decision_block_root != shuffling_decision_block {
|
||||
return Err(Error::ProposerCacheIncorrectState {
|
||||
state_decision_block_root,
|
||||
requested_decision_block_root: shuffling_decision_block,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
let proposers = state.get_beacon_proposer_indices(proposal_epoch, &self.spec)?;
|
||||
Ok::<_, E>(EpochBlockProposers::new(
|
||||
proposal_epoch,
|
||||
state.fork(),
|
||||
proposers,
|
||||
))
|
||||
})?;
|
||||
|
||||
// Run the accessor function on the computed epoch proposers.
|
||||
accessor(epoch_block_proposers).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Runs the `map_fn` with the committee cache for `shuffling_epoch` from the chain with head
|
||||
/// `head_block_root`. The `map_fn` will be supplied two values:
|
||||
///
|
||||
|
||||
@@ -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)
|
||||
// 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)
|
||||
}
|
||||
// The state is suitable, nothing to do.
|
||||
Ordering::Equal => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ use std::sync::Arc;
|
||||
|
||||
use crate::beacon_chain::{BeaconChain, BeaconChainTypes};
|
||||
use crate::block_verification::{
|
||||
BlockSlashInfo, cheap_state_advance_to_obtain_committees, get_validator_pubkey_cache,
|
||||
process_block_slash_info,
|
||||
BlockSlashInfo, get_validator_pubkey_cache, process_block_slash_info,
|
||||
};
|
||||
use crate::kzg_utils::{validate_blob, validate_blobs};
|
||||
use crate::observed_data_sidecars::{ObservationStrategy, Observe};
|
||||
@@ -494,59 +493,31 @@ pub fn validate_blob_sidecar_for_gossip<T: BeaconChainTypes, O: ObservationStrat
|
||||
}
|
||||
|
||||
let proposer_shuffling_root =
|
||||
if parent_block.slot.epoch(T::EthSpec::slots_per_epoch()) == blob_epoch {
|
||||
parent_block
|
||||
.next_epoch_shuffling_id
|
||||
.shuffling_decision_block
|
||||
} else {
|
||||
parent_block.root
|
||||
};
|
||||
parent_block.proposer_shuffling_root_for_child_block(blob_epoch, &chain.spec);
|
||||
|
||||
let proposer_opt = chain
|
||||
.beacon_proposer_cache
|
||||
.lock()
|
||||
.get_slot::<T::EthSpec>(proposer_shuffling_root, blob_slot);
|
||||
|
||||
let (proposer_index, fork) = if let Some(proposer) = proposer_opt {
|
||||
(proposer.index, proposer.fork)
|
||||
} else {
|
||||
let proposer = chain.with_proposer_cache(
|
||||
proposer_shuffling_root,
|
||||
blob_epoch,
|
||||
|proposers| proposers.get_slot::<T::EthSpec>(blob_slot),
|
||||
|| {
|
||||
debug!(
|
||||
%block_root,
|
||||
%blob_index,
|
||||
index = %blob_index,
|
||||
"Proposer shuffling cache miss for blob verification"
|
||||
);
|
||||
let (parent_state_root, mut parent_state) = chain
|
||||
chain
|
||||
.store
|
||||
.get_advanced_hot_state(block_parent_root, blob_slot, parent_block.state_root)
|
||||
.map_err(|e| GossipBlobError::BeaconChainError(Box::new(e.into())))?
|
||||
.ok_or_else(|| {
|
||||
BeaconChainError::DBInconsistent(format!(
|
||||
"Missing state for parent block {block_parent_root:?}",
|
||||
))
|
||||
})?;
|
||||
|
||||
let state = cheap_state_advance_to_obtain_committees::<_, GossipBlobError>(
|
||||
&mut parent_state,
|
||||
Some(parent_state_root),
|
||||
blob_slot,
|
||||
&chain.spec,
|
||||
GossipBlobError::BeaconChainError(Box::new(BeaconChainError::DBInconsistent(
|
||||
format!("Missing state for parent block {block_parent_root:?}",),
|
||||
)))
|
||||
})
|
||||
},
|
||||
)?;
|
||||
|
||||
let epoch = state.current_epoch();
|
||||
let proposers = state.get_beacon_proposer_indices(epoch, &chain.spec)?;
|
||||
let proposer_index = *proposers
|
||||
.get(blob_slot.as_usize() % T::EthSpec::slots_per_epoch() as usize)
|
||||
.ok_or_else(|| BeaconChainError::NoProposerForSlot(blob_slot))?;
|
||||
|
||||
// Prime the proposer shuffling cache with the newly-learned value.
|
||||
chain.beacon_proposer_cache.lock().insert(
|
||||
blob_epoch,
|
||||
proposer_shuffling_root,
|
||||
proposers,
|
||||
state.fork(),
|
||||
)?;
|
||||
(proposer_index, state.fork())
|
||||
};
|
||||
let proposer_index = proposer.index;
|
||||
let fork = proposer.fork;
|
||||
|
||||
// Signature verify the signed block header.
|
||||
let signature_is_valid = {
|
||||
|
||||
@@ -948,61 +948,35 @@ impl<T: BeaconChainTypes> GossipVerifiedBlock<T> {
|
||||
}
|
||||
|
||||
let proposer_shuffling_decision_block =
|
||||
if parent_block.slot.epoch(T::EthSpec::slots_per_epoch()) == block_epoch {
|
||||
parent_block
|
||||
.next_epoch_shuffling_id
|
||||
.shuffling_decision_block
|
||||
} else {
|
||||
parent_block.root
|
||||
};
|
||||
parent_block.proposer_shuffling_root_for_child_block(block_epoch, &chain.spec);
|
||||
|
||||
// We assign to a variable instead of using `if let Some` directly to ensure we drop the
|
||||
// write lock before trying to acquire it again in the `else` clause.
|
||||
let proposer_opt = chain
|
||||
.beacon_proposer_cache
|
||||
.lock()
|
||||
.get_slot::<T::EthSpec>(proposer_shuffling_decision_block, block.slot());
|
||||
let (expected_proposer, fork, parent, block) = if let Some(proposer) = proposer_opt {
|
||||
// The proposer index was cached and we can return it without needing to load the
|
||||
// parent.
|
||||
(proposer.index, proposer.fork, None, block)
|
||||
} else {
|
||||
// The proposer index was *not* cached and we must load the parent in order to determine
|
||||
// the proposer index.
|
||||
let (mut parent, block) = load_parent(block, chain)?;
|
||||
|
||||
debug!(
|
||||
parent_root = ?parent.beacon_block_root,
|
||||
parent_slot = %parent.beacon_block.slot(),
|
||||
?block_root,
|
||||
block_slot = %block.slot(),
|
||||
"Proposer shuffling cache miss"
|
||||
);
|
||||
|
||||
// The state produced is only valid for determining proposer/attester shuffling indices.
|
||||
let state = cheap_state_advance_to_obtain_committees::<_, BlockError>(
|
||||
&mut parent.pre_state,
|
||||
parent.beacon_state_root,
|
||||
block.slot(),
|
||||
&chain.spec,
|
||||
)?;
|
||||
|
||||
let epoch = state.current_epoch();
|
||||
let proposers = state.get_beacon_proposer_indices(epoch, &chain.spec)?;
|
||||
let proposer_index = *proposers
|
||||
.get(block.slot().as_usize() % T::EthSpec::slots_per_epoch() as usize)
|
||||
.ok_or_else(|| BeaconChainError::NoProposerForSlot(block.slot()))?;
|
||||
|
||||
// Prime the proposer shuffling cache with the newly-learned value.
|
||||
chain.beacon_proposer_cache.lock().insert(
|
||||
block_epoch,
|
||||
let block_slot = block.slot();
|
||||
let mut opt_parent = None;
|
||||
let proposer = chain.with_proposer_cache::<_, BlockError>(
|
||||
proposer_shuffling_decision_block,
|
||||
proposers,
|
||||
state.fork(),
|
||||
)?;
|
||||
|
||||
(proposer_index, state.fork(), Some(parent), block)
|
||||
block_epoch,
|
||||
|proposers| proposers.get_slot::<T::EthSpec>(block_slot),
|
||||
|| {
|
||||
// The proposer index was *not* cached and we must load the parent in order to
|
||||
// determine the proposer index.
|
||||
let (mut parent, _) = load_parent(block.clone(), chain)?;
|
||||
let parent_state_root = if let Some(state_root) = parent.beacon_state_root {
|
||||
state_root
|
||||
} else {
|
||||
// This is potentially a little inefficient, although we are likely to need
|
||||
// the state's hash eventually (if the block is valid), and we are also likely
|
||||
// to already have the hash cached (if fetched from the state cache).
|
||||
parent.pre_state.canonical_root()?
|
||||
};
|
||||
let parent_state = parent.pre_state.clone();
|
||||
opt_parent = Some(parent);
|
||||
Ok((parent_state_root, parent_state))
|
||||
},
|
||||
)?;
|
||||
let expected_proposer = proposer.index;
|
||||
let fork = proposer.fork;
|
||||
|
||||
let signature_is_valid = {
|
||||
let pubkey_cache = get_validator_pubkey_cache(chain)?;
|
||||
@@ -1077,7 +1051,7 @@ impl<T: BeaconChainTypes> GossipVerifiedBlock<T> {
|
||||
Ok(Self {
|
||||
block,
|
||||
block_root,
|
||||
parent,
|
||||
parent: opt_parent,
|
||||
consensus_context,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -829,7 +829,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
let head_slot = new_snapshot.beacon_state.slot();
|
||||
let dependent_root = new_snapshot
|
||||
.beacon_state
|
||||
.proposer_shuffling_decision_root(self.genesis_block_root);
|
||||
.attester_shuffling_decision_root(self.genesis_block_root, RelativeEpoch::Next);
|
||||
let prev_dependent_root = new_snapshot
|
||||
.beacon_state
|
||||
.attester_shuffling_decision_root(self.genesis_block_root, RelativeEpoch::Current);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use crate::beacon_proposer_cache::EpochBlockProposers;
|
||||
use crate::block_verification::{
|
||||
BlockSlashInfo, cheap_state_advance_to_obtain_committees, get_validator_pubkey_cache,
|
||||
process_block_slash_info,
|
||||
BlockSlashInfo, get_validator_pubkey_cache, process_block_slash_info,
|
||||
};
|
||||
use crate::kzg_utils::{reconstruct_data_columns, validate_data_columns};
|
||||
use crate::observed_data_sidecars::{ObservationStrategy, Observe};
|
||||
@@ -641,65 +639,34 @@ fn verify_proposer_and_signature<T: BeaconChainTypes>(
|
||||
let block_root = data_column.block_root();
|
||||
let block_parent_root = data_column.block_parent_root();
|
||||
|
||||
let proposer_shuffling_root = if parent_block.slot.epoch(slots_per_epoch) == column_epoch {
|
||||
parent_block
|
||||
.next_epoch_shuffling_id
|
||||
.shuffling_decision_block
|
||||
} else {
|
||||
parent_block.root
|
||||
};
|
||||
let proposer_shuffling_root =
|
||||
parent_block.proposer_shuffling_root_for_child_block(column_epoch, &chain.spec);
|
||||
|
||||
// We lock the cache briefly to get or insert a OnceCell, then drop the lock
|
||||
// before doing proposer shuffling calculation via `OnceCell::get_or_try_init`. This avoids
|
||||
// holding the lock during the computation, while still ensuring the result is cached and
|
||||
// initialised only once.
|
||||
//
|
||||
// This approach exposes the cache internals (`OnceCell` & `EpochBlockProposers`)
|
||||
// as a trade-off for avoiding lock contention.
|
||||
let epoch_proposers_cell = chain
|
||||
.beacon_proposer_cache
|
||||
.lock()
|
||||
.get_or_insert_key(column_epoch, proposer_shuffling_root);
|
||||
|
||||
let epoch_proposers = epoch_proposers_cell.get_or_try_init(move || {
|
||||
let proposer = chain.with_proposer_cache(
|
||||
proposer_shuffling_root,
|
||||
column_epoch,
|
||||
|proposers| proposers.get_slot::<T::EthSpec>(column_slot),
|
||||
|| {
|
||||
debug!(
|
||||
%block_root,
|
||||
index = %column_index,
|
||||
"Proposer shuffling cache miss for column verification"
|
||||
);
|
||||
let (parent_state_root, mut parent_state) = chain
|
||||
chain
|
||||
.store
|
||||
.get_advanced_hot_state(block_parent_root, column_slot, parent_block.state_root)
|
||||
.map_err(|e| GossipDataColumnError::BeaconChainError(Box::new(e.into())))?
|
||||
.ok_or_else(|| {
|
||||
GossipDataColumnError::BeaconChainError(Box::new(
|
||||
BeaconChainError::DBInconsistent(format!(
|
||||
"Missing state for parent block {block_parent_root:?}",
|
||||
)),
|
||||
))
|
||||
})?;
|
||||
|
||||
let state = cheap_state_advance_to_obtain_committees::<_, GossipDataColumnError>(
|
||||
&mut parent_state,
|
||||
Some(parent_state_root),
|
||||
column_slot,
|
||||
&chain.spec,
|
||||
)?;
|
||||
|
||||
let epoch = state.current_epoch();
|
||||
let proposers = state.get_beacon_proposer_indices(epoch, &chain.spec)?;
|
||||
// Prime the proposer shuffling cache with the newly-learned value.
|
||||
Ok::<_, GossipDataColumnError>(EpochBlockProposers {
|
||||
epoch: column_epoch,
|
||||
fork: state.fork(),
|
||||
proposers: proposers.into(),
|
||||
})
|
||||
})?;
|
||||
|
||||
let proposer_index = *epoch_proposers
|
||||
.proposers
|
||||
.get(column_slot.as_usize() % slots_per_epoch as usize)
|
||||
.ok_or_else(|| BeaconChainError::NoProposerForSlot(column_slot))?;
|
||||
|
||||
let fork = epoch_proposers.fork;
|
||||
},
|
||||
)?;
|
||||
let proposer_index = proposer.index;
|
||||
let fork = proposer.fork;
|
||||
|
||||
// Signature verify the signed block header.
|
||||
let signature_is_valid = {
|
||||
|
||||
@@ -230,6 +230,23 @@ pub enum BeaconChainError {
|
||||
columns_found: usize,
|
||||
},
|
||||
FailedToReconstructBlobs(String),
|
||||
ProposerCacheIncorrectState {
|
||||
state_decision_block_root: Hash256,
|
||||
requested_decision_block_root: Hash256,
|
||||
},
|
||||
ProposerCacheAccessorFailure {
|
||||
decision_block_root: Hash256,
|
||||
proposal_epoch: Epoch,
|
||||
},
|
||||
ProposerCacheOutOfBounds {
|
||||
slot: Slot,
|
||||
epoch: Epoch,
|
||||
},
|
||||
ProposerCacheWrongEpoch {
|
||||
request_epoch: Epoch,
|
||||
cache_epoch: Epoch,
|
||||
},
|
||||
SkipProposerPreparation,
|
||||
}
|
||||
|
||||
easy_from_to!(SlotProcessingError, BeaconChainError);
|
||||
|
||||
@@ -497,7 +497,7 @@ impl<E: EthSpec> ValidatorMonitor<E> {
|
||||
});
|
||||
|
||||
// Add missed non-finalized blocks for the monitored validators
|
||||
self.add_validators_missed_blocks(state);
|
||||
self.add_validators_missed_blocks(state, spec);
|
||||
self.process_unaggregated_attestations(state, spec);
|
||||
|
||||
// Update metrics for individual validators.
|
||||
@@ -588,7 +588,7 @@ impl<E: EthSpec> ValidatorMonitor<E> {
|
||||
}
|
||||
|
||||
/// Add missed non-finalized blocks for the monitored validators
|
||||
fn add_validators_missed_blocks(&mut self, state: &BeaconState<E>) {
|
||||
fn add_validators_missed_blocks(&mut self, state: &BeaconState<E>, spec: &ChainSpec) {
|
||||
// Define range variables
|
||||
let current_slot = state.slot();
|
||||
let current_epoch = current_slot.epoch(E::slots_per_epoch());
|
||||
@@ -616,8 +616,8 @@ impl<E: EthSpec> ValidatorMonitor<E> {
|
||||
if block_root == prev_block_root {
|
||||
let slot_epoch = slot.epoch(E::slots_per_epoch());
|
||||
|
||||
if let Ok(shuffling_decision_block) =
|
||||
state.proposer_shuffling_decision_root_at_epoch(slot_epoch, *block_root)
|
||||
if let Ok(shuffling_decision_block) = state
|
||||
.proposer_shuffling_decision_root_at_epoch(slot_epoch, *block_root, spec)
|
||||
{
|
||||
// Update the cache if it has not yet been initialised, or if it is
|
||||
// initialised for a prior epoch. This is an optimisation to avoid bouncing
|
||||
|
||||
@@ -1191,6 +1191,271 @@ fn check_shuffling_compatible(
|
||||
}
|
||||
}
|
||||
|
||||
/// These tests check the consistency of:
|
||||
///
|
||||
/// - ProtoBlock::proposer_shuffling_root_for_child_block, and
|
||||
/// - BeaconState::proposer_shuffling_decision_root{_at_epoch}
|
||||
async fn proposer_shuffling_root_consistency_test(parent_slot: u64, child_slot: u64) {
|
||||
let child_slot = Slot::new(child_slot);
|
||||
let db_path = tempdir().unwrap();
|
||||
let store = get_store(&db_path);
|
||||
let validators_keypairs =
|
||||
types::test_utils::generate_deterministic_keypairs(LOW_VALIDATOR_COUNT);
|
||||
let harness = TestHarness::builder(MinimalEthSpec)
|
||||
.default_spec()
|
||||
.keypairs(validators_keypairs)
|
||||
.fresh_disk_store(store)
|
||||
.mock_execution_layer()
|
||||
.build();
|
||||
let spec = &harness.chain.spec;
|
||||
|
||||
// Build chain out to parent block.
|
||||
let initial_slots: Vec<Slot> = (1..=parent_slot).map(Into::into).collect();
|
||||
let (state, state_root) = harness.get_current_state_and_root();
|
||||
let all_validators = harness.get_all_validators();
|
||||
let (_, _, parent_root, _) = harness
|
||||
.add_attested_blocks_at_slots(state, state_root, &initial_slots, &all_validators)
|
||||
.await;
|
||||
|
||||
// Add the child block.
|
||||
let (state, state_root) = harness.get_current_state_and_root();
|
||||
let all_validators = harness.get_all_validators();
|
||||
let (_, _, child_root, child_block_state) = harness
|
||||
.add_attested_blocks_at_slots(state, state_root, &[child_slot], &all_validators)
|
||||
.await;
|
||||
|
||||
let child_block_epoch = child_slot.epoch(E::slots_per_epoch());
|
||||
|
||||
// Load parent block from fork choice.
|
||||
let fc_parent = harness
|
||||
.chain
|
||||
.canonical_head
|
||||
.fork_choice_read_lock()
|
||||
.get_block(&parent_root.into())
|
||||
.unwrap();
|
||||
|
||||
// The proposer shuffling decision root computed using fork choice should equal the root
|
||||
// computed from the child state.
|
||||
let decision_root = fc_parent.proposer_shuffling_root_for_child_block(child_block_epoch, spec);
|
||||
|
||||
assert_eq!(
|
||||
decision_root,
|
||||
child_block_state
|
||||
.proposer_shuffling_decision_root(child_root.into(), spec)
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
decision_root,
|
||||
child_block_state
|
||||
.proposer_shuffling_decision_root_at_epoch(child_block_epoch, child_root.into(), spec)
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
// The passed block root argument should be irrelevant for all blocks except the genesis block.
|
||||
assert_eq!(
|
||||
decision_root,
|
||||
child_block_state
|
||||
.proposer_shuffling_decision_root(Hash256::ZERO, spec)
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
decision_root,
|
||||
child_block_state
|
||||
.proposer_shuffling_decision_root_at_epoch(child_block_epoch, Hash256::ZERO, spec)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn proposer_shuffling_root_consistency_same_epoch() {
|
||||
proposer_shuffling_root_consistency_test(32, 39).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn proposer_shuffling_root_consistency_next_epoch() {
|
||||
proposer_shuffling_root_consistency_test(32, 47).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn proposer_shuffling_root_consistency_two_epochs() {
|
||||
proposer_shuffling_root_consistency_test(32, 55).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn proposer_shuffling_changing_with_lookahead() {
|
||||
let initial_blocks = E::slots_per_epoch() * 4 - 1;
|
||||
|
||||
let spec = ForkName::Fulu.make_genesis_spec(E::default_spec());
|
||||
let db_path = tempdir().unwrap();
|
||||
let store = get_store_generic(&db_path, Default::default(), spec.clone());
|
||||
let validators_keypairs =
|
||||
types::test_utils::generate_deterministic_keypairs(LOW_VALIDATOR_COUNT);
|
||||
let harness = TestHarness::builder(MinimalEthSpec)
|
||||
.spec(spec.into())
|
||||
.keypairs(validators_keypairs)
|
||||
.fresh_disk_store(store)
|
||||
.mock_execution_layer()
|
||||
.build();
|
||||
let spec = &harness.chain.spec;
|
||||
|
||||
// Start with some blocks, finishing with one slot before a new epoch.
|
||||
harness.advance_slot();
|
||||
harness
|
||||
.extend_chain(
|
||||
initial_blocks as usize,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
|
||||
let pre_deposit_state = harness.get_current_state();
|
||||
assert_eq!(pre_deposit_state.slot(), initial_blocks);
|
||||
let topup_block_slot = Slot::new(initial_blocks + 1);
|
||||
let validator_to_topup_index = 1;
|
||||
let validator_to_topup = pre_deposit_state
|
||||
.get_validator(validator_to_topup_index)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
// Craft a block with a deposit request and consolidation.
|
||||
// XXX: This is a really nasty way to do this, but we need better test facilities in
|
||||
// MockExecutionLayer to address this.
|
||||
let deposit_request: DepositRequest = DepositRequest {
|
||||
index: pre_deposit_state.eth1_deposit_index(),
|
||||
pubkey: validator_to_topup.pubkey,
|
||||
withdrawal_credentials: validator_to_topup.withdrawal_credentials,
|
||||
amount: 63_000_000_000,
|
||||
signature: SignatureBytes::empty(),
|
||||
};
|
||||
|
||||
let consolidation_request: ConsolidationRequest = ConsolidationRequest {
|
||||
source_address: validator_to_topup
|
||||
.get_execution_withdrawal_address(spec)
|
||||
.unwrap(),
|
||||
source_pubkey: validator_to_topup.pubkey,
|
||||
target_pubkey: validator_to_topup.pubkey,
|
||||
};
|
||||
|
||||
let execution_requests = ExecutionRequests::<E> {
|
||||
deposits: VariableList::new(vec![deposit_request]).unwrap(),
|
||||
withdrawals: vec![].into(),
|
||||
consolidations: VariableList::new(vec![consolidation_request]).unwrap(),
|
||||
};
|
||||
|
||||
let mut block = Box::pin(harness.make_block_with_modifier(
|
||||
pre_deposit_state.clone(),
|
||||
topup_block_slot,
|
||||
|block| *block.body_mut().execution_requests_mut().unwrap() = execution_requests,
|
||||
))
|
||||
.await
|
||||
.0;
|
||||
|
||||
let Err(BlockError::StateRootMismatch {
|
||||
local: true_state_root,
|
||||
..
|
||||
}) = harness
|
||||
.process_block(topup_block_slot, block.0.canonical_root(), block.clone())
|
||||
.await
|
||||
else {
|
||||
panic!("state root should not match due to pending deposits changes/etc");
|
||||
};
|
||||
let mut new_block = block.0.message_fulu().unwrap().clone();
|
||||
new_block.state_root = true_state_root;
|
||||
block.0 = Arc::new(harness.sign_beacon_block(new_block.into(), &pre_deposit_state));
|
||||
|
||||
harness
|
||||
.process_block(topup_block_slot, block.0.canonical_root(), block.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Advance two epochs to finalize the deposit and process it.
|
||||
// Start with just a single epoch advance so we can grab the state one epoch prior to where
|
||||
// we end up.
|
||||
harness.advance_slot();
|
||||
harness
|
||||
.extend_chain(
|
||||
E::slots_per_epoch() as usize,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Grab the epoch start state. This is the state from which the proposers at the next epoch were
|
||||
// computed.
|
||||
let prev_epoch_state = harness.get_current_state();
|
||||
assert_eq!(prev_epoch_state.slot() % E::slots_per_epoch(), 0);
|
||||
|
||||
// The deposit should be pending.
|
||||
let pending_deposits = prev_epoch_state.pending_deposits().unwrap();
|
||||
assert_eq!(pending_deposits.len(), 1, "{pending_deposits:?}");
|
||||
|
||||
// Advance the 2nd epoch to finalize the deposit and process it.
|
||||
harness.advance_slot();
|
||||
harness
|
||||
.extend_chain(
|
||||
E::slots_per_epoch() as usize,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
|
||||
let current_epoch_state = harness.get_current_state();
|
||||
assert_eq!(current_epoch_state.slot() % E::slots_per_epoch(), 0);
|
||||
|
||||
// Deposit is processed!
|
||||
let pending_deposits = current_epoch_state.pending_deposits().unwrap();
|
||||
assert_eq!(pending_deposits.len(), 0, "{pending_deposits:?}");
|
||||
|
||||
let validator = current_epoch_state
|
||||
.get_validator(validator_to_topup_index)
|
||||
.unwrap();
|
||||
assert!(validator.has_compounding_withdrawal_credential(spec));
|
||||
assert_eq!(validator.effective_balance, 95_000_000_000);
|
||||
|
||||
// The shuffling for the current epoch from `prev_epoch_state` should match the shuffling
|
||||
// for the current epoch from `current_epoch_state` because we should be correctly using the
|
||||
// stored lookahead.
|
||||
let current_epoch = current_epoch_state.current_epoch();
|
||||
let proposer_shuffling = prev_epoch_state
|
||||
.get_beacon_proposer_indices(current_epoch, spec)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
proposer_shuffling,
|
||||
current_epoch_state
|
||||
.get_beacon_proposer_indices(current_epoch, spec)
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
// If we bypass the safety checks in `get_proposer_indices`, we should see that the shuffling
|
||||
// differs due to the effective balance change.
|
||||
let unsafe_get_proposer_indices = |state: &BeaconState<E>, epoch| -> Vec<usize> {
|
||||
let indices = state.get_active_validator_indices(epoch, spec).unwrap();
|
||||
let preimage = state.get_seed(epoch, Domain::BeaconProposer, spec).unwrap();
|
||||
epoch
|
||||
.slot_iter(E::slots_per_epoch())
|
||||
.map(|slot| {
|
||||
let mut preimage = preimage.to_vec();
|
||||
preimage.append(&mut int_to_bytes::int_to_bytes8(slot.as_u64()));
|
||||
let seed = ethereum_hashing::hash(&preimage);
|
||||
state.compute_proposer_index(&indices, &seed, spec).unwrap()
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
// The unsafe function is correct when used with lookahead.
|
||||
assert_eq!(
|
||||
unsafe_get_proposer_indices(&prev_epoch_state, current_epoch),
|
||||
proposer_shuffling
|
||||
);
|
||||
|
||||
// Computing the shuffling for current epoch without lookahead is WRONG.
|
||||
assert_ne!(
|
||||
unsafe_get_proposer_indices(¤t_epoch_state, current_epoch),
|
||||
proposer_shuffling,
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure blocks from abandoned forks are pruned from the Hot DB
|
||||
#[tokio::test]
|
||||
async fn prunes_abandoned_fork_between_two_finalized_checkpoints() {
|
||||
|
||||
@@ -3,7 +3,7 @@ use beacon_chain::test_utils::{
|
||||
};
|
||||
use beacon_chain::validator_monitor::{MISSED_BLOCK_LAG_SLOTS, ValidatorMonitorConfig};
|
||||
use std::sync::LazyLock;
|
||||
use types::{Epoch, EthSpec, Keypair, MainnetEthSpec, PublicKeyBytes, Slot};
|
||||
use types::{Epoch, EthSpec, Hash256, Keypair, MainnetEthSpec, PublicKeyBytes, Slot};
|
||||
|
||||
// Should ideally be divisible by 3.
|
||||
pub const VALIDATOR_COUNT: usize = 48;
|
||||
@@ -74,7 +74,7 @@ async fn missed_blocks_across_epochs() {
|
||||
.get_hot_state(state_roots_by_slot[&start_slot])
|
||||
.unwrap();
|
||||
let decision_root = state
|
||||
.proposer_shuffling_decision_root(genesis_block_root)
|
||||
.proposer_shuffling_decision_root(genesis_block_root, &harness.chain.spec)
|
||||
.unwrap();
|
||||
proposer_shuffling_cache
|
||||
.insert(
|
||||
@@ -152,7 +152,7 @@ async fn missed_blocks_basic() {
|
||||
.unwrap();
|
||||
let mut missed_block_proposer = validator_indexes[slot_in_epoch.as_usize()];
|
||||
let mut proposer_shuffling_decision_root = _state
|
||||
.proposer_shuffling_decision_root(duplicate_block_root)
|
||||
.proposer_shuffling_decision_root(duplicate_block_root, &harness1.chain.spec)
|
||||
.unwrap();
|
||||
|
||||
let beacon_proposer_cache = harness1
|
||||
@@ -235,17 +235,20 @@ async fn missed_blocks_basic() {
|
||||
// Let's fill the cache with the proposers for the current epoch
|
||||
// and push the duplicate_block_root to the block_roots vector
|
||||
assert_eq!(
|
||||
beacon_proposer_cache.lock().insert(
|
||||
epoch,
|
||||
duplicate_block_root,
|
||||
validator_indexes.clone(),
|
||||
_state2.fork()
|
||||
),
|
||||
_state2.set_block_root(prev_slot, duplicate_block_root),
|
||||
Ok(())
|
||||
);
|
||||
|
||||
let decision_block_root = _state2
|
||||
.proposer_shuffling_decision_root_at_epoch(epoch, Hash256::ZERO, &harness2.chain.spec)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
_state2.set_block_root(prev_slot, duplicate_block_root),
|
||||
beacon_proposer_cache.lock().insert(
|
||||
epoch,
|
||||
decision_block_root,
|
||||
validator_indexes.clone(),
|
||||
_state2.fork()
|
||||
),
|
||||
Ok(())
|
||||
);
|
||||
|
||||
@@ -326,7 +329,11 @@ async fn missed_blocks_basic() {
|
||||
.unwrap();
|
||||
missed_block_proposer = validator_indexes[slot_in_epoch.as_usize()];
|
||||
proposer_shuffling_decision_root = _state3
|
||||
.proposer_shuffling_decision_root_at_epoch(epoch, duplicate_block_root)
|
||||
.proposer_shuffling_decision_root_at_epoch(
|
||||
epoch,
|
||||
duplicate_block_root,
|
||||
&harness1.chain.spec,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let beacon_proposer_cache = harness3
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
use crate::state_id::StateId;
|
||||
use beacon_chain::{
|
||||
BeaconChain, BeaconChainError, BeaconChainTypes,
|
||||
beacon_proposer_cache::{compute_proposer_duties_from_head, ensure_state_is_in_epoch},
|
||||
beacon_proposer_cache::{
|
||||
compute_proposer_duties_from_head, ensure_state_can_determine_proposers_for_epoch,
|
||||
},
|
||||
};
|
||||
use eth2::types::{self as api_types};
|
||||
use safe_arith::SafeArith;
|
||||
use slot_clock::SlotClock;
|
||||
use std::cmp::Ordering;
|
||||
use tracing::debug;
|
||||
use types::{Epoch, EthSpec, Hash256, Slot};
|
||||
|
||||
@@ -105,36 +106,29 @@ fn try_proposer_duties_from_cache<T: BeaconChainTypes>(
|
||||
let head_decision_root = head
|
||||
.snapshot
|
||||
.beacon_state
|
||||
.proposer_shuffling_decision_root(head_block_root)
|
||||
.proposer_shuffling_decision_root(head_block_root, &chain.spec)
|
||||
.map_err(warp_utils::reject::beacon_state_error)?;
|
||||
let execution_optimistic = chain
|
||||
.is_optimistic_or_invalid_head_block(head_block)
|
||||
.map_err(warp_utils::reject::unhandled_error)?;
|
||||
|
||||
let dependent_root = match head_epoch.cmp(&request_epoch) {
|
||||
// head_epoch == request_epoch
|
||||
Ordering::Equal => head_decision_root,
|
||||
// head_epoch < request_epoch
|
||||
Ordering::Less => head_block_root,
|
||||
// head_epoch > request_epoch
|
||||
Ordering::Greater => {
|
||||
// This code path can't handle requests for past epochs.
|
||||
if head_epoch > request_epoch {
|
||||
return Err(warp_utils::reject::custom_server_error(format!(
|
||||
"head epoch {} is later than request epoch {}",
|
||||
head_epoch, request_epoch
|
||||
"head epoch {head_epoch} is later than request epoch {request_epoch}",
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
chain
|
||||
.beacon_proposer_cache
|
||||
.lock()
|
||||
.get_epoch::<T::EthSpec>(dependent_root, request_epoch)
|
||||
.get_epoch::<T::EthSpec>(head_decision_root, request_epoch)
|
||||
.cloned()
|
||||
.map(|indices| {
|
||||
convert_to_api_response(
|
||||
chain,
|
||||
request_epoch,
|
||||
dependent_root,
|
||||
head_decision_root,
|
||||
execution_optimistic,
|
||||
indices.to_vec(),
|
||||
)
|
||||
@@ -204,11 +198,12 @@ fn compute_historic_proposer_duties<T: BeaconChainTypes>(
|
||||
}
|
||||
};
|
||||
|
||||
let (state, execution_optimistic) =
|
||||
if let Some((state_root, mut state, execution_optimistic)) = state_opt {
|
||||
let (state, execution_optimistic) = if let Some((state_root, mut state, execution_optimistic)) =
|
||||
state_opt
|
||||
{
|
||||
// If we've loaded the head state it might be from a previous epoch, ensure it's in a
|
||||
// suitable epoch.
|
||||
ensure_state_is_in_epoch(&mut state, state_root, epoch, &chain.spec)
|
||||
ensure_state_can_determine_proposers_for_epoch(&mut state, state_root, epoch, &chain.spec)
|
||||
.map_err(warp_utils::reject::unhandled_error)?;
|
||||
(state, execution_optimistic)
|
||||
} else {
|
||||
@@ -234,7 +229,7 @@ fn compute_historic_proposer_duties<T: BeaconChainTypes>(
|
||||
// We can supply the genesis block root as the block root since we know that the only block that
|
||||
// decides its own root is the genesis block.
|
||||
let dependent_root = state
|
||||
.proposer_shuffling_decision_root(chain.genesis_block_root)
|
||||
.proposer_shuffling_decision_root(chain.genesis_block_root, &chain.spec)
|
||||
.map_err(BeaconChainError::from)
|
||||
.map_err(warp_utils::reject::unhandled_error)?;
|
||||
|
||||
|
||||
@@ -160,6 +160,50 @@ pub struct Block {
|
||||
pub unrealized_finalized_checkpoint: Option<Checkpoint>,
|
||||
}
|
||||
|
||||
impl Block {
|
||||
/// Compute the proposer shuffling decision root of a child block in `child_block_epoch`.
|
||||
///
|
||||
/// This function assumes that `child_block_epoch >= self.epoch`. It is the responsibility of
|
||||
/// the caller to check this condition, or else incorrect results will be produced.
|
||||
pub fn proposer_shuffling_root_for_child_block(
|
||||
&self,
|
||||
child_block_epoch: Epoch,
|
||||
spec: &ChainSpec,
|
||||
) -> Hash256 {
|
||||
let block_epoch = self.current_epoch_shuffling_id.shuffling_epoch;
|
||||
|
||||
if !spec.fork_name_at_epoch(child_block_epoch).fulu_enabled() {
|
||||
// Prior to Fulu the proposer shuffling decision root for the current epoch is the same
|
||||
// as the attestation shuffling for the *next* epoch, i.e. it is determined at the start
|
||||
// of the current epoch.
|
||||
if block_epoch == child_block_epoch {
|
||||
self.next_epoch_shuffling_id.shuffling_decision_block
|
||||
} else {
|
||||
// Otherwise, the child block epoch is greater, so its decision root is its parent
|
||||
// root itself (this block's root).
|
||||
self.root
|
||||
}
|
||||
} else {
|
||||
// After Fulu the proposer shuffling is determined with lookahead, so if the block
|
||||
// lies in the same epoch as its parent, its decision root is the same as the
|
||||
// parent's current epoch attester shuffling
|
||||
//
|
||||
// i.e. the block from the end of epoch N - 2.
|
||||
if child_block_epoch == block_epoch {
|
||||
self.current_epoch_shuffling_id.shuffling_decision_block
|
||||
} else if child_block_epoch == block_epoch + 1 {
|
||||
// If the block is the next epoch, then it instead shares its decision root with
|
||||
// the parent's *next epoch* attester shuffling.
|
||||
self.next_epoch_shuffling_id.shuffling_decision_block
|
||||
} else {
|
||||
// The child block lies in the future beyond the lookahead, at the point where this
|
||||
// block (its parent) will be the decision block.
|
||||
self.root
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Vec-wrapper which will grow to match any request.
|
||||
///
|
||||
/// E.g., a `get` or `insert` to an out-of-bounds element will cause the Vec to grow (using
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use crate::common::update_progressive_balances_cache::initialize_progressive_balances_cache;
|
||||
use crate::epoch_cache::initialize_epoch_cache;
|
||||
use tracing::instrument;
|
||||
use types::{
|
||||
BeaconState, ChainSpec, EpochCacheError, EthSpec, FixedBytesExtended, Hash256, RelativeEpoch,
|
||||
};
|
||||
use types::{BeaconState, ChainSpec, EpochCacheError, EthSpec, Hash256, RelativeEpoch};
|
||||
|
||||
/// Mixin trait for the beacon state that provides operations on *all* caches.
|
||||
///
|
||||
@@ -34,8 +32,7 @@ impl<E: EthSpec> AllCaches for BeaconState<E> {
|
||||
|
||||
fn all_caches_built(&self) -> bool {
|
||||
let current_epoch = self.current_epoch();
|
||||
let Ok(epoch_cache_decision_block_root) =
|
||||
self.proposer_shuffling_decision_root(Hash256::zero())
|
||||
let Ok(epoch_cache_decision_block_root) = self.epoch_cache_decision_root(Hash256::ZERO)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -123,7 +123,7 @@ pub fn is_epoch_cache_initialized<E: EthSpec>(
|
||||
let current_epoch = state.current_epoch();
|
||||
let epoch_cache: &EpochCache = state.epoch_cache();
|
||||
let decision_block_root = state
|
||||
.proposer_shuffling_decision_root(Hash256::zero())
|
||||
.epoch_cache_decision_root(Hash256::zero())
|
||||
.map_err(EpochCacheError::BeaconState)?;
|
||||
|
||||
Ok(epoch_cache
|
||||
@@ -146,7 +146,7 @@ pub fn initialize_epoch_cache<E: EthSpec>(
|
||||
let current_epoch = state.current_epoch();
|
||||
let next_epoch = state.next_epoch().map_err(EpochCacheError::BeaconState)?;
|
||||
let decision_block_root = state
|
||||
.proposer_shuffling_decision_root(Hash256::zero())
|
||||
.epoch_cache_decision_root(Hash256::zero())
|
||||
.map_err(EpochCacheError::BeaconState)?;
|
||||
|
||||
state.build_total_active_balance_cache(spec)?;
|
||||
|
||||
@@ -33,9 +33,7 @@ fn initialize_proposer_lookahead<E: EthSpec>(
|
||||
);
|
||||
}
|
||||
|
||||
Vector::new(lookahead).map_err(|e| {
|
||||
Error::PleaseNotifyTheDevs(format!("Failed to initialize proposer lookahead: {:?}", e))
|
||||
})
|
||||
Vector::new(lookahead).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub fn upgrade_state_to_fulu<E: EthSpec>(
|
||||
|
||||
@@ -173,7 +173,21 @@ pub enum Error {
|
||||
AggregatorNotInCommittee {
|
||||
aggregator_index: u64,
|
||||
},
|
||||
PleaseNotifyTheDevs(String),
|
||||
ComputeProposerIndicesPastEpoch {
|
||||
current_epoch: Epoch,
|
||||
request_epoch: Epoch,
|
||||
},
|
||||
ComputeProposerIndicesInsufficientLookahead {
|
||||
current_epoch: Epoch,
|
||||
request_epoch: Epoch,
|
||||
},
|
||||
ComputeProposerIndicesExcessiveLookahead {
|
||||
current_epoch: Epoch,
|
||||
request_epoch: Epoch,
|
||||
},
|
||||
ProposerLookaheadOutOfBounds {
|
||||
i: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// Control whether an epoch-indexed field can be indexed at the next epoch or not.
|
||||
@@ -886,8 +900,9 @@ impl<E: EthSpec> BeaconState<E> {
|
||||
&self,
|
||||
epoch: Epoch,
|
||||
block_root: Hash256,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Hash256, Error> {
|
||||
let decision_slot = self.proposer_shuffling_decision_slot(epoch);
|
||||
let decision_slot = spec.proposer_shuffling_decision_slot::<E>(epoch);
|
||||
if self.slot() <= decision_slot {
|
||||
Ok(block_root)
|
||||
} else {
|
||||
@@ -902,19 +917,18 @@ impl<E: EthSpec> BeaconState<E> {
|
||||
///
|
||||
/// The `block_root` covers the one-off scenario where the genesis block decides its own
|
||||
/// shuffling. It should be set to the latest block applied to `self` or the genesis block root.
|
||||
pub fn proposer_shuffling_decision_root(&self, block_root: Hash256) -> Result<Hash256, Error> {
|
||||
let decision_slot = self.proposer_shuffling_decision_slot(self.current_epoch());
|
||||
if self.slot() == decision_slot {
|
||||
Ok(block_root)
|
||||
} else {
|
||||
self.get_block_root(decision_slot).copied()
|
||||
}
|
||||
pub fn proposer_shuffling_decision_root(
|
||||
&self,
|
||||
block_root: Hash256,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Hash256, Error> {
|
||||
self.proposer_shuffling_decision_root_at_epoch(self.current_epoch(), block_root, spec)
|
||||
}
|
||||
|
||||
/// Returns the slot at which the proposer shuffling was decided. The block root at this slot
|
||||
/// can be used to key the proposer shuffling for the given epoch.
|
||||
fn proposer_shuffling_decision_slot(&self, epoch: Epoch) -> Slot {
|
||||
epoch.start_slot(E::slots_per_epoch()).saturating_sub(1_u64)
|
||||
pub fn epoch_cache_decision_root(&self, block_root: Hash256) -> Result<Hash256, Error> {
|
||||
// Epoch cache decision root for the current epoch (N) is the block root at the end of epoch
|
||||
// N - 1. This is the same as the root that determines the next epoch attester shuffling.
|
||||
self.attester_shuffling_decision_root(block_root, RelativeEpoch::Next)
|
||||
}
|
||||
|
||||
/// Returns the block root which decided the attester shuffling for the given `relative_epoch`.
|
||||
@@ -998,6 +1012,45 @@ impl<E: EthSpec> BeaconState<E> {
|
||||
indices: &[usize],
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Vec<usize>, Error> {
|
||||
// Regardless of fork, we never support computing proposer indices for past epochs.
|
||||
let current_epoch = self.current_epoch();
|
||||
if epoch < current_epoch {
|
||||
return Err(Error::ComputeProposerIndicesPastEpoch {
|
||||
current_epoch,
|
||||
request_epoch: epoch,
|
||||
});
|
||||
}
|
||||
|
||||
if spec.fork_name_at_epoch(epoch).fulu_enabled() {
|
||||
// Post-Fulu we must never compute proposer indices using insufficient lookahead. This
|
||||
// would be very dangerous as it would lead to conflicts between the *true* proposer as
|
||||
// defined by `self.proposer_lookahead` and the output of this function.
|
||||
// With MIN_SEED_LOOKAHEAD=1 (common config), this is equivalent to checking that the
|
||||
// requested epoch is not the current epoch.
|
||||
//
|
||||
// We do not run this check if this function is called from `upgrade_to_fulu`,
|
||||
// which runs *after* the slot is incremented, and needs to compute the proposer
|
||||
// shuffling for the epoch that was just transitioned into.
|
||||
if self.fork_name_unchecked().fulu_enabled()
|
||||
&& epoch < current_epoch.safe_add(spec.min_seed_lookahead)?
|
||||
{
|
||||
return Err(Error::ComputeProposerIndicesInsufficientLookahead {
|
||||
current_epoch,
|
||||
request_epoch: epoch,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Pre-Fulu the situation is reversed, we *should not* compute proposer indices using
|
||||
// too much lookahead. To do so would make us vulnerable to changes in the proposer
|
||||
// indices caused by effective balance changes.
|
||||
if epoch >= current_epoch.safe_add(spec.min_seed_lookahead)? {
|
||||
return Err(Error::ComputeProposerIndicesExcessiveLookahead {
|
||||
current_epoch,
|
||||
request_epoch: epoch,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
epoch
|
||||
.slot_iter(E::slots_per_epoch())
|
||||
.map(|slot| {
|
||||
@@ -1146,10 +1199,7 @@ impl<E: EthSpec> BeaconState<E> {
|
||||
let index = slot.as_usize().safe_rem(E::slots_per_epoch() as usize)?;
|
||||
proposer_lookahead
|
||||
.get(index)
|
||||
.ok_or(Error::PleaseNotifyTheDevs(format!(
|
||||
"Proposer lookahead out of bounds: {} for slot: {}",
|
||||
index, slot
|
||||
)))
|
||||
.ok_or(Error::ProposerLookaheadOutOfBounds { i: index })
|
||||
.map(|index| *index as usize)
|
||||
} else {
|
||||
// Pre-Fulu
|
||||
@@ -1168,6 +1218,25 @@ impl<E: EthSpec> BeaconState<E> {
|
||||
epoch: Epoch,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Vec<usize>, Error> {
|
||||
// This isn't in the spec, but we remove the footgun that is requesting the current epoch
|
||||
// for a Fulu state.
|
||||
if let Ok(proposer_lookahead) = self.proposer_lookahead()
|
||||
&& epoch >= self.current_epoch()
|
||||
&& epoch <= self.next_epoch()?
|
||||
{
|
||||
let slots_per_epoch = E::slots_per_epoch() as usize;
|
||||
let start_offset = if epoch == self.current_epoch() {
|
||||
0
|
||||
} else {
|
||||
slots_per_epoch
|
||||
};
|
||||
return Ok(proposer_lookahead
|
||||
.iter_from(start_offset)?
|
||||
.take(slots_per_epoch)
|
||||
.map(|x| *x as usize)
|
||||
.collect());
|
||||
}
|
||||
|
||||
// Not using the cached validator indices since they are shuffled.
|
||||
let indices = self.get_active_validator_indices(epoch, spec)?;
|
||||
|
||||
|
||||
@@ -865,6 +865,28 @@ impl ChainSpec {
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the slot at which the proposer shuffling was decided.
|
||||
///
|
||||
/// The block root at this slot can be used to key the proposer shuffling for the given epoch.
|
||||
pub fn proposer_shuffling_decision_slot<E: EthSpec>(&self, epoch: Epoch) -> Slot {
|
||||
if self.fork_name_at_epoch(epoch).fulu_enabled() {
|
||||
// Post-Fulu the proposer shuffling decision slot for epoch N is the slot at the end
|
||||
// of epoch N - 2 (note: min_seed_lookahead=1 in all current configs).
|
||||
epoch
|
||||
.saturating_sub(self.min_seed_lookahead)
|
||||
.start_slot(E::slots_per_epoch())
|
||||
.saturating_sub(1_u64)
|
||||
} else {
|
||||
// Pre-Fulu the proposer shuffling decision slot for epoch N is the slot at the end of
|
||||
// epoch N - 1 (note: +1 -1 for min_seed_lookahead=1 in all current configs).
|
||||
epoch
|
||||
.saturating_add(Epoch::new(1))
|
||||
.saturating_sub(self.min_seed_lookahead)
|
||||
.start_slot(E::slots_per_epoch())
|
||||
.saturating_sub(1_u64)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `ChainSpec` compatible with the Ethereum Foundation specification.
|
||||
pub fn mainnet() -> Self {
|
||||
Self {
|
||||
|
||||
@@ -5,9 +5,13 @@ use std::sync::Arc;
|
||||
/// Cache of values which are uniquely determined at the start of an epoch.
|
||||
///
|
||||
/// The values are fixed with respect to the last block of the _prior_ epoch, which we refer
|
||||
/// to as the "decision block". This cache is very similar to the `BeaconProposerCache` in that
|
||||
/// beacon proposers are determined at exactly the same time as the values in this cache, so
|
||||
/// the keys for the two caches are identical.
|
||||
/// to as the "decision block".
|
||||
///
|
||||
/// Prior to Fulu this cache was similar to the `BeaconProposerCache` in that beacon proposers were
|
||||
/// determined at exactly the same time as the values in this cache, so the keys for the two caches
|
||||
/// were identical.
|
||||
///
|
||||
/// Post-Fulu, we use a different key (the proposers have more lookahead).
|
||||
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Default)]
|
||||
pub struct EpochCache {
|
||||
|
||||
@@ -60,7 +60,7 @@ impl<E: EthSpec> Case for ForkTest<E> {
|
||||
fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> {
|
||||
let mut result_state = self.pre.clone();
|
||||
let mut expected = Some(self.post.clone());
|
||||
let spec = &E::default_spec();
|
||||
let spec = &fork_name.make_genesis_spec(E::default_spec());
|
||||
|
||||
let mut result = match fork_name {
|
||||
ForkName::Base => panic!("phase0 not supported"),
|
||||
|
||||
Reference in New Issue
Block a user