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,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 = {