Gloas payload attestation consensus (#8827)

- Implement `process_payload_attestation`
- Implement EF tests for payload attestations (allows simplification of handler now that we support all `operations` tests).
- Update the `BlockSignatureVerifier` to signature-verify payload attestations


Co-Authored-By: Michael Sproul <michael@sigmaprime.io>
This commit is contained in:
Michael Sproul
2026-02-17 16:50:44 +11:00
committed by GitHub
parent eec0700f94
commit 67b9673191
14 changed files with 378 additions and 34 deletions

View File

@@ -1,7 +1,9 @@
#![allow(clippy::arithmetic_side_effects)]
use super::signature_sets::{Error as SignatureSetError, *};
use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError};
use crate::per_block_processing::errors::{
AttestationInvalid, BlockOperationError, PayloadAttestationInvalid,
};
use crate::{ConsensusContext, ContextError};
use bls::{PublicKey, PublicKeyBytes, SignatureSet, verify_signature_sets};
use std::borrow::Cow;
@@ -18,6 +20,8 @@ pub enum Error {
SignatureInvalid,
/// An attestation in the block was invalid. The block is invalid.
AttestationValidationError(BlockOperationError<AttestationInvalid>),
/// A payload attestation in the block was invalid. The block is invalid.
PayloadAttestationValidationError(BlockOperationError<PayloadAttestationInvalid>),
/// There was an error attempting to read from a `BeaconState`. Block
/// validity was not determined.
BeaconStateError(BeaconStateError),
@@ -66,6 +70,12 @@ impl From<BlockOperationError<AttestationInvalid>> for Error {
}
}
impl From<BlockOperationError<PayloadAttestationInvalid>> for Error {
fn from(e: BlockOperationError<PayloadAttestationInvalid>) -> Error {
Error::PayloadAttestationValidationError(e)
}
}
/// Reads the BLS signatures and keys from a `SignedBeaconBlock`, storing them as a `Vec<SignatureSet>`.
///
/// This allows for optimizations related to batch BLS operations (see the
@@ -171,6 +181,7 @@ where
self.include_sync_aggregate(block)?;
self.include_bls_to_execution_changes(block)?;
self.include_execution_payload_bid(block)?;
self.include_payload_attestations(block, ctxt)?;
Ok(())
}
@@ -296,6 +307,39 @@ where
})
}
/// Includes all signatures in `self.block.body.payload_attestations` for verification.
pub fn include_payload_attestations<Payload: AbstractExecPayload<E>>(
&mut self,
block: &'a SignedBeaconBlock<E, Payload>,
ctxt: &mut ConsensusContext<E>,
) -> Result<()> {
let Ok(payload_attestations) = block.message().body().payload_attestations() else {
// Nothing to do pre-Gloas.
return Ok(());
};
self.sets.sets.reserve(payload_attestations.len());
payload_attestations
.iter()
.try_for_each(|payload_attestation| {
let indexed_payload_attestation = ctxt.get_indexed_payload_attestation(
self.state,
payload_attestation,
self.spec,
)?;
self.sets.push(indexed_payload_attestation_signature_set(
self.state,
self.get_pubkey.clone(),
&payload_attestation.signature,
indexed_payload_attestation,
self.spec,
)?);
Ok(())
})
}
/// Includes all signatures in `self.block.body.voluntary_exits` for verification.
pub fn include_exits<Payload: AbstractExecPayload<E>>(
&mut self,

View File

@@ -41,6 +41,10 @@ pub enum BlockProcessingError {
index: usize,
reason: AttestationInvalid,
},
PayloadAttestationInvalid {
index: usize,
reason: PayloadAttestationInvalid,
},
DepositInvalid {
index: usize,
reason: DepositInvalid,
@@ -217,6 +221,7 @@ impl_into_block_processing_error_with_index!(
AttesterSlashingInvalid,
IndexedAttestationInvalid,
AttestationInvalid,
PayloadAttestationInvalid,
DepositInvalid,
ExitInvalid,
BlsExecutionChangeInvalid
@@ -422,6 +427,52 @@ 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.
BadValidatorIndicesOrdering,
/// 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),
}
#[derive(Debug, PartialEq, Clone)]
pub enum DepositInvalid {
/// The signature (proof-of-possession) does not match the given pubkey.

View File

@@ -0,0 +1,32 @@
use super::errors::{BlockOperationError, IndexedPayloadAttestationInvalid as Invalid};
use super::signature_sets::{get_pubkey_from_state, indexed_payload_attestation_signature_set};
use crate::VerifySignatures;
use types::*;
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);
verify!(indices.is_sorted(), Invalid::BadValidatorIndicesOrdering);
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 bls::{PublicKeyBytes, SignatureBytes};
use ssz_types::FixedVector;
use typenum::U33;
@@ -39,8 +40,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() && !state.fork_name_unchecked().gloas_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_pre_gloas(
state,
@@ -1074,3 +1082,45 @@ pub fn process_consolidation_request<E: EthSpec>(
Ok(())
}
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>>,
{
// Presently the PTC cache requires the committee cache for `state.slot() - 1` which is either
// in the current or previous epoch.
// TODO(gloas): These requirements may change if we introduce a PTC cache.
state.build_committee_cache(RelativeEpoch::Current, spec)?;
state.build_committee_cache(RelativeEpoch::Previous, spec)?;
payload_attestations
.enumerate()
.try_for_each(|(i, payload_attestation)| {
process_payload_attestation(
state,
payload_attestation,
i,
verify_signatures,
ctxt,
spec,
)
})
}

View File

@@ -10,10 +10,10 @@ use typenum::Unsigned;
use types::{
AbstractExecPayload, AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError,
BuilderIndex, ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork,
IndexedAttestation, IndexedAttestationRef, ProposerSlashing, SignedAggregateAndProof,
SignedBeaconBlock, SignedBeaconBlockHeader, SignedBlsToExecutionChange,
SignedContributionAndProof, SignedExecutionPayloadBid, SignedRoot, SignedVoluntaryExit,
SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData,
IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation, ProposerSlashing,
SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockHeader,
SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid, SignedRoot,
SignedVoluntaryExit, SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData,
consts::gloas::BUILDER_INDEX_SELF_BUILD,
};
@@ -355,6 +355,40 @@ 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 epoch = indexed_payload_attestation
.data
.slot
.epoch(E::slots_per_epoch());
let domain = spec.get_domain(
epoch,
Domain::PTCAttester,
&state.fork(),
state.genesis_validators_root(),
);
let message = indexed_payload_attestation.data.signing_root(domain);
Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message))
}
pub fn execution_payload_bid_signature_set<'a, E, F>(
state: &'a BeaconState<E>,
get_builder_pubkey: 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, payload_attestation, spec)?;
is_valid_indexed_payload_attestation(
state,
indexed_payload_attestation,
verify_signatures,
spec,
)?;
Ok(())
}