This commit is contained in:
Michael Sproul
2026-05-14 14:55:54 +10:00
parent d4eb6e4727
commit c65ed44d50
5 changed files with 45 additions and 84 deletions

View File

@@ -9,12 +9,12 @@ use bls::AggregateSignature;
use educe::Educe; use educe::Educe;
use eth2::types::{EventKind, ForkVersionedResponse}; use eth2::types::{EventKind, ForkVersionedResponse};
use parking_lot::RwLock; use parking_lot::RwLock;
use safe_arith::SafeArith;
use slot_clock::SlotClock; use slot_clock::SlotClock;
use state_processing::per_block_processing::signature_sets::indexed_payload_attestation_signature_set; use state_processing::per_block_processing::signature_sets::indexed_payload_attestation_signature_set_from_pubkeys;
use state_processing::state_advance::partial_state_advance;
use std::borrow::Cow; use std::borrow::Cow;
use types::{ChainSpec, EthSpec, IndexedPayloadAttestation, PTC, PayloadAttestationMessage, Slot}; use types::{
ChainSpec, EthSpec, Hash256, IndexedPayloadAttestation, PTC, PayloadAttestationMessage, Slot,
};
pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { pub struct GossipVerificationContext<'a, T: BeaconChainTypes> {
pub slot_clock: &'a T::SlotClock, pub slot_clock: &'a T::SlotClock,
@@ -24,6 +24,7 @@ pub struct GossipVerificationContext<'a, T: BeaconChainTypes> {
pub shuffling_cache: &'a RwLock<ShufflingCache<T::EthSpec>>, pub shuffling_cache: &'a RwLock<ShufflingCache<T::EthSpec>>,
pub validator_pubkey_cache: &'a RwLock<ValidatorPubkeyCache<T>>, pub validator_pubkey_cache: &'a RwLock<ValidatorPubkeyCache<T>>,
pub store: &'a BeaconStore<T>, pub store: &'a BeaconStore<T>,
pub genesis_validators_root: Hash256,
} }
/// A `PayloadAttestationMessage` that has been verified for propagation on the gossip network. /// A `PayloadAttestationMessage` that has been verified for propagation on the gossip network.
@@ -98,53 +99,6 @@ impl<T: BeaconChainTypes> VerifiedPayloadAttestationMessage<T> {
}); });
} }
// Get a state for signature verification. If the cached head state is too stale
// (e.g. during liveness failures with many skipped slots), fall back to loading
// a more recent state from the store and advancing it if necessary.
let head = ctx.canonical_head.cached_head();
let head_state = &head.snapshot.beacon_state;
let state_epoch = head_state.current_epoch();
// get_ptc can serve epochs in [state_epoch - 1, state_epoch + min_seed_lookahead].
// If the message epoch is beyond that range, the head state is stale.
let advanced_state = if message_epoch
> state_epoch
.safe_add(ctx.spec.min_seed_lookahead)
.map_err(BeaconChainError::from)?
{
let head_block_root = head.head_block_root();
let target_slot = message_epoch.start_slot(T::EthSpec::slots_per_epoch());
let (state_root, mut state) = ctx
.store
.get_advanced_hot_state(
head_block_root,
target_slot,
head.snapshot.beacon_state_root(),
)
.map_err(BeaconChainError::from)?
.ok_or(BeaconChainError::MissingBeaconState(
head.snapshot.beacon_state_root(),
))?;
if state
.current_epoch()
.safe_add(ctx.spec.min_seed_lookahead)
.map_err(BeaconChainError::from)?
< message_epoch
{
partial_state_advance(&mut state, Some(state_root), target_slot, ctx.spec)
.map_err(BeaconChainError::from)?;
}
Some(state)
} else {
None
};
let state = advanced_state.as_ref().unwrap_or(head_state);
// Build the indexed form for signature verification and downstream fork choice. // Build the indexed form for signature verification and downstream fork choice.
let indexed_payload_attestation = IndexedPayloadAttestation { let indexed_payload_attestation = IndexedPayloadAttestation {
attesting_indices: vec![validator_index] attesting_indices: vec![validator_index]
@@ -157,11 +111,13 @@ impl<T: BeaconChainTypes> VerifiedPayloadAttestationMessage<T> {
{ {
// [REJECT] The signature is valid with respect to the `validator_index`. // [REJECT] The signature is valid with respect to the `validator_index`.
let pubkey_cache = ctx.validator_pubkey_cache.read(); let pubkey_cache = ctx.validator_pubkey_cache.read();
let signature_set = indexed_payload_attestation_signature_set( let fork = ctx.spec.fork_at_epoch(message_epoch);
state, let signature_set = indexed_payload_attestation_signature_set_from_pubkeys(
|validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed),
&indexed_payload_attestation.signature, &indexed_payload_attestation.signature,
&indexed_payload_attestation, &indexed_payload_attestation,
&fork,
ctx.genesis_validators_root,
ctx.spec, ctx.spec,
) )
.map_err(|_| Error::UnknownValidatorIndex(validator_index))?; .map_err(|_| Error::UnknownValidatorIndex(validator_index))?;
@@ -219,6 +175,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
shuffling_cache: &self.shuffling_cache, shuffling_cache: &self.shuffling_cache,
validator_pubkey_cache: &self.validator_pubkey_cache, validator_pubkey_cache: &self.validator_pubkey_cache,
store: &self.store, store: &self.store,
genesis_validators_root: self.genesis_validators_root,
} }
} }

