Add gossip validation spec tests for proposer/attester slashings (#9323)

Addresses #9232 partially. This PR covers two topics only.
* #9232

Wires up networking test vectors for `gossip_proposer_slashing` and `gossip_attester_slashing` topics.

The tests also revealed minor spec non-compliance where invalid slashings were ignored rather than rejected.


  - Refactor `process_gossip_proposer_slashing` and `process_gossip_attester_slashing` to return `MessageAcceptance`, so it can be verified in the tests
- Add `GossipValidation` test case, handler, and test entries
- Spec compliance fix: distinguish between internal errors and validation error - return `Reject` when the slashing is invalid and only penalise on invalid messages


Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com>
This commit is contained in:
Jimmy Chen
2026-05-28 10:27:16 +10:00
committed by GitHub
parent dfb259171a
commit 5636030b49
10 changed files with 374 additions and 63 deletions

View File

@@ -11,6 +11,7 @@ mod subnet_service;
mod sync;
pub use lighthouse_network::NetworkConfig;
pub use network_beacon_processor::NetworkBeaconProcessor;
pub use service::{
NetworkMessage, NetworkReceivers, NetworkSenders, NetworkService, ValidatorSubscriptionMessage,
};

View File

@@ -174,6 +174,17 @@ impl<E: EthSpec> FailedAtt<E> {
}
}
/// `MessageAcceptance` doesn't implement clone so we do a manual match here.
/// TODO: remove this once `Clone` is available on this type:
/// https://github.com/libp2p/rust-libp2p/pull/6445
fn clone_message_acceptance(a: &MessageAcceptance) -> MessageAcceptance {
match a {
MessageAcceptance::Accept => MessageAcceptance::Accept,
MessageAcceptance::Reject => MessageAcceptance::Reject,
MessageAcceptance::Ignore => MessageAcceptance::Ignore,
}
}
impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
/* Auxiliary functions */
@@ -2194,14 +2205,14 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
message_id: MessageId,
peer_id: PeerId,
proposer_slashing: ProposerSlashing,
) {
) -> MessageAcceptance {
let validator_index = proposer_slashing.signed_header_1.message.proposer_index;
let slashing = match self
let (validation_result, verified_slashing_opt) = match self
.chain
.verify_proposer_slashing_for_gossip(proposer_slashing)
{
Ok(ObservationOutcome::New(slashing)) => slashing,
Ok(ObservationOutcome::New(slashing)) => (MessageAcceptance::Accept, Some(slashing)),
Ok(ObservationOutcome::AlreadyKnown) => {
debug!(
reason = "Already seen a proposer slashing for that validator",
@@ -2209,44 +2220,54 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
peer = %peer_id,
"Dropping proposer slashing"
);
self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore);
return;
(MessageAcceptance::Ignore, None)
}
Err(e) => {
// This is likely a fault with the beacon chain and not necessarily a
// malicious message from the peer.
debug!(
validator_index,
%peer_id,
error = ?e,
"Dropping invalid proposer slashing"
"Dropping proposer slashing due to an error"
);
self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore);
// Penalize peer slightly for invalids.
self.gossip_penalize_peer(
peer_id,
PeerAction::HighToleranceError,
"invalid_gossip_proposer_slashing",
);
return;
if matches!(e, BeaconChainError::ProposerSlashingValidationError(_)) {
// Penalize peer slightly for invalids.
self.gossip_penalize_peer(
peer_id,
PeerAction::HighToleranceError,
"invalid_gossip_proposer_slashing",
);
(MessageAcceptance::Reject, None)
} else {
// This is likely a fault with the beacon chain and not necessarily a
// malicious message from the peer.
(MessageAcceptance::Ignore, None)
}
}
};
metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_VERIFIED_TOTAL);
self.propagate_validation_result(
message_id,
peer_id,
clone_message_acceptance(&validation_result),
);
self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept);
if let Some(slashing) = verified_slashing_opt {
metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_VERIFIED_TOTAL);
// Register the slashing with any monitored validators.
self.chain
.validator_monitor
.read()
.register_gossip_proposer_slashing(slashing.as_inner());
// Register the slashing with any monitored validators.
self.chain
.validator_monitor
.read()
.register_gossip_proposer_slashing(slashing.as_inner());
self.chain.import_proposer_slashing(slashing);
debug!("Successfully imported proposer slashing");
self.chain.import_proposer_slashing(slashing);
debug!("Successfully imported proposer slashing");
metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_IMPORTED_TOTAL);
metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_IMPORTED_TOTAL);
}
validation_result
}
pub fn process_gossip_attester_slashing(
@@ -2254,51 +2275,64 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
message_id: MessageId,
peer_id: PeerId,
attester_slashing: AttesterSlashing<T::EthSpec>,
) {
let slashing = match self
) -> MessageAcceptance {
let (validation_result, verified_slashing_opt) = match self
.chain
.verify_attester_slashing_for_gossip(attester_slashing)
{
Ok(ObservationOutcome::New(slashing)) => slashing,
Ok(ObservationOutcome::New(slashing)) => (MessageAcceptance::Accept, Some(slashing)),
Ok(ObservationOutcome::AlreadyKnown) => {
debug!(
reason = "Slashings already known for all slashed validators",
peer = %peer_id,
"Dropping attester slashing"
);
self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore);
return;
(MessageAcceptance::Ignore, None)
}
Err(e) => {
debug!(
%peer_id,
error = ?e,
"Dropping invalid attester slashing"
"Dropping attester slashing due to an error"
);
self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore);
// Penalize peer slightly for invalids.
self.gossip_penalize_peer(
peer_id,
PeerAction::HighToleranceError,
"invalid_gossip_attester_slashing",
);
return;
if matches!(e, BeaconChainError::AttesterSlashingValidationError(_)) {
// Penalize peer slightly for invalids.
self.gossip_penalize_peer(
peer_id,
PeerAction::HighToleranceError,
"invalid_gossip_attester_slashing",
);
(MessageAcceptance::Reject, None)
} else {
// This is likely a fault with the beacon chain and not necessarily a
// malicious message from the peer.
(MessageAcceptance::Ignore, None)
}
}
};
metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_VERIFIED_TOTAL);
self.propagate_validation_result(
message_id,
peer_id,
clone_message_acceptance(&validation_result),
);
self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept);
if let Some(slashing) = verified_slashing_opt {
metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_VERIFIED_TOTAL);
// Register the slashing with any monitored validators.
self.chain
.validator_monitor
.read()
.register_gossip_attester_slashing(slashing.as_inner().to_ref());
// Register the slashing with any monitored validators.
self.chain
.validator_monitor
.read()
.register_gossip_attester_slashing(slashing.as_inner().to_ref());
self.chain.import_attester_slashing(slashing);
debug!("Successfully imported attester slashing");
metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_IMPORTED_TOTAL);
self.chain.import_attester_slashing(slashing);
debug!("Successfully imported attester slashing");
metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_IMPORTED_TOTAL);
}
validation_result
}
pub fn process_gossip_bls_to_execution_change(

View File

@@ -7,6 +7,7 @@ use beacon_chain::data_column_verification::{GossipDataColumnError, observe_goss
use beacon_chain::fetch_blobs::{
EngineGetBlobsOutput, FetchEngineBlobError, fetch_and_process_engine_blobs,
};
use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType};
use beacon_chain::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError};
use beacon_processor::{
BeaconProcessorSend, DuplicateCache, GossipAggregatePackage, GossipAttestationPackage, Work,
@@ -20,7 +21,7 @@ use lighthouse_network::rpc::methods::{
};
use lighthouse_network::service::api_types::CustodyBackfillBatchId;
use lighthouse_network::{
Client, GossipTopic, MessageId, NetworkGlobals, PeerId, PubsubMessage,
Client, GossipTopic, MessageId, NetworkConfig, NetworkGlobals, PeerId, PubsubMessage,
rpc::{BlocksByRangeRequest, BlocksByRootRequest, LightClientBootstrapRequest, StatusMessage},
};
use rand::prelude::SliceRandom;
@@ -31,6 +32,10 @@ use task_executor::TaskExecutor;
use tokio::sync::mpsc::{self, error::TrySendError};
use tracing::{debug, error, instrument, trace, warn};
use types::*;
use {
beacon_chain::builder::Witness, beacon_processor::BeaconProcessorChannels,
slot_clock::ManualSlotClock, store::MemoryStore, tokio::sync::mpsc::UnboundedSender,
};
pub use sync_methods::ChainSegmentProcessId;
use types::data::FixedBlobSidecarList;
@@ -353,7 +358,7 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
) -> Result<(), Error<T::EthSpec>> {
let processor = self.clone();
let process_fn = move || {
processor.process_gossip_proposer_slashing(message_id, peer_id, *proposer_slashing)
processor.process_gossip_proposer_slashing(message_id, peer_id, *proposer_slashing);
};
self.try_send(BeaconWorkEvent {
@@ -420,7 +425,7 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
) -> Result<(), Error<T::EthSpec>> {
let processor = self.clone();
let process_fn = move || {
processor.process_gossip_attester_slashing(message_id, peer_id, *attester_slashing)
processor.process_gossip_attester_slashing(message_id, peer_id, *attester_slashing);
};
self.try_send(BeaconWorkEvent {
@@ -1260,16 +1265,8 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
}
}
#[cfg(test)]
use {
beacon_chain::builder::Witness, beacon_processor::BeaconProcessorChannels,
slot_clock::ManualSlotClock, store::MemoryStore, tokio::sync::mpsc::UnboundedSender,
};
#[cfg(test)]
pub(crate) type TestBeaconChainType<E> = Witness<ManualSlotClock, E, MemoryStore, MemoryStore>;
#[cfg(test)]
impl<E: EthSpec> NetworkBeaconProcessor<TestBeaconChainType<E>> {
// Instantiates a mostly non-functional version of `Self` and returns the
// event receiver that would normally go to the beacon processor. This is
@@ -1301,4 +1298,22 @@ impl<E: EthSpec> NetworkBeaconProcessor<TestBeaconChainType<E>> {
(network_beacon_processor, beacon_processor_rx)
}
/// Constructs a mostly non-functional `NetworkBeaconProcessor` from a test harness,
/// suitable for directly calling gossip processing methods in tests.
pub fn null_from_harness(harness: &BeaconChainHarness<EphemeralHarnessType<E>>) -> Self {
let network_globals = NetworkGlobals::new_test_globals(
vec![],
Arc::new(NetworkConfig::default()),
harness.spec.clone(),
);
Self::null_for_testing(
Arc::new(network_globals),
mpsc::unbounded_channel().0,
harness.chain.clone(),
harness.runtime.task_executor.clone(),
)
.0
}
}