Skip to content
Merged
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
7 changes: 7 additions & 0 deletions beacon_node/beacon_chain/src/canonical_head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.start_slot(T::EthSpec::slots_per_epoch()),
);

self.observed_execution_proofs.write().prune(
new_view
.finalized_checkpoint
.epoch
.start_slot(T::EthSpec::slots_per_epoch()),
);

if let Some(event_handler) = self.event_handler.as_ref()
&& event_handler.has_finalized_subscribers()
{
Expand Down
48 changes: 33 additions & 15 deletions beacon_node/beacon_chain/src/observed_execution_proofs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

use bls::PublicKeyBytes;
use std::collections::{HashMap, HashSet};
use types::{Hash256, ProofType};
use types::{Hash256, ProofType, Slot};

/// Gossip deduplication cache for execution proofs.
///
Expand All @@ -22,6 +22,10 @@ pub struct ObservedExecutionProofs {
/// Tracks `(request_root, proof_type, validator_pubkey)` triples we have already attempted
/// to verify (regardless of outcome). Used to implement IGNORE-3.
seen_from_validator: HashSet<(Hash256, ProofType, PublicKeyBytes)>,

/// Maps slot → set of request roots observed at that slot. Populated when a valid/accepted
/// proof is observed. Used to prune `valid_proofs` and `seen_from_validator` at finalization.
slot_to_request_roots: HashMap<Slot, HashSet<Hash256>>,
}

/// Result of checking the dedup cache.
Expand Down Expand Up @@ -74,20 +78,35 @@ impl ObservedExecutionProofs {
.insert((request_root, proof_type, validator_pubkey));
}

/// Record that a valid proof was received for `(request_root, proof_type)`.
pub fn observe_valid_proof(&mut self, request_root: Hash256, proof_type: ProofType) {
/// Record that a valid proof was received for `(request_root, proof_type)` at `slot`.
pub fn observe_valid_proof(
&mut self,
request_root: Hash256,
proof_type: ProofType,
slot: Slot,
) {
self.valid_proofs.insert((request_root, proof_type), ());
self.slot_to_request_roots
.entry(slot)
.or_default()
.insert(request_root);
}

/// Prune entries for finalized request roots.
/// Prune entries for request roots whose slot is at or below `finalized_slot`.
///
/// Call at finalization. Any `request_root` whose block is finalized will never need
/// dedup again, so we can drop its entries.
pub fn prune(&mut self, finalized_request_roots: &HashSet<Hash256>) {
/// Call at finalization. Any proof for a finalized block will never need dedup again.
/// Entries in `seen_from_validator` without a known slot (e.g. for proofs that failed
/// BLS or engine verification) are retained — those validators are typically banned anyway.
pub fn prune(&mut self, finalized_slot: Slot) {
let pruned_roots: HashSet<Hash256> = self
.slot_to_request_roots
.extract_if(|&slot, _| slot <= finalized_slot)
.flat_map(|(_, roots)| roots)
.collect();
self.valid_proofs
.retain(|(root, _), _| !finalized_request_roots.contains(root));
.retain(|(root, _), _| !pruned_roots.contains(root));
self.seen_from_validator
.retain(|(root, _, _)| !finalized_request_roots.contains(root));
.retain(|(root, _, _)| !pruned_roots.contains(root));
}

/// Number of valid-proof entries (for metrics / tests).
Expand Down Expand Up @@ -126,7 +145,7 @@ mod tests {
let root = Hash256::repeat_byte(0x01);
let pk = test_pubkey(99);

cache.observe_valid_proof(root, 1);
cache.observe_valid_proof(root, 1, Slot::new(1));

// Same (root, type) from a different validator → still IGNORE
assert_eq!(
Expand Down Expand Up @@ -168,14 +187,13 @@ mod tests {
let pk_43 = test_pubkey(43);
let pk_99 = test_pubkey(99);

cache.observe_valid_proof(root_a, 1);
cache.observe_valid_proof(root_b, 1);
// root_a at slot 10 (will be finalized), root_b at slot 20 (will be retained).
cache.observe_valid_proof(root_a, 1, Slot::new(10));
cache.observe_valid_proof(root_b, 1, Slot::new(20));
cache.observe_verification_attempt(root_a, 1, pk_42);
cache.observe_verification_attempt(root_b, 1, pk_43);

let mut finalized = HashSet::new();
finalized.insert(root_a);
cache.prune(&finalized);
cache.prune(Slot::new(15));

assert_eq!(cache.valid_proof_count(), 1);
assert_eq!(cache.seen_from_validator_count(), 1);
Expand Down
21 changes: 11 additions & 10 deletions beacon_node/network/src/network_beacon_processor/gossip_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2109,12 +2109,11 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
);

// Layer A: record valid proof for IGNORE-2 dedup.
self.chain
.observed_execution_proofs
.write()
.observe_valid_proof(request_root, proof_type);

if let Some((block_root, slot)) = verified_block {
self.chain
.observed_execution_proofs
.write()
.observe_valid_proof(request_root, proof_type, slot);
self.network_globals
.set_local_execution_proof_status(ExecutionProofStatus {
slot: slot.as_u64(),
Expand Down Expand Up @@ -2153,7 +2152,7 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
"invalid_execution_proof",
);
}
Ok((ProofStatus::Accepted, _)) => {
Ok((ProofStatus::Accepted, verified_block)) => {
debug!(
?request_root,
validator_index,
Expand All @@ -2162,10 +2161,12 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
);

// Layer A: record valid proof for IGNORE-2 dedup (accepted counts as valid).
self.chain
.observed_execution_proofs
.write()
.observe_valid_proof(request_root, proof_type);
if let Some((_, slot)) = verified_block {
self.chain
.observed_execution_proofs
.write()
.observe_valid_proof(request_root, proof_type, slot);
}

self.propagate_validation_result(message_id, peer_id, gossip_behaviour);
}
Expand Down
Loading