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

@@ -0,0 +1,43 @@
use crate::per_block_processing::errors::{
BlockOperationError, PayloadAttestationInvalid as Invalid,
};
use types::*;
pub fn get_indexed_payload_attestation<E: EthSpec>(
state: &BeaconState<E>,
slot: Slot,
payload_attestation: &PayloadAttestation<E>,
spec: &ChainSpec,
) -> Result<IndexedPayloadAttestation<E>, BlockOperationError<Invalid>> {
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<E: EthSpec>(
state: &BeaconState<E>,
slot: Slot,
payload_attestation: &PayloadAttestation<E>,
spec: &ChainSpec,
) -> Result<Vec<u64>, 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::<u64>::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)
}

View File

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

View File

@@ -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<E: EthSpec> {
pub current_block_root: Option<Hash256>,
/// Cache of indexed attestations constructed during block processing.
pub indexed_attestations: HashMap<Hash256, IndexedAttestation<E>>,
/// Cache of indexed payload attestations constructed during block processing.
pub indexed_payload_attestations: HashMap<Hash256, IndexedPayloadAttestation<E>>,
}
#[derive(Debug, PartialEq, Clone)]
@@ -55,6 +62,7 @@ impl<E: EthSpec> ConsensusContext<E> {
proposer_index: None,
current_block_root: None,
indexed_attestations: HashMap::new(),
indexed_payload_attestations: HashMap::new(),
}
}
@@ -177,6 +185,25 @@ impl<E: EthSpec> ConsensusContext<E> {
.map(|indexed_attestation| (*indexed_attestation).to_ref())
}
pub fn get_indexed_payload_attestation<'a>(
&'a mut self,
state: &BeaconState<E>,
slot: Slot,
payload_attestation: &'a PayloadAttestation<E>,
spec: &ChainSpec,
) -> Result<&'a IndexedPayloadAttestation<E>, BlockOperationError<PayloadAttestationInvalid>>
{
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()
}

View File

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

View File

@@ -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<HeaderInvalid>;
@@ -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<BlockOperationError<IndexedPayloadAttestationInvalid>>
for BlockOperationError<PayloadAttestationInvalid>
{
fn from(e: BlockOperationError<IndexedPayloadAttestationInvalid>) -> 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.

View File

@@ -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<Invalid> {
BlockOperationError::invalid(reason)
}
pub fn is_valid_indexed_payload_attestation<E: EthSpec>(
state: &BeaconState<E>,
indexed_payload_attestation: &IndexedPayloadAttestation<E>,
verify_signatures: VerifySignatures,
spec: &ChainSpec,
) -> Result<(), BlockOperationError<Invalid>> {
// 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<Invalid>> {
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(())
}

View File

@@ -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<E: EthSpec, Payload: AbstractExecPayload<E>>(
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,52 @@ pub fn process_consolidation_request<E: EthSpec>(
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<E: EthSpec>(
state: &mut BeaconState<E>,
payload_attestation: &PayloadAttestation<E>,
att_index: usize,
verify_signatures: VerifySignatures,
ctxt: &mut ConsensusContext<E>,
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<E>,
payload_attestations: I,
verify_signatures: VerifySignatures,
ctxt: &mut ConsensusContext<E>,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError>
where
I: Iterator<Item = &'a PayloadAttestation<E>>,
{
// 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,
)
})
}

View File

@@ -9,11 +9,12 @@ 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,
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<T> = std::result::Result<T, Error>;
@@ -299,6 +300,35 @@ where
Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message))
}
pub fn indexed_payload_attestation_signature_set<'a, 'b, E, F>(
state: &'a BeaconState<E>,
get_pubkey: F,
signature: &'a AggregateSignature,
indexed_payload_attestation: &'b IndexedPayloadAttestation<E>,
spec: &'a ChainSpec,
) -> Result<SignatureSet<'a>>
where
E: EthSpec,
F: Fn(usize) -> Option<Cow<'a, PublicKey>>,
{
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>(

View File

@@ -0,0 +1,46 @@
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>(
state: &mut BeaconState<E>,
payload_attestation: &'ctxt PayloadAttestation<E>,
ctxt: &'ctxt mut ConsensusContext<E>,
verify_signatures: VerifySignatures,
spec: &ChainSpec,
) -> Result<(), BlockOperationError<Invalid>> {
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.safe_add(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(())
}