Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions beacon_node/beacon_chain/src/beacon_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ use crate::payload_attestation_verification::VerifiedPayloadAttestationMessage;
use crate::payload_bid_verification::payload_bid_cache::GossipVerifiedPayloadBidCache;
#[cfg(not(test))]
use crate::payload_envelope_streamer::{EnvelopeRequestSource, launch_payload_envelope_stream};
use crate::payload_envelope_verification::gossip_verified_envelope_cache::GossipVerifiedEnvelopeCache;
use crate::pending_payload_cache::PendingPayloadCache;
use crate::pending_payload_cache::{
Availability as PayloadAvailability,
Expand Down Expand Up @@ -485,6 +486,8 @@ pub struct BeaconChain<T: BeaconChainTypes> {
pub pre_finalization_block_cache: PreFinalizationBlockCache,
/// A cache used to store gossip verified payload bids.
pub gossip_verified_payload_bid_cache: GossipVerifiedPayloadBidCache<T>,
/// A cache used to deduplicate gossip verified execution payload envelopes.
pub gossip_verified_envelope_cache: GossipVerifiedEnvelopeCache,
/// A cache used to store gossip verified proposer preferences.
pub gossip_verified_proposer_preferences_cache: GossipVerifiedProposerPreferenceCache,
/// A cache used to produce light_client server messages
Expand Down Expand Up @@ -6952,6 +6955,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
self.block_times_cache.write().prune(slot);
self.envelope_times_cache.write().prune(slot);
self.gossip_verified_payload_bid_cache.prune(slot);
self.gossip_verified_envelope_cache.prune(slot);
self.gossip_verified_proposer_preferences_cache.prune(slot);

// Don't run heavy-weight tasks during sync.
Expand Down
1 change: 1 addition & 0 deletions beacon_node/beacon_chain/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,7 @@ where
kzg: self.kzg.clone(),
rng: Arc::new(Mutex::new(rng)),
gossip_verified_payload_bid_cache: <_>::default(),
gossip_verified_envelope_cache: <_>::default(),
gossip_verified_proposer_preferences_cache: <_>::default(),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ use crate::{
beacon_proposer_cache::{self, BeaconProposerCache},
canonical_head::CanonicalHead,
payload_envelope_verification::{
EnvelopeError, EnvelopeProcessingSnapshot, load_snapshot_from_state_root,
EnvelopeError, EnvelopeProcessingSnapshot,
gossip_verified_envelope_cache::GossipVerifiedEnvelopeCache, load_snapshot_from_state_root,
},
validator_pubkey_cache::ValidatorPubkeyCache,
};
Expand All @@ -30,6 +31,7 @@ pub struct GossipVerificationContext<'a, T: BeaconChainTypes> {
pub validator_pubkey_cache: &'a RwLock<ValidatorPubkeyCache<T>>,
pub genesis_validators_root: Hash256,
pub event_handler: &'a Option<ServerSentEventHandler<T::EthSpec>>,
pub gossip_verified_envelope_cache: &'a GossipVerifiedEnvelopeCache,
}

/// Verify that an execution payload envelope is consistent with its beacon block
Expand Down Expand Up @@ -120,8 +122,22 @@ impl<T: BeaconChainTypes> GossipVerifiedEnvelope<T> {
.epoch
.start_slot(T::EthSpec::slots_per_epoch());

// TODO(EIP-7732): check that we haven't seen another valid `SignedExecutionPayloadEnvelope`
// for this block root from this builder - envelope status table check
// Ignore a second valid `SignedExecutionPayloadEnvelope` for this block root from this
// builder, per the Gloas `execution_payload` gossip rules. Only envelopes that pass the
// full verification below are recorded (see the insert before returning `Ok`), so an
// earlier invalid envelope from this builder never suppresses a later valid one. Checking
// here, before the state load and signature verification, keeps duplicates cheap.
if ctx.gossip_verified_envelope_cache.seen_envelope(
&envelope.slot(),
beacon_block_root,
envelope.builder_index,
) {
return Err(EnvelopeError::EnvelopeAlreadySeen {
block_root: beacon_block_root,
builder_index: envelope.builder_index,
});
}

let block = match ctx.store.try_get_full_block(&beacon_block_root)? {
Some(DatabaseBlock::Full(block)) => Arc::new(block),
Some(DatabaseBlock::Blinded(_)) | None => {
Expand Down Expand Up @@ -215,6 +231,14 @@ impl<T: BeaconChainTypes> GossipVerifiedEnvelope<T> {
return Err(EnvelopeError::BadSignature);
}

// Record this now fully-verified envelope so any subsequent duplicate for the same block
// root from the same builder is ignored by the `seen_envelope` check above.
ctx.gossip_verified_envelope_cache.insert_seen_envelope(
block_slot,
beacon_block_root,
builder_index,
);

if let Some(event_handler) = ctx.event_handler.as_ref()
&& event_handler.has_execution_payload_gossip_subscribers()
{
Expand Down Expand Up @@ -251,6 +275,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
validator_pubkey_cache: &self.validator_pubkey_cache,
genesis_validators_root: self.genesis_validators_root,
event_handler: &self.event_handler,
gossip_verified_envelope_cache: &self.gossip_verified_envelope_cache,
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use std::collections::{BTreeMap, HashSet};

use parking_lot::RwLock;
use types::{BuilderIndex, Hash256, Slot};

/// Tracks which execution payload envelopes have already been seen over gossip, keyed by
/// `(block_root, builder_index)`, so that a second valid `SignedExecutionPayloadEnvelope` for the
/// same block root from the same builder can be ignored per the Gloas `execution_payload` gossip
/// rules.
///
/// This is the envelope counterpart of the seen-builder half of
/// [`GossipVerifiedPayloadBidCache`](crate::payload_bid_verification::payload_bid_cache::GossipVerifiedPayloadBidCache).
/// Unlike the bid cache it does not need to be generic over `BeaconChainTypes`, because it only
/// stores the `(block_root, builder_index)` identity rather than a verified message. The `Slot` is
/// retained purely so entries can be pruned once they fall below the current slot; the dedup
/// identity is `(block_root, builder_index)`.
#[derive(Default)]
pub struct GossipVerifiedEnvelopeCache {
seen_envelope: RwLock<BTreeMap<Slot, HashSet<(Hash256, BuilderIndex)>>>,
}

impl GossipVerifiedEnvelopeCache {
/// A gossip verified envelope for `(block_root, builder_index)` already exists at `slot`.
pub fn seen_envelope(
&self,
slot: &Slot,
block_root: Hash256,
builder_index: BuilderIndex,
) -> bool {
self.seen_envelope
.read()
.get(slot)
.is_some_and(|seen_envelopes| seen_envelopes.contains(&(block_root, builder_index)))
}

/// Record that a valid envelope for `(block_root, builder_index)` at `slot` has been seen.
pub fn insert_seen_envelope(
&self,
slot: Slot,
block_root: Hash256,
builder_index: BuilderIndex,
) {
self.seen_envelope
.write()
.entry(slot)
.or_default()
.insert((block_root, builder_index));
}

/// Prune anything before `current_slot`.
pub fn prune(&self, current_slot: Slot) {
self.seen_envelope
.write()
.retain(|&slot, _| slot >= current_slot);
}
}

#[cfg(test)]
mod tests {
use super::GossipVerifiedEnvelopeCache;
use types::{Hash256, Slot};

fn root(byte: u8) -> Hash256 {
Hash256::repeat_byte(byte)
}

#[test]
fn seen_envelope_discriminates_by_block_root_and_builder() {
let cache = GossipVerifiedEnvelopeCache::default();
let slot = Slot::new(3);

assert!(!cache.seen_envelope(&slot, root(1), 7));

cache.insert_seen_envelope(slot, root(1), 7);

// Same (block_root, builder_index) at the same slot is now seen.
assert!(cache.seen_envelope(&slot, root(1), 7));
// A different builder for the same block root is not seen.
assert!(!cache.seen_envelope(&slot, root(1), 8));
// A different block root for the same builder is not seen.
assert!(!cache.seen_envelope(&slot, root(2), 7));
// The same identity at a different slot is tracked independently.
assert!(!cache.seen_envelope(&Slot::new(4), root(1), 7));
}

#[test]
fn prune_removes_old_retains_current() {
let cache = GossipVerifiedEnvelopeCache::default();

for slot in [1, 2, 3, 7, 8, 9, 10] {
cache.insert_seen_envelope(Slot::new(slot), root(slot as u8), slot);
}

cache.prune(Slot::new(8));

// Slots 1-7 pruned.
for slot in [1, 2, 3, 7] {
assert!(!cache.seen_envelope(&Slot::new(slot), root(slot as u8), slot));
}
// Slots 8-10 retained.
for slot in [8, 9, 10] {
assert!(cache.seen_envelope(&Slot::new(slot), root(slot as u8), slot));
}
}
}
11 changes: 11 additions & 0 deletions beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ use types::{

pub mod execution_pending_envelope;
pub mod gossip_verified_envelope;
pub mod gossip_verified_envelope_cache;
pub mod import;
mod payload_notifier;

Expand Down Expand Up @@ -200,6 +201,15 @@ impl<E: EthSpec> AvailableExecutedEnvelope<E> {
pub enum EnvelopeError {
/// The envelope's block root is unknown.
BlockRootUnknown { block_root: Hash256 },
/// A valid envelope for this block root from this builder has already been seen over gossip.
///
/// Per the Gloas `execution_payload` gossip rules, a node forwards at most one valid
/// `SignedExecutionPayloadEnvelope` for a given `(block_root, builder_index)`, so duplicates
/// are ignored (not penalized).
EnvelopeAlreadySeen {
block_root: Hash256,
builder_index: u64,
},
/// The signature is invalid.
BadSignature,
/// The builder index doesn't match the committed bid
Expand Down Expand Up @@ -262,6 +272,7 @@ impl EnvelopeError {
| EnvelopeError::EnvelopeProcessingError(_) => true,
EnvelopeError::ExecutionPayloadError(e) => e.penalize_peer(),
EnvelopeError::BlockRootUnknown { .. }
| EnvelopeError::EnvelopeAlreadySeen { .. }
| EnvelopeError::PriorToFinalization { .. }
| EnvelopeError::BeaconChainError(_)
| EnvelopeError::BeaconStateError(_)
Expand Down
64 changes: 64 additions & 0 deletions beacon_node/beacon_chain/tests/events.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use arbitrary::Arbitrary;
use beacon_chain::data_column_verification::GossipVerifiedDataColumn;
use beacon_chain::payload_envelope_verification::EnvelopeError;
use beacon_chain::test_utils::{
BeaconChainHarness, fork_name_from_env, generate_data_column_sidecars_from_block, test_spec,
};
Expand Down Expand Up @@ -336,6 +337,69 @@ async fn execution_payload_envelope_events() {
);
}

/// Verifies that a second valid `SignedExecutionPayloadEnvelope` for the same block root from the
/// same builder is ignored (`EnvelopeAlreadySeen`) during gossip verification, per the Gloas
/// `execution_payload` gossip deduplication rule.
#[tokio::test]
async fn duplicate_execution_payload_envelope_is_ignored() {
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
return;
}

let harness = BeaconChainHarness::builder(E::default())
.default_spec()
.deterministic_keypairs(64)
.fresh_ephemeral_store()
.mock_execution_layer()
.build();

harness.extend_to_slot(Slot::new(1)).await;

let state = harness.get_current_state();
let target_slot = Slot::new(2);
harness.advance_slot();
let (block_contents, opt_envelope, _new_state) =
harness.make_block_with_envelope(state, target_slot).await;

let block_root = block_contents.0.canonical_root();

harness
.process_block(target_slot, block_root, block_contents)
.await
.expect("block should be processed");

let signed_envelope = opt_envelope.expect("Gloas block should produce an envelope");
let envelope = Arc::new(signed_envelope);

let event_handler = harness.chain.event_handler.as_ref().unwrap();
let mut gossip_receiver = event_handler.subscribe_execution_payload_gossip();

// The first envelope passes gossip verification and fires the gossip event.
harness
.chain
.verify_envelope_for_gossip(envelope.clone())
.await
.expect("first envelope gossip verification should succeed");
assert!(
gossip_receiver.try_recv().is_ok(),
"the first envelope should fire an execution_payload_gossip event"
);

// A second, identical envelope is ignored as a duplicate and does not re-propagate.
let duplicate_result = harness.chain.verify_envelope_for_gossip(envelope).await;
assert!(
matches!(
duplicate_result,
Err(EnvelopeError::EnvelopeAlreadySeen { .. })
),
"the duplicate envelope should be ignored, got {duplicate_result:?}"
);
assert!(
gossip_receiver.try_recv().is_err(),
"the duplicate envelope should not fire another gossip event"
);
}

/// Verifies that a `payload_attestation_message` event is emitted when a payload attestation
/// message passes gossip verification.
#[tokio::test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3833,6 +3833,23 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
}
}

EnvelopeError::EnvelopeAlreadySeen {
block_root,
builder_index,
} => {
debug!(
?block_root,
%builder_index,
"Ignoring duplicate execution payload envelope"
);

self.propagate_validation_result(
message_id,
peer_id,
MessageAcceptance::Ignore,
);
}

EnvelopeError::PriorToFinalization { .. }
| EnvelopeError::BeaconChainError(_)
| EnvelopeError::BeaconStateError(_)
Expand Down