Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
267 changes: 257 additions & 10 deletions anchor/common/ssv_types/src/consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -615,8 +615,8 @@ impl QbftData for BeaconVote {

pub struct BeaconVoteValidator<E: EthSpec> {
slot: Slot,
slashing_database: Arc<SlashingDatabase>,
disable_slashing_protection: bool,
// `None` if slashing protection is disabled via CLI.
slashing_database: Option<Arc<SlashingDatabase>>,
spec: Arc<ChainSpec>,
validator_attestation_committees: HashMap<PublicKeyBytes, u64>,
genesis_validators_root: Hash256,
Expand All @@ -638,16 +638,14 @@ impl<E: EthSpec> QbftDataValidator<BeaconVote> for BeaconVoteValidator<E> {
impl<E: EthSpec> BeaconVoteValidator<E> {
pub fn new(
slot: Slot,
slashing_database: Arc<SlashingDatabase>,
disable_slashing_protection: bool,
slashing_database: Option<Arc<SlashingDatabase>>,
spec: Arc<ChainSpec>,
validator_attestation_committees: HashMap<PublicKeyBytes, u64>,
genesis_validators_root: Hash256,
) -> Self {
Self {
slot,
slashing_database,
disable_slashing_protection,
spec,
validator_attestation_committees,
genesis_validators_root,
Expand All @@ -658,7 +656,7 @@ impl<E: EthSpec> BeaconVoteValidator<E> {
pub fn do_validation(
&self,
value: &BeaconVote,
_our_value: &BeaconVote,
our_value: &BeaconVote,
) -> Result<(), BeaconVoteValidationError> {
// Check target epoch is not too far in the future
let current_epoch = self.slot.epoch(E::slots_per_epoch());
Expand All @@ -679,10 +677,25 @@ impl<E: EthSpec> BeaconVoteValidator<E> {
)));
}

// Check slashing protection for all validator public keys
if !self.disable_slashing_protection {
self.check_attestation_slashing(value)?;
// Majority fork protection:
// If we disagree on the epoch to finalize, we fail validation to avoid deciding on an
// attestation that tries to finalize a potentially faulty fork.
// https://github.com/ssvlabs/ssv-spec/issues/555
if value.source != our_value.source {
return Err(BeaconVoteValidationError::DifferentSource {
our: our_value.source,
proposed: value.source,
});
}
if value.target != our_value.target {
return Err(BeaconVoteValidationError::DifferentTarget {
our: our_value.target,
proposed: value.target,
});
}

// Check slashing protection for all validator public keys
self.check_attestation_slashing(value)?;

Ok(())
}
Expand All @@ -691,6 +704,10 @@ impl<E: EthSpec> BeaconVoteValidator<E> {
&self,
value: &BeaconVote,
) -> Result<(), BeaconVoteValidationError> {
let Some(slashing_database) = &self.slashing_database else {
return Ok(());
};

// Create attestation data for slashing protection check
let mut attestation_data = AttestationData {
slot: self.slot,
Expand All @@ -711,7 +728,7 @@ impl<E: EthSpec> BeaconVoteValidator<E> {

for (validator_pubkey, committee_index) in &self.validator_attestation_committees {
attestation_data.index = *committee_index;
self.slashing_database
slashing_database
.preliminary_check_attestation(validator_pubkey, &attestation_data, domain_hash)
.map_err(BeaconVoteValidationError::SlashableAttestation)?;
}
Expand All @@ -728,6 +745,236 @@ pub enum BeaconVoteValidationError {
FarFutureTargetEpoch(String),
#[error("Invalid epoch order: {0}")]
TargetNotAfterSource(String),
#[error("Different source: our {our:?}, proposed {proposed:?}")]
DifferentSource {
our: Checkpoint,
proposed: Checkpoint,
},
#[error("Different target: our {our:?}, proposed {proposed:?}")]
DifferentTarget {
our: Checkpoint,
proposed: Checkpoint,
},
#[error("Attestation would be slashable: {0}")]
SlashableAttestation(NotSafe),
}

#[cfg(test)]
mod tests {
use std::collections::HashMap;

use types::{Checkpoint, Epoch, FixedBytesExtended, MainnetEthSpec};

use super::*;

/// Helper function to create a BeaconVoteValidator for testing.
/// This validator has slashing protection disabled for simpler testing.
fn create_test_validator() -> BeaconVoteValidator<MainnetEthSpec> {
let spec = Arc::new(ChainSpec::mainnet());
let validator_attestation_committees = HashMap::new();
let genesis_validators_root = Hash256::zero();
let slot = Slot::new(100);

BeaconVoteValidator::new(
slot,
None,
spec,
validator_attestation_committees,
genesis_validators_root,
)
}

#[test]
fn test_mismatched_source_different_epochs() {
let validator = create_test_validator();

let our_source = Checkpoint {
epoch: Epoch::new(2),
root: Hash256::from_low_u64_be(1),
};
let our_target = Checkpoint {
epoch: Epoch::new(3),
root: Hash256::from_low_u64_be(2),
};
let our_vote = BeaconVote {
block_root: Hash256::random(),
source: our_source,
target: our_target,
};

// Create a proposed vote with different source epoch
let proposed_source = Checkpoint {
epoch: Epoch::new(1), // Different epoch
root: Hash256::from_low_u64_be(1),
};
let proposed_vote = BeaconVote {
block_root: Hash256::random(),
source: proposed_source,
target: our_target,
};

let result = validator.do_validation(&proposed_vote, &our_vote);
assert!(result.is_err());
match result.unwrap_err() {
BeaconVoteValidationError::DifferentSource { our, proposed } => {
assert_eq!(our, our_source);
assert_eq!(proposed, proposed_source);
}
err => panic!("Expected DifferentSource error, got: {:?}", err),
}
}

#[test]
fn test_mismatched_source_same_epoch_different_roots() {
let validator = create_test_validator();

let our_source = Checkpoint {
epoch: Epoch::new(2),
root: Hash256::from_low_u64_be(1),
};
let our_target = Checkpoint {
epoch: Epoch::new(3),
root: Hash256::from_low_u64_be(2),
};
let our_vote = BeaconVote {
block_root: Hash256::random(),
source: our_source,
target: our_target,
};

// Create a proposed vote with same source epoch but different root
let proposed_source = Checkpoint {
epoch: Epoch::new(2), // Same epoch
root: Hash256::from_low_u64_be(999), // Different root
};
let proposed_vote = BeaconVote {
block_root: Hash256::random(),
source: proposed_source,
target: our_target,
};

let result = validator.do_validation(&proposed_vote, &our_vote);
assert!(result.is_err());
match result.unwrap_err() {
BeaconVoteValidationError::DifferentSource { our, proposed } => {
assert_eq!(our, our_source);
assert_eq!(proposed, proposed_source);
}
err => panic!("Expected DifferentSource error, got: {:?}", err),
}
}

#[test]
fn test_mismatched_target_different_epochs() {
let validator = create_test_validator();

let our_source = Checkpoint {
epoch: Epoch::new(2),
root: Hash256::from_low_u64_be(1),
};
let our_target = Checkpoint {
epoch: Epoch::new(3),
root: Hash256::from_low_u64_be(2),
};
let our_vote = BeaconVote {
block_root: Hash256::random(),
source: our_source,
target: our_target,
};

// Create a proposed vote with different target epoch
let proposed_target = Checkpoint {
epoch: Epoch::new(4), // Different epoch (but still valid, current epoch is 3, max is 4)
root: Hash256::from_low_u64_be(2),
};
let proposed_vote = BeaconVote {
block_root: Hash256::random(),
source: our_source,
target: proposed_target,
};

let result = validator.do_validation(&proposed_vote, &our_vote);
assert!(result.is_err());
match result.unwrap_err() {
BeaconVoteValidationError::DifferentTarget { our, proposed } => {
assert_eq!(our, our_target);
assert_eq!(proposed, proposed_target);
}
err => panic!("Expected DifferentTarget error, got: {:?}", err),
}
}

#[test]
fn test_mismatched_target_same_epoch_different_roots() {
let validator = create_test_validator();

let our_source = Checkpoint {
epoch: Epoch::new(2),
root: Hash256::from_low_u64_be(1),
};
let our_target = Checkpoint {
epoch: Epoch::new(3),
root: Hash256::from_low_u64_be(2),
};
let our_vote = BeaconVote {
block_root: Hash256::random(),
source: our_source,
target: our_target,
};

// Create a proposed vote with same target epoch but different root
let proposed_target = Checkpoint {
epoch: Epoch::new(3), // Same epoch
root: Hash256::from_low_u64_be(999), // Different root
};
let proposed_vote = BeaconVote {
block_root: Hash256::random(),
source: our_source,
target: proposed_target,
};

let result = validator.do_validation(&proposed_vote, &our_vote);
assert!(result.is_err());
match result.unwrap_err() {
BeaconVoteValidationError::DifferentTarget { our, proposed } => {
assert_eq!(our, our_target);
assert_eq!(proposed, proposed_target);
}
err => panic!("Expected DifferentTarget error, got: {:?}", err),
}
}

#[test]
fn test_valid_matching_checkpoints() {
let validator = create_test_validator();

let source = Checkpoint {
epoch: Epoch::new(2),
root: Hash256::from_low_u64_be(1),
};
let target = Checkpoint {
epoch: Epoch::new(3),
root: Hash256::from_low_u64_be(2),
};

let our_vote = BeaconVote {
block_root: Hash256::random(),
source,
target,
};
// Proposed vote has same source and target (but different head vote)
let proposed_vote = BeaconVote {
block_root: Hash256::random(),
source,
target,
};

// This should succeed since checkpoints match
let result = validator.do_validation(&proposed_vote, &our_vote);
assert!(
result.is_ok(),
"Expected validation to succeed for matching checkpoints, got error: {:?}",
result.unwrap_err()
);
}
}
6 changes: 4 additions & 2 deletions anchor/validator_store/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,10 +562,12 @@ impl<T: SlotClock, E: EthSpec> AnchorValidatorStore<T, E> {
slot: Slot,
validator_attestation_committees: HashMap<PublicKeyBytes, u64>,
) -> Box<BeaconVoteValidator<E>> {
let slashing_protection =
(!self.disable_slashing_protection).then(|| Arc::clone(&self.slashing_protection));

Box::new(BeaconVoteValidator::new(
slot,
Arc::clone(&self.slashing_protection),
self.disable_slashing_protection,
slashing_protection,
self.spec.clone(),
validator_attestation_committees,
self.genesis_validators_root,
Expand Down
Loading