From 3b3a12aacd06db34aa4dc6ebcf2483bec7bfce78 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Sun, 17 Aug 2025 15:37:17 -0700 Subject: [PATCH 1/3] process_payload_attestation implemented per eip-7732 --- beacon_node/store/src/consensus_context.rs | 2 + .../common/get_payload_attesting_indices.rs | 43 +++++ consensus/state_processing/src/common/mod.rs | 4 + .../state_processing/src/consensus_context.rs | 33 +++- .../src/per_block_processing.rs | 3 + .../src/per_block_processing/errors.rs | 59 +++++- .../is_valid_indexed_payload_attestation.rs | 50 +++++ .../process_operations.rs | 55 +++++- .../per_block_processing/signature_sets.rs | 39 +++- .../verify_payload_attestation.rs | 45 +++++ consensus/types/src/beacon_state.rs | 182 +++++++++++++++--- consensus/types/src/beacon_state/tests.rs | 2 + consensus/types/src/lib.rs | 2 + consensus/types/src/ptc.rs | 128 ++++++++++++ 14 files changed, 608 insertions(+), 39 deletions(-) create mode 100644 consensus/state_processing/src/common/get_payload_attesting_indices.rs create mode 100644 consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs create mode 100644 consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs create mode 100644 consensus/types/src/ptc.rs diff --git a/beacon_node/store/src/consensus_context.rs b/beacon_node/store/src/consensus_context.rs index 281106d9aaa..9b492d68871 100644 --- a/beacon_node/store/src/consensus_context.rs +++ b/beacon_node/store/src/consensus_context.rs @@ -35,6 +35,8 @@ impl OnDiskConsensusContext { proposer_index, current_block_root, indexed_attestations, + indexed_payload_attestations: _, + // TODO(EIP-7732): add indexed_payload_attestations to the on-disk format. } = ctxt; OnDiskConsensusContext { slot, diff --git a/consensus/state_processing/src/common/get_payload_attesting_indices.rs b/consensus/state_processing/src/common/get_payload_attesting_indices.rs new file mode 100644 index 00000000000..5aec3baf39e --- /dev/null +++ b/consensus/state_processing/src/common/get_payload_attesting_indices.rs @@ -0,0 +1,43 @@ +use crate::per_block_processing::errors::{ + BlockOperationError, PayloadAttestationInvalid as Invalid, +}; +use types::*; + +pub fn get_indexed_payload_attestation( + state: &BeaconState, + slot: Slot, + payload_attestation: &PayloadAttestation, + spec: &ChainSpec, +) -> Result, BlockOperationError> { + let attesting_indices = get_payload_attesting_indices(state, slot, payload_attestation, spec)?; + + Ok(IndexedPayloadAttestation { + attesting_indices: VariableList::new(attesting_indices)?, + data: payload_attestation.data.clone(), + signature: payload_attestation.signature.clone(), + }) +} + +pub fn get_payload_attesting_indices( + state: &BeaconState, + slot: Slot, + payload_attestation: &PayloadAttestation, + spec: &ChainSpec, +) -> Result, BeaconStateError> { + let ptc = state.get_ptc(slot, spec)?; + + let bitlist = &payload_attestation.aggregation_bits; + if bitlist.len() != E::PTCSize::to_usize() { + return Err(BeaconStateError::InvalidBitfield); + } + + let mut attesting_indices = Vec::::new(); + for (i, index) in ptc.into_iter().enumerate() { + if let Ok(true) = bitlist.get(i) { + attesting_indices.push(index as u64); + } + } + attesting_indices.sort_unstable(); + + Ok(attesting_indices) +} diff --git a/consensus/state_processing/src/common/mod.rs b/consensus/state_processing/src/common/mod.rs index 0287748fd04..e550a6c48b1 100644 --- a/consensus/state_processing/src/common/mod.rs +++ b/consensus/state_processing/src/common/mod.rs @@ -1,6 +1,7 @@ mod deposit_data_tree; mod get_attestation_participation; mod get_attesting_indices; +mod get_payload_attesting_indices; mod initiate_validator_exit; mod slash_validator; @@ -13,6 +14,9 @@ pub use get_attestation_participation::get_attestation_participation_flag_indice pub use get_attesting_indices::{ attesting_indices_base, attesting_indices_electra, get_attesting_indices_from_state, }; +pub use get_payload_attesting_indices::{ + get_indexed_payload_attestation, get_payload_attesting_indices, +}; pub use initiate_validator_exit::initiate_validator_exit; pub use slash_validator::slash_validator; diff --git a/consensus/state_processing/src/consensus_context.rs b/consensus/state_processing/src/consensus_context.rs index 07d554e3037..a7af510f716 100644 --- a/consensus/state_processing/src/consensus_context.rs +++ b/consensus/state_processing/src/consensus_context.rs @@ -1,11 +1,16 @@ use crate::EpochCacheError; -use crate::common::{attesting_indices_base, attesting_indices_electra}; -use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError}; +use crate::common::{ + attesting_indices_base, attesting_indices_electra, get_indexed_payload_attestation, +}; +use crate::per_block_processing::errors::{ + AttestationInvalid, BlockOperationError, PayloadAttestationInvalid, +}; use std::collections::{HashMap, hash_map::Entry}; use tree_hash::TreeHash; use types::{ AbstractExecPayload, AttestationRef, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, - Hash256, IndexedAttestation, IndexedAttestationRef, SignedBeaconBlock, Slot, + Hash256, IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation, + PayloadAttestation, SignedBeaconBlock, Slot, }; #[derive(Debug, PartialEq, Clone)] @@ -22,6 +27,8 @@ pub struct ConsensusContext { pub current_block_root: Option, /// Cache of indexed attestations constructed during block processing. pub indexed_attestations: HashMap>, + /// Cache of indexed payload attestations constructed during block processing. + pub indexed_payload_attestations: HashMap>, } #[derive(Debug, PartialEq, Clone)] @@ -55,6 +62,7 @@ impl ConsensusContext { proposer_index: None, current_block_root: None, indexed_attestations: HashMap::new(), + indexed_payload_attestations: HashMap::new(), } } @@ -177,6 +185,25 @@ impl ConsensusContext { .map(|indexed_attestation| (*indexed_attestation).to_ref()) } + pub fn get_indexed_payload_attestation<'a>( + &'a mut self, + state: &BeaconState, + slot: Slot, + payload_attestation: &'a PayloadAttestation, + spec: &ChainSpec, + ) -> Result<&'a IndexedPayloadAttestation, BlockOperationError> + { + let key = payload_attestation.tree_hash_root(); + match self.indexed_payload_attestations.entry(key) { + Entry::Occupied(occupied) => Ok(occupied.into_mut()), + Entry::Vacant(vacant) => { + let indexed_payload_attestation = + get_indexed_payload_attestation(state, slot, payload_attestation, spec)?; + Ok(vacant.insert(indexed_payload_attestation)) + } + } + } + pub fn num_cached_indexed_attestations(&self) -> usize { self.indexed_attestations.len() } diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 4d87c49652d..ab644f8ba66 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -18,6 +18,7 @@ pub use self::verify_proposer_slashing::verify_proposer_slashing; pub use altair::sync_committee::process_sync_aggregate; pub use block_signature_verifier::{BlockSignatureVerifier, ParallelSignatureSets}; pub use is_valid_indexed_attestation::is_valid_indexed_attestation; +pub use is_valid_indexed_payload_attestation::is_valid_indexed_payload_attestation; pub use process_operations::process_operations; pub use verify_attestation::{ verify_attestation_for_block_inclusion, verify_attestation_for_state, @@ -33,6 +34,7 @@ pub mod block_signature_verifier; pub mod deneb; pub mod errors; mod is_valid_indexed_attestation; +mod is_valid_indexed_payload_attestation; pub mod process_operations; pub mod process_withdrawals; pub mod signature_sets; @@ -42,6 +44,7 @@ mod verify_attester_slashing; mod verify_bls_to_execution_change; mod verify_deposit; mod verify_exit; +mod verify_payload_attestation; mod verify_proposer_slashing; use crate::common::update_progressive_balances_cache::{ diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index 0374547a207..f3f5ef96af9 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -41,6 +41,10 @@ pub enum BlockProcessingError { index: usize, reason: AttestationInvalid, }, + PayloadAttestationInvalid { + index: usize, + reason: PayloadAttestationInvalid, + }, DepositInvalid { index: usize, reason: DepositInvalid, @@ -209,7 +213,8 @@ impl_into_block_processing_error_with_index!( AttestationInvalid, DepositInvalid, ExitInvalid, - BlsExecutionChangeInvalid + BlsExecutionChangeInvalid, + PayloadAttestationInvalid ); pub type HeaderValidationError = BlockOperationError; @@ -410,6 +415,58 @@ pub enum IndexedAttestationInvalid { SignatureSetError(SignatureSetError), } +#[derive(Debug, PartialEq, Clone)] +pub enum PayloadAttestationInvalid { + /// Block root does not match the parent beacon block root. + BlockRootMismatch { + expected: Hash256, + found: Hash256, + }, + /// The attestation slot is not the previous slot. + SlotMismatch { + expected: Slot, + found: Slot, + }, + BadIndexedPayloadAttestation(IndexedPayloadAttestationInvalid), +} + +impl From> + for BlockOperationError +{ + fn from(e: BlockOperationError) -> Self { + match e { + BlockOperationError::Invalid(e) => BlockOperationError::invalid( + PayloadAttestationInvalid::BadIndexedPayloadAttestation(e), + ), + BlockOperationError::BeaconStateError(e) => BlockOperationError::BeaconStateError(e), + BlockOperationError::SignatureSetError(e) => BlockOperationError::SignatureSetError(e), + BlockOperationError::SszTypesError(e) => BlockOperationError::SszTypesError(e), + BlockOperationError::BitfieldError(e) => BlockOperationError::BitfieldError(e), + BlockOperationError::ConsensusContext(e) => BlockOperationError::ConsensusContext(e), + BlockOperationError::ArithError(e) => BlockOperationError::ArithError(e), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum IndexedPayloadAttestationInvalid { + /// The number of indices is 0. + IndicesEmpty, + /// The validator indices were not in increasing order. + /// + /// The error occurred between the given `index` and `index + 1` + BadValidatorIndicesOrdering(usize), + /// The validator index is unknown. One cannot slash one who does not exist. + UnknownValidator(u64), + /// The indexed attestation aggregate signature was not valid. + BadSignature, + /// There was an error whilst attempting to get a set of signatures. The signatures may have + /// been invalid or an internal error occurred. + SignatureSetError(SignatureSetError), + /// Invalid Payload Status + PayloadStatusInvalid, +} + #[derive(Debug, PartialEq, Clone)] pub enum DepositInvalid { /// The signature (proof-of-possession) does not match the given pubkey. diff --git a/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs b/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs new file mode 100644 index 00000000000..4be053f17bc --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs @@ -0,0 +1,50 @@ +use super::errors::{BlockOperationError, IndexedPayloadAttestationInvalid as Invalid}; +use super::signature_sets::{get_pubkey_from_state, indexed_payload_attestation_signature_set}; +use crate::VerifySignatures; +use itertools::Itertools; +use types::*; + +fn error(reason: Invalid) -> BlockOperationError { + BlockOperationError::invalid(reason) +} + +pub fn is_valid_indexed_payload_attestation( + state: &BeaconState, + indexed_payload_attestation: &IndexedPayloadAttestation, + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), BlockOperationError> { + // Verify indices are sorted and unique + let indices = &indexed_payload_attestation.attesting_indices; + verify!(!indices.is_empty(), Invalid::IndicesEmpty); + let check_sorted = |list: &[u64]| -> Result<(), BlockOperationError> { + list.iter() + .tuple_windows() + .enumerate() + .try_for_each(|(i, (x, y))| { + if x < y { + Ok(()) + } else { + Err(error(Invalid::BadValidatorIndicesOrdering(i))) + } + })?; + Ok(()) + }; + check_sorted(&indices)?; + + if verify_signatures.is_true() { + verify!( + indexed_payload_attestation_signature_set( + state, + |i| get_pubkey_from_state(state, i), + &indexed_payload_attestation.signature, + &indexed_payload_attestation, + spec + )? + .verify(), + Invalid::BadSignature + ); + } + + Ok(()) +} diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index 9f5b916743f..7cde69cbe29 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -5,6 +5,7 @@ use crate::common::{ slash_validator, }; use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; +use crate::per_block_processing::verify_payload_attestation::verify_payload_attestation; use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}; use types::typenum::U33; @@ -37,7 +38,15 @@ pub fn process_operations>( process_bls_to_execution_changes(state, bls_to_execution_changes, verify_signatures, spec)?; } - if state.fork_name_unchecked().electra_enabled() { + if state.fork_name_unchecked().gloas_enabled() { + process_payload_attestations( + state, + block_body.payload_attestations()?.iter(), + verify_signatures, + ctxt, + spec, + )?; + } else if state.fork_name_unchecked().electra_enabled() { state.update_pubkey_cache()?; process_deposit_requests(state, &block_body.execution_requests()?.deposits, spec)?; process_withdrawal_requests(state, &block_body.execution_requests()?.withdrawals, spec)?; @@ -789,3 +798,47 @@ pub fn process_consolidation_request( Ok(()) } + +pub fn process_payload_attestation( + state: &mut BeaconState, + payload_attestation: &PayloadAttestation, + att_index: usize, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + verify_payload_attestation(state, payload_attestation, ctxt, verify_signatures, spec) + .map_err(|e| e.into_with_index(att_index)) +} + +pub fn process_payload_attestations<'a, E: EthSpec, I>( + state: &mut BeaconState, + payload_attestations: I, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> +where + I: Iterator>, +{ + // Ensure required caches are all built. These should be no-ops during regular operation. + // TODO(EIP-7732): verify necessary caches + state.build_committee_cache(RelativeEpoch::Current, spec)?; + state.build_committee_cache(RelativeEpoch::Previous, spec)?; + initialize_epoch_cache(state, spec)?; + initialize_progressive_balances_cache(state, spec)?; + state.build_slashings_cache()?; + + payload_attestations + .enumerate() + .try_for_each(|(i, payload_attestation)| { + process_payload_attestation( + state, + payload_attestation, + i, + verify_signatures, + ctxt, + spec, + ) + }) +} diff --git a/consensus/state_processing/src/per_block_processing/signature_sets.rs b/consensus/state_processing/src/per_block_processing/signature_sets.rs index c18cbbe4b53..10116a9ac18 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -9,11 +9,11 @@ use tree_hash::TreeHash; use types::{ AbstractExecPayload, AggregateSignature, AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError, ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, - InconsistentFork, IndexedAttestation, IndexedAttestationRef, ProposerSlashing, PublicKey, - PublicKeyBytes, Signature, SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockHeader, - SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid, - SignedExecutionPayloadEnvelope, SignedRoot, SignedVoluntaryExit, SigningData, Slot, - SyncAggregate, SyncAggregatorSelectionData, Unsigned, + IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation, ProposerSlashing, + PublicKey, PublicKeyBytes, Signature, SignedAggregateAndProof, SignedBeaconBlock, + SignedBeaconBlockHeader, SignedBlsToExecutionChange, SignedContributionAndProof, + SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedRoot, SignedVoluntaryExit, + SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData, Unsigned, }; pub type Result = std::result::Result; @@ -299,6 +299,35 @@ where Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message)) } +pub fn indexed_payload_attestation_signature_set<'a, 'b, E, F>( + state: &'a BeaconState, + get_pubkey: F, + signature: &'a AggregateSignature, + indexed_payload_attestation: &'b IndexedPayloadAttestation, + spec: &'a ChainSpec, +) -> Result> +where + E: EthSpec, + F: Fn(usize) -> Option>, +{ + let mut pubkeys = Vec::with_capacity(indexed_payload_attestation.attesting_indices.len()); + for &validator_idx in indexed_payload_attestation.attesting_indices.iter() { + pubkeys.push( + get_pubkey(validator_idx as usize).ok_or(Error::ValidatorUnknown(validator_idx))?, + ); + } + + let domain = spec.compute_domain( + Domain::PTCAttester, + spec.genesis_fork_version, + state.genesis_validators_root(), + ); + + let message = indexed_payload_attestation.data.signing_root(domain); + + Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message)) +} + /// Returns the signature set for the given `indexed_attestation` but pubkeys are supplied directly /// instead of from the state. pub fn indexed_attestation_signature_set_from_pubkeys<'a, 'b, E, F>( diff --git a/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs new file mode 100644 index 00000000000..ae0c4fb86b9 --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs @@ -0,0 +1,45 @@ +use super::VerifySignatures; +use super::errors::{BlockOperationError, PayloadAttestationInvalid as Invalid}; +use crate::ConsensusContext; +use crate::per_block_processing::is_valid_indexed_payload_attestation; +use types::*; + +pub fn verify_payload_attestation<'ctxt, E: EthSpec>( + state: &mut BeaconState, + payload_attestation: &'ctxt PayloadAttestation, + ctxt: &'ctxt mut ConsensusContext, + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), BlockOperationError> { + let data = &payload_attestation.data; + + // Check that the attestation is for the parent beacon block + verify!( + data.beacon_block_root == state.latest_block_header().parent_root, + Invalid::BlockRootMismatch { + expected: state.latest_block_header().parent_root, + found: data.beacon_block_root, + } + ); + + // Check that the attestation is for the previous slot + verify!( + data.slot + 1 == state.slot(), + Invalid::SlotMismatch { + expected: state.slot().saturating_sub(Slot::new(1)), + found: data.slot, + } + ); + + let indexed_payload_attestation = + ctxt.get_indexed_payload_attestation(state, data.slot, payload_attestation, spec)?; + + is_valid_indexed_payload_attestation( + state, + &indexed_payload_attestation, + verify_signatures, + spec, + )?; + + Ok(()) +} diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index b734c3a8c8d..7ae9cd5ac8f 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -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. @@ -1094,7 +1096,15 @@ impl BeaconState { 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 self.fork_name_unchecked().gloas_enabled() { + self.compute_balance_weighted_selection(indices, &seed, 1, true, spec)? + .get(0) + .copied() + .ok_or(Error::InsufficientValidators) + } else { + self.compute_proposer_index(indices, &seed, spec) + } }) .collect() } @@ -1334,41 +1344,51 @@ impl BeaconState { /// Compute the sync committee indices for the next sync committee. fn get_next_sync_committee_indices(&self, spec: &ChainSpec) -> Result, Error> { 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 +2796,110 @@ impl BeaconState { 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, 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 = 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::from(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, 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. + /// + /// Spec: compute_balance_weighted_selection helper + fn compute_balance_weighted_selection( + &self, + indices: &[usize], + seed: &[u8], + size: usize, + shuffle_indices: bool, + spec: &ChainSpec, + ) -> Result, 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 % 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 += 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 { + 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 * max_random_value >= max_effective_balance * random_value) + } } impl ForkVersionDecode for BeaconState { diff --git a/consensus/types/src/beacon_state/tests.rs b/consensus/types/src/beacon_state/tests.rs index e5b05a4a5bd..780008473e8 100644 --- a/consensus/types/src/beacon_state/tests.rs +++ b/consensus/types/src/beacon_state/tests.rs @@ -53,6 +53,8 @@ async fn build_state(validator_count: usize) -> BeaconState { .head_beacon_state_cloned() } +/// TODO(EIP7732) Add test for ptc attester index using get_ptc_attester_seed + async fn test_beacon_proposer_index() { let spec = E::default_spec(); diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 63d986055d0..a5b8e6ca0e7 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -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; diff --git a/consensus/types/src/ptc.rs b/consensus/types/src/ptc.rs new file mode 100644 index 00000000000..1b81593312e --- /dev/null +++ b/consensus/types/src/ptc.rs @@ -0,0 +1,128 @@ +use crate::*; + +/// TODO(EIP-7732): is it easier to return u64 or usize? +#[derive(Clone, Debug, PartialEq)] +pub struct PTC(pub FixedVector); + +impl<'a, E: EthSpec> IntoIterator for &'a PTC { + type Item = &'a usize; + type IntoIter = std::slice::Iter<'a, usize>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl IntoIterator for PTC { + type Item = usize; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +// TODO(EIP7732) Mark's implementation below was using from_committees to filter out ptc members from attestation committees, but I don't see that in the spec, so perhaps is a deprecated implementation +// impl PTC { +// pub fn from_committees(committees: &[BeaconCommittee]) -> Result { +// // this function is only used here and +// // I have no idea where else to put it +// fn bit_floor(n: u64) -> u64 { +// if n == 0 { +// 0 +// } else { +// 1 << (n.leading_zeros() as u64 ^ 63) +// } +// } + +// let committee_count_per_slot = committees.len() as u64; +// let committees_per_slot = bit_floor(std::cmp::min( +// committee_count_per_slot, +// E::PTCSize::to_u64(), +// )) as usize; +// let members_per_committee = E::PTCSize::to_usize().safe_div(committees_per_slot)?; + +// let mut ptc = Vec::with_capacity(E::PTCSize::to_usize()); +// for idx in 0..committees_per_slot { +// let beacon_committee = committees +// .get(idx as usize) +// .ok_or_else(|| Error::InvalidCommitteeIndex(idx as u64))?; +// ptc.extend_from_slice(&beacon_committee.committee[..members_per_committee]); +// } + +// Ok(Self(FixedVector::from(ptc))) +// } +// } + +// impl<'a, E: EthSpec> IntoIterator for &'a PTC { +// type Item = &'a usize; +// type IntoIter = std::slice::Iter<'a, usize>; + +// fn into_iter(self) -> Self::IntoIter { +// self.0.iter() +// } +// } + +// impl IntoIterator for PTC { +// type Item = usize; +// type IntoIter = std::vec::IntoIter; + +// fn into_iter(self) -> Self::IntoIter { +// self.0.into_iter() +// } +// } +// use crate::*; +// use safe_arith::SafeArith; + +// /// TODO(EIP-7732): is it easier to return u64 or usize? +// #[derive(Clone, Debug, PartialEq)] +// pub struct PTC(FixedVector); + +// impl PTC { +// pub fn from_committees(committees: &[BeaconCommittee]) -> Result { +// // this function is only used here and +// // I have no idea where else to put it +// fn bit_floor(n: u64) -> u64 { +// if n == 0 { +// 0 +// } else { +// 1 << (n.leading_zeros() as u64 ^ 63) +// } +// } + +// let committee_count_per_slot = committees.len() as u64; +// let committees_per_slot = bit_floor(std::cmp::min( +// committee_count_per_slot, +// E::PTCSize::to_u64(), +// )) as usize; +// let members_per_committee = E::PTCSize::to_usize().safe_div(committees_per_slot)?; + +// let mut ptc = Vec::with_capacity(E::PTCSize::to_usize()); +// for idx in 0..committees_per_slot { +// let beacon_committee = committees +// .get(idx as usize) +// .ok_or_else(|| Error::InvalidCommitteeIndex(idx as u64))?; +// ptc.extend_from_slice(&beacon_committee.committee[..members_per_committee]); +// } + +// Ok(Self(FixedVector::from(ptc))) +// } +// } + +// impl<'a, E: EthSpec> IntoIterator for &'a PTC { +// type Item = &'a usize; +// type IntoIter = std::slice::Iter<'a, usize>; + +// fn into_iter(self) -> Self::IntoIter { +// self.0.iter() +// } +// } + +// impl IntoIterator for PTC { +// type Item = usize; +// type IntoIter = std::vec::IntoIter; + +// fn into_iter(self) -> Self::IntoIter { +// self.0.into_iter() +// } +// } From cec252b0b2850eb0070e742999cf0884ef439cda Mon Sep 17 00:00:00 2001 From: shane-moore Date: Tue, 7 Oct 2025 15:50:34 -0700 Subject: [PATCH 2/3] allow duplicates in indexed payload attestation indices --- .../is_valid_indexed_payload_attestation.rs | 8 +- .../process_operations.rs | 5 + .../verify_payload_attestation.rs | 5 +- consensus/types/src/beacon_state.rs | 12 +- consensus/types/src/beacon_state/tests.rs | 10 +- consensus/types/src/ptc.rs | 105 ------------------ 6 files changed, 27 insertions(+), 118 deletions(-) diff --git a/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs b/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs index 4be053f17bc..45ccdf35e22 100644 --- a/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs +++ b/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs @@ -14,7 +14,7 @@ pub fn is_valid_indexed_payload_attestation( verify_signatures: VerifySignatures, spec: &ChainSpec, ) -> Result<(), BlockOperationError> { - // Verify indices are sorted and unique + // Verify indices are non-empty and sorted (duplicates allowed) let indices = &indexed_payload_attestation.attesting_indices; verify!(!indices.is_empty(), Invalid::IndicesEmpty); let check_sorted = |list: &[u64]| -> Result<(), BlockOperationError> { @@ -22,7 +22,7 @@ pub fn is_valid_indexed_payload_attestation( .tuple_windows() .enumerate() .try_for_each(|(i, (x, y))| { - if x < y { + if x <= y { Ok(()) } else { Err(error(Invalid::BadValidatorIndicesOrdering(i))) @@ -30,7 +30,7 @@ pub fn is_valid_indexed_payload_attestation( })?; Ok(()) }; - check_sorted(&indices)?; + check_sorted(indices)?; if verify_signatures.is_true() { verify!( @@ -38,7 +38,7 @@ pub fn is_valid_indexed_payload_attestation( state, |i| get_pubkey_from_state(state, i), &indexed_payload_attestation.signature, - &indexed_payload_attestation, + indexed_payload_attestation, spec )? .verify(), diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index 7cde69cbe29..c0007424ef2 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -799,6 +799,11 @@ pub fn process_consolidation_request( Ok(()) } +// TODO(EIP-7732): Add test cases for `process_payload_attestations` to +// `consensus/state_processing/src/per_block_processing/tests.rs`. +// The tests will require being able to build Gloas blocks with PayloadAttestations, +// which currently fails due to incomplete Gloas block structure as mentioned here +// https://github.com/sigp/lighthouse/pull/8273 pub fn process_payload_attestation( state: &mut BeaconState, payload_attestation: &PayloadAttestation, diff --git a/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs index ae0c4fb86b9..a65f132462c 100644 --- a/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs +++ b/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs @@ -2,6 +2,7 @@ use super::VerifySignatures; use super::errors::{BlockOperationError, PayloadAttestationInvalid as Invalid}; use crate::ConsensusContext; use crate::per_block_processing::is_valid_indexed_payload_attestation; +use safe_arith::SafeArith; use types::*; pub fn verify_payload_attestation<'ctxt, E: EthSpec>( @@ -24,7 +25,7 @@ pub fn verify_payload_attestation<'ctxt, E: EthSpec>( // Check that the attestation is for the previous slot verify!( - data.slot + 1 == state.slot(), + data.slot.safe_add(1)? == state.slot(), Invalid::SlotMismatch { expected: state.slot().saturating_sub(Slot::new(1)), found: data.slot, @@ -36,7 +37,7 @@ pub fn verify_payload_attestation<'ctxt, E: EthSpec>( is_valid_indexed_payload_attestation( state, - &indexed_payload_attestation, + indexed_payload_attestation, verify_signatures, spec, )?; diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 7ae9cd5ac8f..f55561f75da 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -1099,7 +1099,7 @@ impl BeaconState { if self.fork_name_unchecked().gloas_enabled() { self.compute_balance_weighted_selection(indices, &seed, 1, true, spec)? - .get(0) + .first() .copied() .ok_or(Error::InsufficientValidators) } else { @@ -1344,6 +1344,7 @@ impl BeaconState { /// Compute the sync committee indices for the next sync committee. fn get_next_sync_committee_indices(&self, spec: &ChainSpec) -> Result, Error> { let epoch = self.current_epoch().safe_add(1)?; + let active_validator_indices = self.get_active_validator_indices(epoch, spec)?; let seed = self.get_seed(epoch, Domain::SyncCommittee, spec)?; @@ -2838,8 +2839,6 @@ impl BeaconState { /// /// If shuffle_indices is True, candidate indices are themselves sampled from indices /// by shuffling it, otherwise indices is traversed in order. - /// - /// Spec: compute_balance_weighted_selection helper fn compute_balance_weighted_selection( &self, indices: &[usize], @@ -2857,7 +2856,7 @@ impl BeaconState { let mut count = 0usize; while selected.len() < size { - let mut next_index = count % total; + let mut next_index = count.safe_rem(total)?; if shuffle_indices { next_index = @@ -2871,7 +2870,7 @@ impl BeaconState { selected.push(*candidate_index); } - count += 1; + count.safe_add_assign(1)?; } Ok(selected) @@ -2898,7 +2897,8 @@ impl BeaconState { MAX_RANDOM_BYTE }; - Ok(effective_balance * max_random_value >= max_effective_balance * random_value) + Ok(effective_balance.safe_mul(max_random_value)? + >= max_effective_balance.safe_mul(random_value)?) } } diff --git a/consensus/types/src/beacon_state/tests.rs b/consensus/types/src/beacon_state/tests.rs index 780008473e8..ec6a2efa25e 100644 --- a/consensus/types/src/beacon_state/tests.rs +++ b/consensus/types/src/beacon_state/tests.rs @@ -53,7 +53,15 @@ async fn build_state(validator_count: usize) -> BeaconState { .head_beacon_state_cloned() } -/// TODO(EIP7732) Add test for ptc attester index using get_ptc_attester_seed +/// 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() { let spec = E::default_spec(); diff --git a/consensus/types/src/ptc.rs b/consensus/types/src/ptc.rs index 1b81593312e..d3b50b11346 100644 --- a/consensus/types/src/ptc.rs +++ b/consensus/types/src/ptc.rs @@ -21,108 +21,3 @@ impl IntoIterator for PTC { self.0.into_iter() } } - -// TODO(EIP7732) Mark's implementation below was using from_committees to filter out ptc members from attestation committees, but I don't see that in the spec, so perhaps is a deprecated implementation -// impl PTC { -// pub fn from_committees(committees: &[BeaconCommittee]) -> Result { -// // this function is only used here and -// // I have no idea where else to put it -// fn bit_floor(n: u64) -> u64 { -// if n == 0 { -// 0 -// } else { -// 1 << (n.leading_zeros() as u64 ^ 63) -// } -// } - -// let committee_count_per_slot = committees.len() as u64; -// let committees_per_slot = bit_floor(std::cmp::min( -// committee_count_per_slot, -// E::PTCSize::to_u64(), -// )) as usize; -// let members_per_committee = E::PTCSize::to_usize().safe_div(committees_per_slot)?; - -// let mut ptc = Vec::with_capacity(E::PTCSize::to_usize()); -// for idx in 0..committees_per_slot { -// let beacon_committee = committees -// .get(idx as usize) -// .ok_or_else(|| Error::InvalidCommitteeIndex(idx as u64))?; -// ptc.extend_from_slice(&beacon_committee.committee[..members_per_committee]); -// } - -// Ok(Self(FixedVector::from(ptc))) -// } -// } - -// impl<'a, E: EthSpec> IntoIterator for &'a PTC { -// type Item = &'a usize; -// type IntoIter = std::slice::Iter<'a, usize>; - -// fn into_iter(self) -> Self::IntoIter { -// self.0.iter() -// } -// } - -// impl IntoIterator for PTC { -// type Item = usize; -// type IntoIter = std::vec::IntoIter; - -// fn into_iter(self) -> Self::IntoIter { -// self.0.into_iter() -// } -// } -// use crate::*; -// use safe_arith::SafeArith; - -// /// TODO(EIP-7732): is it easier to return u64 or usize? -// #[derive(Clone, Debug, PartialEq)] -// pub struct PTC(FixedVector); - -// impl PTC { -// pub fn from_committees(committees: &[BeaconCommittee]) -> Result { -// // this function is only used here and -// // I have no idea where else to put it -// fn bit_floor(n: u64) -> u64 { -// if n == 0 { -// 0 -// } else { -// 1 << (n.leading_zeros() as u64 ^ 63) -// } -// } - -// let committee_count_per_slot = committees.len() as u64; -// let committees_per_slot = bit_floor(std::cmp::min( -// committee_count_per_slot, -// E::PTCSize::to_u64(), -// )) as usize; -// let members_per_committee = E::PTCSize::to_usize().safe_div(committees_per_slot)?; - -// let mut ptc = Vec::with_capacity(E::PTCSize::to_usize()); -// for idx in 0..committees_per_slot { -// let beacon_committee = committees -// .get(idx as usize) -// .ok_or_else(|| Error::InvalidCommitteeIndex(idx as u64))?; -// ptc.extend_from_slice(&beacon_committee.committee[..members_per_committee]); -// } - -// Ok(Self(FixedVector::from(ptc))) -// } -// } - -// impl<'a, E: EthSpec> IntoIterator for &'a PTC { -// type Item = &'a usize; -// type IntoIter = std::slice::Iter<'a, usize>; - -// fn into_iter(self) -> Self::IntoIter { -// self.0.iter() -// } -// } - -// impl IntoIterator for PTC { -// type Item = usize; -// type IntoIter = std::vec::IntoIter; - -// fn into_iter(self) -> Self::IntoIter { -// self.0.into_iter() -// } -// } From 6f8f6fcc9e8312017b82a43f6bceb5c9b54f68aa Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 3 Nov 2025 10:20:43 -0800 Subject: [PATCH 3/3] updates per pr review --- .../src/per_block_processing/signature_sets.rs | 11 ++++++----- consensus/types/src/beacon_state.rs | 10 ++++++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/consensus/state_processing/src/per_block_processing/signature_sets.rs b/consensus/state_processing/src/per_block_processing/signature_sets.rs index 10116a9ac18..d9efcad6fbe 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -9,11 +9,12 @@ use tree_hash::TreeHash; use types::{ AbstractExecPayload, AggregateSignature, AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError, ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, - IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation, ProposerSlashing, - PublicKey, PublicKeyBytes, Signature, SignedAggregateAndProof, SignedBeaconBlock, - SignedBeaconBlockHeader, SignedBlsToExecutionChange, SignedContributionAndProof, - SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedRoot, SignedVoluntaryExit, - SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData, Unsigned, + InconsistentFork, IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation, + ProposerSlashing, PublicKey, PublicKeyBytes, Signature, SignedAggregateAndProof, + SignedBeaconBlock, SignedBeaconBlockHeader, SignedBlsToExecutionChange, + SignedContributionAndProof, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, + SignedRoot, SignedVoluntaryExit, SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData, + Unsigned, }; pub type Result = std::result::Result; diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index f55561f75da..550f0d67705 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -1090,6 +1090,7 @@ impl BeaconState { } } + let gloas_enabled = self.fork_name_unchecked().gloas_enabled(); epoch .slot_iter(E::slots_per_epoch()) .map(|slot| { @@ -1097,7 +1098,7 @@ impl BeaconState { preimage.append(&mut int_to_bytes8(slot.as_u64())); let seed = hash(&preimage); - if self.fork_name_unchecked().gloas_enabled() { + if gloas_enabled { self.compute_balance_weighted_selection(indices, &seed, 1, true, spec)? .first() .copied() @@ -2819,7 +2820,7 @@ impl BeaconState { spec, )?; - Ok(PTC(FixedVector::from(selected_indices))) + Ok(PTC(FixedVector::new(selected_indices)?)) } /// Compute the seed to use for the ptc attester selection at the given `slot`. @@ -2885,6 +2886,11 @@ impl BeaconState { iteration: usize, spec: &ChainSpec, ) -> Result { + // 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());