View File

@@ -9,7 +9,7 @@
use crate::BeaconChainError; use crate::BeaconChainError;
use strum::AsRefStr; use strum::AsRefStr;
use types::{BeaconStateError, Hash256, Slot}; use types::{Hash256, Slot};
pub mod gossip_verified_payload_attestation; pub mod gossip_verified_payload_attestation;
@@ -92,12 +92,6 @@ pub enum Error {
/// We were unable to process this message due to an internal error. It's unclear if the /// We were unable to process this message due to an internal error. It's unclear if the
/// message is valid. /// message is valid.
BeaconChainError(Box<BeaconChainError>), BeaconChainError(Box<BeaconChainError>),
/// An error reading beacon state.
///
/// ## Peer scoring
///
/// We were unable to process this message due to an internal error.
BeaconStateError(BeaconStateError),
} }
impl From<BeaconChainError> for Error { impl From<BeaconChainError> for Error {
@@ -106,11 +100,5 @@ impl From<BeaconChainError> for Error {
} }
} }
impl From<BeaconStateError> for Error {
fn from(e: BeaconStateError) -> Self {
Error::BeaconStateError(e)
}
}
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;

View File

@@ -260,20 +260,16 @@ fn duplicate_after_valid() {
)); ));
} }
/// Exercises the `partial_state_advance` fallback in gossip verification when /// Exercises payload attestation gossip verification when the message epoch is ahead of the
/// the head state is too stale to compute PTC membership (e.g., during a /// canonical head due to many missed slots.
/// network liveness failure with many missed slots).
#[tokio::test] #[tokio::test]
async fn stale_head_with_partial_advance() { async fn stale_head_payload_attestation() {
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
return; return;
} }
let slots_per_epoch = E::slots_per_epoch(); let slots_per_epoch = E::slots_per_epoch();
// Head at epoch 1, message at epoch 5 4 epochs of missed slots. // Head at epoch 1, message at epoch 5: 4 epochs of missed slots.
// This exceeds min_seed_lookahead (1), triggering the fallback path:
// get_advanced_hot_state loads the stored state, then partial_state_advance
// advances it through epoch boundaries to populate ptc_window.
let head_slot = Slot::new(slots_per_epoch); let head_slot = Slot::new(slots_per_epoch);
let missed_epochs = 4; let missed_epochs = 4;
let target_slot = Slot::new(slots_per_epoch * (1 + missed_epochs)); let target_slot = Slot::new(slots_per_epoch * (1 + missed_epochs));
@@ -292,7 +288,7 @@ async fn stale_head_with_partial_advance() {
let head_epoch = head.snapshot.beacon_state.current_epoch(); let head_epoch = head.snapshot.beacon_state.current_epoch();
assert!( assert!(
target_epoch > head_epoch + harness.spec.min_seed_lookahead, target_epoch > head_epoch + harness.spec.min_seed_lookahead,
"precondition: message epoch must exceed head + min_seed_lookahead to trigger fallback" "precondition: message epoch must exceed head + min_seed_lookahead"
); );
// GIVEN a slot clock advanced to epoch 5 without producing blocks // GIVEN a slot clock advanced to epoch 5 without producing blocks
@@ -317,7 +313,9 @@ async fn stale_head_with_partial_advance() {
.expect("should get PTC from reference state"); .expect("should get PTC from reference state");
let validator_index = *ptc.0.first().expect("PTC should have at least one member") as u64; let validator_index = *ptc.0.first().expect("PTC should have at least one member") as u64;
// WHEN a properly-signed payload attestation from a PTC member is verified. // WHEN a properly-signed payload attestation from a PTC member is verified. The signature
// domain should come from the spec fork schedule and genesis validators root, not a loaded
// state in the verifier.
let domain = harness.spec.get_domain( let domain = harness.spec.get_domain(
target_epoch, target_epoch,
Domain::PTCAttester, Domain::PTCAttester,

View File

@@ -4242,8 +4242,7 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
); );
} }
PayloadAttestationError::MissingPTC { .. } PayloadAttestationError::MissingPTC { .. }
| PayloadAttestationError::BeaconChainError(_) | PayloadAttestationError::BeaconChainError(_) => {
| PayloadAttestationError::BeaconStateError(_) => {
debug!( debug!(
%peer_id, %peer_id,
%message_slot, %message_slot,

View File

@@ -363,6 +363,30 @@ pub fn indexed_payload_attestation_signature_set<'a, 'b, E, F>(
indexed_payload_attestation: &'b IndexedPayloadAttestation<E>, indexed_payload_attestation: &'b IndexedPayloadAttestation<E>,
spec: &'a ChainSpec, spec: &'a ChainSpec,
) -> Result<SignatureSet<'a>> ) -> Result<SignatureSet<'a>>
where
E: EthSpec,
F: Fn(usize) -> Option<Cow<'a, PublicKey>>,
{
let fork = state.fork();
indexed_payload_attestation_signature_set_from_pubkeys(
get_pubkey,
signature,
indexed_payload_attestation,
&fork,
state.genesis_validators_root(),
spec,
)
}
pub fn indexed_payload_attestation_signature_set_from_pubkeys<'a, 'b, E, F>(
get_pubkey: F,
signature: &'a AggregateSignature,
indexed_payload_attestation: &'b IndexedPayloadAttestation<E>,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &'a ChainSpec,
) -> Result<SignatureSet<'a>>
where where
E: EthSpec, E: EthSpec,
F: Fn(usize) -> Option<Cow<'a, PublicKey>>, F: Fn(usize) -> Option<Cow<'a, PublicKey>>,
@@ -378,12 +402,7 @@ where
.data .data
.slot .slot
.epoch(E::slots_per_epoch()); .epoch(E::slots_per_epoch());
let domain = spec.get_domain( let domain = spec.get_domain(epoch, Domain::PTCAttester, fork, genesis_validators_root);
epoch,
Domain::PTCAttester,
&state.fork(),
state.genesis_validators_root(),
);
let message = indexed_payload_attestation.data.signing_root(domain); let message = indexed_payload_attestation.data.signing_root(domain);