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:
Michael Sproul
2025-09-27 00:44:50 +10:00
committed by GitHub
parent 20c6ce4553
commit c754234b2c
19 changed files with 765 additions and 351 deletions

View File

@@ -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
.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 {
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.",
%proposal_epoch,
%head_epoch,
"Skipping proposer preparation"
);
let shuffling_decision_root = cached_head
.snapshot
.beacon_state
.proposer_shuffling_decision_root_at_epoch(proposal_epoch, proposer_head, &self.spec)?;
// Don't skip the head forward more than two 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);
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",
%proposal_epoch,
%head_epoch,
"Skipping proposer preparation"
);
// 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.
Err(Error::SkipProposerPreparation)
} else {
let head = self.canonical_head.cached_head();
Ok((
head.head_state_root(),
head.snapshot.beacon_state.clone(),
))
}
},
).map_or_else(|e| {
match e {
Error::ProposerCacheIncorrectState { .. } => {
warn!("Head changed during proposer preparation");
Ok(None)
}
Error::SkipProposerPreparation => {
// Warning logged for this above.
Ok(None)
}
e => Err(e)
}
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 {
warn!("Head changed during proposer preparation");
return Ok(None);
}
proposer as u64
}, |value| Ok(Some(value)))? else {
return Ok(None);
};
// Get the `prev_randao` and parent block number.
@@ -4916,14 +4906,19 @@ 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
.shuffling_decision_block;
// 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
.lock()
@@ -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:
///

View File

@@ -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)
}
}

View File

@@ -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 {
debug!(
%block_root,
%blob_index,
"Proposer shuffling cache miss for blob verification"
);
let (parent_state_root, mut parent_state) = 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,
)?;
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 = chain.with_proposer_cache(
proposer_shuffling_root,
blob_epoch,
|proposers| proposers.get_slot::<T::EthSpec>(blob_slot),
|| {
debug!(
%block_root,
index = %blob_index,
"Proposer shuffling cache miss for blob verification"
);
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(|| {
GossipBlobError::BeaconChainError(Box::new(BeaconChainError::DBInconsistent(
format!("Missing state for parent block {block_parent_root:?}",),
)))
})
},
)?;
let proposer_index = proposer.index;
let fork = proposer.fork;
// Signature verify the signed block header.
let signature_is_valid = {

View File

@@ -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,
proposer_shuffling_decision_block,
proposers,
state.fork(),
)?;
(proposer_index, state.fork(), Some(parent), block)
};
let block_slot = block.slot();
let mut opt_parent = None;
let proposer = chain.with_proposer_cache::<_, BlockError>(
proposer_shuffling_decision_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,
})
}

View File

@@ -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);

View File

@@ -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 || {
debug!(
%block_root,
index = %column_index,
"Proposer shuffling cache miss for column verification"
);
let (parent_state_root, mut parent_state) = 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(|| {
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 = 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"
);
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 proposer_index = proposer.index;
let fork = proposer.fork;
// Signature verify the signed block header.
let signature_is_valid = {

View File

@@ -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);

View File

@@ -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

View File

@@ -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(&current_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() {

View File

@@ -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

View File

@@ -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 => {
return Err(warp_utils::reject::custom_server_error(format!(
"head epoch {} is later than request epoch {}",
head_epoch, request_epoch
)));
}
};
// 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 {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,18 +198,19 @@ fn compute_historic_proposer_duties<T: BeaconChainTypes>(
}
};
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)
.map_err(warp_utils::reject::unhandled_error)?;
(state, execution_optimistic)
} else {
let (state, execution_optimistic, _finalized) =
StateId::from_slot(epoch.start_slot(T::EthSpec::slots_per_epoch())).state(chain)?;
(state, execution_optimistic)
};
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_can_determine_proposers_for_epoch(&mut state, state_root, epoch, &chain.spec)
.map_err(warp_utils::reject::unhandled_error)?;
(state, execution_optimistic)
} else {
let (state, execution_optimistic, _finalized) =
StateId::from_slot(epoch.start_slot(T::EthSpec::slots_per_epoch())).state(chain)?;
(state, execution_optimistic)
};
// Ensure the state lookup was correct.
if state.current_epoch() != epoch {
@@ -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)?;

View File

@@ -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

View File

@@ -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;
};

View File

@@ -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)?;

View File

@@ -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>(

View File

@@ -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)?;

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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"),