Gloas add process_payload_attestation (#8286)

* process_payload_attestation implemented per eip-7732

* allow duplicates in indexed payload attestation indices

* updates per pr review
This commit is contained in:
Shane K Moore
2025-11-10 10:39:19 -08:00
committed by GitHub
parent e354038210
commit 27dce06d25
14 changed files with 523 additions and 38 deletions

View File

@@ -188,6 +188,8 @@ pub enum Error {
ProposerLookaheadOutOfBounds {
i: usize,
},
InvalidIndicesCount,
PleaseNotifyTheDevs(String),
}
/// Control whether an epoch-indexed field can be indexed at the next epoch or not.
@@ -1088,13 +1090,22 @@ impl<E: EthSpec> BeaconState<E> {
}
}
let gloas_enabled = self.fork_name_unchecked().gloas_enabled();
epoch
.slot_iter(E::slots_per_epoch())
.map(|slot| {
let mut preimage = seed.to_vec();
preimage.append(&mut int_to_bytes8(slot.as_u64()));
let seed = hash(&preimage);
self.compute_proposer_index(indices, &seed, spec)
if gloas_enabled {
self.compute_balance_weighted_selection(indices, &seed, 1, true, spec)?
.first()
.copied()
.ok_or(Error::InsufficientValidators)
} else {
self.compute_proposer_index(indices, &seed, spec)
}
})
.collect()
}
@@ -1336,39 +1347,50 @@ impl<E: EthSpec> BeaconState<E> {
let epoch = self.current_epoch().safe_add(1)?;
let active_validator_indices = self.get_active_validator_indices(epoch, spec)?;
let active_validator_count = active_validator_indices.len();
let seed = self.get_seed(epoch, Domain::SyncCommittee, spec)?;
let max_effective_balance = spec.max_effective_balance_for_fork(self.fork_name_unchecked());
let max_random_value = if self.fork_name_unchecked().electra_enabled() {
MAX_RANDOM_VALUE
} else {
MAX_RANDOM_BYTE
};
let mut i = 0;
let mut sync_committee_indices = Vec::with_capacity(E::SyncCommitteeSize::to_usize());
while sync_committee_indices.len() < E::SyncCommitteeSize::to_usize() {
let shuffled_index = compute_shuffled_index(
i.safe_rem(active_validator_count)?,
active_validator_count,
if self.fork_name_unchecked().gloas_enabled() {
self.compute_balance_weighted_selection(
&active_validator_indices,
seed.as_slice(),
spec.shuffle_round_count,
E::SyncCommitteeSize::to_usize(),
true,
spec,
)
.ok_or(Error::UnableToShuffle)?;
let candidate_index = *active_validator_indices
.get(shuffled_index)
.ok_or(Error::ShuffleIndexOutOfBounds(shuffled_index))?;
let random_value = self.shuffling_random_value(i, seed.as_slice())?;
let effective_balance = self.get_validator(candidate_index)?.effective_balance;
if effective_balance.safe_mul(max_random_value)?
>= max_effective_balance.safe_mul(random_value)?
{
sync_committee_indices.push(candidate_index);
} else {
let active_validator_count = active_validator_indices.len();
let max_effective_balance =
spec.max_effective_balance_for_fork(self.fork_name_unchecked());
let max_random_value = if self.fork_name_unchecked().electra_enabled() {
MAX_RANDOM_VALUE
} else {
MAX_RANDOM_BYTE
};
let mut i = 0;
let mut sync_committee_indices = Vec::with_capacity(E::SyncCommitteeSize::to_usize());
while sync_committee_indices.len() < E::SyncCommitteeSize::to_usize() {
let shuffled_index = compute_shuffled_index(
i.safe_rem(active_validator_count)?,
active_validator_count,
seed.as_slice(),
spec.shuffle_round_count,
)
.ok_or(Error::UnableToShuffle)?;
let candidate_index = *active_validator_indices
.get(shuffled_index)
.ok_or(Error::ShuffleIndexOutOfBounds(shuffled_index))?;
let random_value = self.shuffling_random_value(i, seed.as_slice())?;
let effective_balance = self.get_validator(candidate_index)?.effective_balance;
if effective_balance.safe_mul(max_random_value)?
>= max_effective_balance.safe_mul(random_value)?
{
sync_committee_indices.push(candidate_index);
}
i.safe_add_assign(1)?;
}
i.safe_add_assign(1)?;
Ok(sync_committee_indices)
}
Ok(sync_committee_indices)
}
/// Compute the next sync committee.
@@ -2776,6 +2798,114 @@ impl<E: EthSpec> BeaconState<E> {
Ok(())
}
/// Get the PTC
/// Requires the committee cache to be initialized.
/// TODO(EIP-7732): definitely gonna have to cache this..
pub fn get_ptc(&self, slot: Slot, spec: &ChainSpec) -> Result<PTC<E>, Error> {
let committee_cache = self.committee_cache_at_slot(slot)?;
let committees = committee_cache.get_beacon_committees_at_slot(slot)?;
let seed = self.get_ptc_attester_seed(slot, spec)?;
let committee_indices: Vec<usize> = committees
.iter()
.flat_map(|committee| committee.committee.iter().copied())
.collect();
let selected_indices = self.compute_balance_weighted_selection(
&committee_indices,
&seed,
E::ptc_size(),
false,
spec,
)?;
Ok(PTC(FixedVector::new(selected_indices)?))
}
/// Compute the seed to use for the ptc attester selection at the given `slot`.
///
/// Spec v0.12.1
pub fn get_ptc_attester_seed(&self, slot: Slot, spec: &ChainSpec) -> Result<Vec<u8>, Error> {
let epoch = slot.epoch(E::slots_per_epoch());
let mut preimage = self
.get_seed(epoch, Domain::PTCAttester, spec)?
.as_slice()
.to_vec();
preimage.append(&mut int_to_bytes8(slot.as_u64()));
Ok(hash(&preimage))
}
/// Return size indices sampled by effective balance, using indices as candidates.
///
/// If shuffle_indices is True, candidate indices are themselves sampled from indices
/// by shuffling it, otherwise indices is traversed in order.
fn compute_balance_weighted_selection(
&self,
indices: &[usize],
seed: &[u8],
size: usize,
shuffle_indices: bool,
spec: &ChainSpec,
) -> Result<Vec<usize>, Error> {
let total = indices.len();
if total == 0 {
return Err(Error::InvalidIndicesCount);
}
let mut selected = Vec::with_capacity(size);
let mut count = 0usize;
while selected.len() < size {
let mut next_index = count.safe_rem(total)?;
if shuffle_indices {
next_index =
compute_shuffled_index(next_index, total, seed, spec.shuffle_round_count)
.ok_or(Error::UnableToShuffle)?;
}
let candidate_index = indices.get(next_index).ok_or(Error::InvalidIndicesCount)?;
if self.compute_balance_weighted_acceptance(*candidate_index, seed, count, spec)? {
selected.push(*candidate_index);
}
count.safe_add_assign(1)?;
}
Ok(selected)
}
/// Return whether to accept the selection of the validator `index`, with probability
/// proportional to its `effective_balance`, and randomness given by `seed` and `iteration`.
fn compute_balance_weighted_acceptance(
&self,
index: usize,
seed: &[u8],
iteration: usize,
spec: &ChainSpec,
) -> Result<bool, Error> {
// TODO(EIP-7732): Consider grabbing effective balances from the epoch cache here.
// Note that this function will be used in a loop, so using cached values could be nice for performance.
// However, post-gloas, this function will be used in `compute_proposer_indices`, `get_next_sync_committee_indices`, and `get_ptc`, which has ~15 call sites in total
// so we will need to check each one to ensure epoch cache is initialized first, if we deem a good idea.
// Currently, we can't test if making the change would work since the test suite is not ready for gloas.
let effective_balance = self.get_effective_balance(index)?;
let max_effective_balance = spec.max_effective_balance_for_fork(self.fork_name_unchecked());
let random_value = self.shuffling_random_value(iteration, seed)?;
// this codepath should technically never be hit pre-gloas, but added this defensively
let max_random_value = if self.fork_name_unchecked().electra_enabled() {
MAX_RANDOM_VALUE
} else {
MAX_RANDOM_BYTE
};
Ok(effective_balance.safe_mul(max_random_value)?
>= max_effective_balance.safe_mul(random_value)?)
}
}
impl<E: EthSpec> ForkVersionDecode for BeaconState<E> {

View File

@@ -53,6 +53,16 @@ async fn build_state<E: EthSpec>(validator_count: usize) -> BeaconState<E> {
.head_beacon_state_cloned()
}
/// TODO(EIP-7732): Add tests for PTC (Payload Timeliness Committee) functions:
/// - get_ptc: Test committee selection, size, balance-weighted selection
/// - get_ptc_attester_seed: Test seed generation and determinism
/// - compute_balance_weighted_selection: Test selection algorithm with various balances
/// - compute_balance_weighted_acceptance: Test acceptance probability
/// These tests require being able to build Gloas states with initialized committee caches,
/// which currently fails due to incomplete Gloas block structure as mentioned here:
/// https://github.com/sigp/lighthouse/pull/8273
/// Similar to existing committee_consistency_test suite for get_beacon_committee.
async fn test_beacon_proposer_index<E: EthSpec>() {
let spec = E::default_spec();

View File

@@ -69,6 +69,7 @@ pub mod pending_deposit;
pub mod pending_partial_withdrawal;
pub mod proposer_preparation_data;
pub mod proposer_slashing;
pub mod ptc;
pub mod relative_epoch;
pub mod selection_proof;
pub mod shuffling_id;
@@ -249,6 +250,7 @@ pub use crate::preset::{
};
pub use crate::proposer_preparation_data::ProposerPreparationData;
pub use crate::proposer_slashing::ProposerSlashing;
pub use crate::ptc::PTC;
pub use crate::relative_epoch::{Error as RelativeEpochError, RelativeEpoch};
pub use crate::runtime_fixed_vector::RuntimeFixedVector;
pub use crate::runtime_var_list::RuntimeVariableList;

View File

@@ -0,0 +1,23 @@
use crate::*;
/// TODO(EIP-7732): is it easier to return u64 or usize?
#[derive(Clone, Debug, PartialEq)]
pub struct PTC<E: EthSpec>(pub FixedVector<usize, E::PTCSize>);
impl<'a, E: EthSpec> IntoIterator for &'a PTC<E> {
type Item = &'a usize;
type IntoIter = std::slice::Iter<'a, usize>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl<E: EthSpec> IntoIterator for PTC<E> {
type Item = usize;
type IntoIter = std::vec::IntoIter<usize>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}