Update proposer boost calculation (#9215)

Closes:

- https://github.com/sigp/lighthouse/issues/8689


  - Calculate the proposer index on the canonical chain (from canonical head) at `slot` and plumb it through to fork choice so it can be used to determine whether or not to apply the proposer boost. We use the proposer cache to handle state advances and avoid duplicate work.
- Update our FC tests to use `block.message().proposer_index()` (always pass), we are not attempting to test this feature in those tests. The EF tests use the correct canonical proposer idnex via `on_block`, except for invalid blocks which just auto-pass this check (these blocks get rejected by other checks in `on_block` anyway).


Co-Authored-By: Michael Sproul <michael@sigmaprime.io>
This commit is contained in:
Michael Sproul
2026-04-29 22:19:44 +10:00
committed by GitHub
parent 0e427ab77b
commit f406e9c3fb
6 changed files with 79 additions and 33 deletions

View File

@@ -4175,7 +4175,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
};
// Read the cached head prior to taking the fork choice lock to avoid potential deadlocks.
let old_head_slot = self.canonical_head.cached_head().head_slot();
let cached_head = self.canonical_head.cached_head();
let old_head_slot = cached_head.head_slot();
// Compute the expected proposer for `current_slot` on the canonical chain. This is used by
// `on_block` to gate proposer boost on the block's proposer matching the canonical proposer
// (per spec `update_proposer_boost_root` added in v1.7.0-alpha.5).
let canonical_head_proposer_index =
self.canonical_head_proposer_index(current_slot, &cached_head)?;
// Take an upgradable read lock on fork choice so we can check if this block has already
// been imported. We don't want to repeat work importing a block that is already imported.
@@ -4208,6 +4215,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
block_delay,
&state,
payload_verification_status,
canonical_head_proposer_index,
&self.spec,
)
.map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?;
@@ -4950,6 +4958,42 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
}))
}
/// Compute the expected beacon proposer for `slot` on the canonical chain extending `cached_head`.
///
/// Uses the beacon proposer cache to avoid recomputing the shuffling on every block import.
///
/// This is used by `update_proposer_boost_root` to gate proposer boost on the block's proposer
/// matching the canonical proposer, per consensus-specs v1.7.0-alpha.5.
///
/// This function should never error unless there is some corruption of the head state. If a
/// state advance is needed, it will be handled by the proposer cache.
pub fn canonical_head_proposer_index(
&self,
slot: Slot,
cached_head: &CachedHead<T::EthSpec>,
) -> Result<u64, Error> {
let proposal_epoch = slot.epoch(T::EthSpec::slots_per_epoch());
let head_block_root = cached_head.head_block_root();
let head_state = &cached_head.snapshot.beacon_state;
let shuffling_decision_root = head_state.proposer_shuffling_decision_root_at_epoch(
proposal_epoch,
head_block_root,
&self.spec,
)?;
self.with_proposer_cache::<_, Error>(
shuffling_decision_root,
proposal_epoch,
|proposers| {
proposers
.get_slot::<T::EthSpec>(slot)
.map(|p| p.index as u64)
},
|| Ok((cached_head.head_state_root(), head_state.clone())),
)
}
pub fn get_expected_withdrawals(
&self,
forkchoice_update_params: &ForkchoiceUpdateParameters,

View File

@@ -1093,6 +1093,7 @@ async fn invalid_parent() {
Duration::from_secs(0),
&state,
PayloadVerificationStatus::Optimistic,
block.message().proposer_index(),
&rig.harness.chain.spec,
),
Err(ForkChoiceError::ProtoArrayStringError(message))