Skip to content

Commit d9cbc7c

Browse files
authored
feat: QBFT value checks for BeaconVote (#487)
- #436
1 parent 1a8a69b commit d9cbc7c

File tree

5 files changed

+202
-15
lines changed

5 files changed

+202
-15
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

anchor/common/ssv_types/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ openssl = { workspace = true }
1919
operator_key = { workspace = true }
2020
rusqlite = { workspace = true }
2121
sha2 = { workspace = true }
22+
slashing_protection = { workspace = true }
2223
thiserror = { workspace = true }
24+
tracing = { workspace = true }
2325
tree_hash = { workspace = true }
2426
tree_hash_derive = { workspace = true }
2527
types = { workspace = true }

anchor/common/ssv_types/src/consensus.rs

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
use std::{
2+
collections::HashMap,
23
fmt::{Debug, DebugStruct, Display, Formatter},
34
hash::Hash,
5+
marker::PhantomData,
46
ops::Deref,
7+
sync::Arc,
58
};
69

710
use derive_more::{From, Into};
811
use sha2::{Digest, Sha256};
12+
use slashing_protection::{NotSafe, SlashingDatabase};
913
use ssz::{Decode, DecodeError, Encode};
1014
use ssz_derive::{Decode, Encode};
15+
use thiserror::Error;
16+
use tracing::warn;
1117
use tree_hash::{PackedEncoding, TreeHash, TreeHashType};
1218
use tree_hash_derive::TreeHash;
1319
use types::{
14-
Checkpoint, CommitteeIndex, EthSpec, ForkName, Hash256, PublicKeyBytes, Signature, Slot,
15-
SyncCommitteeContribution, VariableList,
20+
AttestationData, ChainSpec, Checkpoint, CommitteeIndex, Domain, EthSpec, ForkName, Hash256,
21+
PublicKeyBytes, Signature, Slot, SyncCommitteeContribution, VariableList,
1622
typenum::{U13, U56},
1723
};
1824

@@ -360,3 +366,122 @@ impl QbftData for BeaconVote {
360366
Hash256::from(hash)
361367
}
362368
}
369+
370+
pub struct BeaconVoteValidator<E: EthSpec> {
371+
slot: Slot,
372+
slashing_database: Arc<SlashingDatabase>,
373+
disable_slashing_protection: bool,
374+
spec: Arc<ChainSpec>,
375+
validator_attestation_committees: HashMap<PublicKeyBytes, u64>,
376+
genesis_validators_root: Hash256,
377+
_phantom: PhantomData<E>,
378+
}
379+
380+
impl<E: EthSpec> QbftDataValidator<BeaconVote> for BeaconVoteValidator<E> {
381+
fn validate(&self, value: &BeaconVote, our_value: &BeaconVote) -> bool {
382+
match self.do_validation(value, our_value) {
383+
Ok(_) => true,
384+
Err(err) => {
385+
warn!(%err, "Operator proposed invalid beacon vote");
386+
false
387+
}
388+
}
389+
}
390+
}
391+
392+
impl<E: EthSpec> BeaconVoteValidator<E> {
393+
pub fn new(
394+
slot: Slot,
395+
slashing_database: Arc<SlashingDatabase>,
396+
disable_slashing_protection: bool,
397+
spec: Arc<ChainSpec>,
398+
validator_attestation_committees: HashMap<PublicKeyBytes, u64>,
399+
genesis_validators_root: Hash256,
400+
) -> Self {
401+
Self {
402+
slot,
403+
slashing_database,
404+
disable_slashing_protection,
405+
spec,
406+
validator_attestation_committees,
407+
genesis_validators_root,
408+
_phantom: PhantomData,
409+
}
410+
}
411+
412+
pub fn do_validation(
413+
&self,
414+
value: &BeaconVote,
415+
_our_value: &BeaconVote,
416+
) -> Result<(), BeaconVoteValidationError> {
417+
// Check target epoch is not too far in the future
418+
let current_epoch = self.slot.epoch(E::slots_per_epoch());
419+
if value.target.epoch > current_epoch + 1 {
420+
return Err(BeaconVoteValidationError::FarFutureTargetEpoch(format!(
421+
"current: {}, target: {}",
422+
current_epoch.as_u64(),
423+
value.target.epoch.as_u64()
424+
)));
425+
}
426+
427+
// Check source epoch < target epoch
428+
if value.source.epoch >= value.target.epoch {
429+
return Err(BeaconVoteValidationError::TargetNotAfterSource(format!(
430+
"source {} >= target {}",
431+
value.source.epoch.as_u64(),
432+
value.target.epoch.as_u64()
433+
)));
434+
}
435+
436+
// Check slashing protection for all validator public keys
437+
if !self.disable_slashing_protection {
438+
self.check_attestation_slashing(value)?;
439+
}
440+
441+
Ok(())
442+
}
443+
444+
fn check_attestation_slashing(
445+
&self,
446+
value: &BeaconVote,
447+
) -> Result<(), BeaconVoteValidationError> {
448+
// Create attestation data for slashing protection check
449+
let mut attestation_data = AttestationData {
450+
slot: self.slot,
451+
index: 0, // Will be individually set below
452+
beacon_block_root: value.block_root,
453+
source: value.source,
454+
target: value.target,
455+
};
456+
457+
let epoch = self.slot.epoch(E::slots_per_epoch());
458+
459+
let domain_hash = self.spec.get_domain(
460+
epoch,
461+
Domain::BeaconAttester,
462+
&self.spec.fork_at_epoch(epoch),
463+
self.genesis_validators_root,
464+
);
465+
466+
for (validator_pubkey, committee_index) in &self.validator_attestation_committees {
467+
attestation_data.index = *committee_index;
468+
self.slashing_database
469+
.preliminary_check_attestation(validator_pubkey, &attestation_data, domain_hash)
470+
.map_err(BeaconVoteValidationError::SlashableAttestation)?;
471+
}
472+
473+
Ok(())
474+
}
475+
}
476+
477+
#[derive(Error, Debug)]
478+
pub enum BeaconVoteValidationError {
479+
#[error("Unable to validate, bad slot clock")]
480+
BadSlotClock,
481+
#[error("Target epoch is too far in future: {0}")]
482+
FarFutureTargetEpoch(String),
483+
#[error("Invalid epoch order: {0}")]
484+
TargetNotAfterSource(String),
485+
#[error("Attestation would be slashable: {0}")]
486+
SlashableAttestation(NotSafe),
487+
}

anchor/validator_store/src/lib.rs

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ pub mod metadata_service;
22
mod metrics;
33

44
use std::{
5-
collections::HashMap,
5+
collections::{HashMap, HashSet},
66
fmt::Debug,
77
future::Future,
88
num::NonZeroUsize,
@@ -31,11 +31,11 @@ use signature_collector::{
3131
use slashing_protection::{NotSafe, Safe, SlashingDatabase};
3232
use slot_clock::SlotClock;
3333
use ssv_types::{
34-
Cluster, ClusterId, ENCRYPTED_KEY_LENGTH, ValidatorIndex, ValidatorMetadata,
34+
Cluster, ClusterId, CommitteeId, ENCRYPTED_KEY_LENGTH, ValidatorIndex, ValidatorMetadata,
3535
consensus::{
3636
BEACON_ROLE_AGGREGATOR, BEACON_ROLE_PROPOSER, BEACON_ROLE_SYNC_COMMITTEE_CONTRIBUTION,
37-
BeaconVote, Contribution, ContributionWrapper, Contributions, NoDataValidation, QbftData,
38-
ValidatorConsensusData, ValidatorDuty,
37+
BeaconVote, BeaconVoteValidator, Contribution, ContributionWrapper, Contributions,
38+
NoDataValidation, QbftData, ValidatorConsensusData, ValidatorDuty,
3939
},
4040
msgid::Role,
4141
partial_sig::PartialSignatureKind,
@@ -228,7 +228,7 @@ impl<T: SlotClock, E: EthSpec> AnchorValidatorStore<T, E> {
228228
.map(|validator| {
229229
let mut duties = 0;
230230
if let Some(idx) = &validator.index {
231-
if slot_metadata.attesting_validators.contains(idx) {
231+
if slot_metadata.attesting_validator_indices.contains(idx) {
232232
duties += 1;
233233
}
234234
if slot_metadata.sync_validators.contains(idx) {
@@ -525,6 +525,45 @@ impl<T: SlotClock, E: EthSpec> AnchorValidatorStore<T, E> {
525525

526526
Ok(signed_exit)
527527
}
528+
529+
fn create_beacon_vote_validator(
530+
&self,
531+
slot: Slot,
532+
validator_attestation_committees: HashMap<PublicKeyBytes, u64>,
533+
) -> Box<BeaconVoteValidator<E>> {
534+
Box::new(BeaconVoteValidator::new(
535+
slot,
536+
Arc::clone(&self.slashing_protection),
537+
self.disable_slashing_protection,
538+
self.spec.clone(),
539+
validator_attestation_committees,
540+
self.genesis_validators_root,
541+
))
542+
}
543+
544+
fn get_attesting_validators_in_committee(
545+
&self,
546+
metadata: &SlotMetadata<E>,
547+
committee_id: CommitteeId,
548+
) -> HashMap<PublicKeyBytes, u64> {
549+
let committee_validators = self
550+
.database
551+
.state()
552+
.metadata()
553+
.get_all_by(&committee_id)
554+
.map(|v| v.public_key)
555+
.collect::<HashSet<_>>();
556+
557+
metadata
558+
.attesting_validator_committees
559+
.iter()
560+
.filter_map(|(&pubkey, &index)| {
561+
committee_validators
562+
.contains(&pubkey)
563+
.then_some((pubkey, index))
564+
})
565+
.collect::<HashMap<_, _>>()
566+
}
528567
}
529568

530569
/// # Arguments
@@ -609,8 +648,11 @@ struct SlotMetadata<E: EthSpec> {
609648
slot: Slot,
610649
/// The BeaconVote we will use as initial QBFT data.
611650
beacon_vote: BeaconVote,
612-
/// All our validators that are attesting in this slot.
613-
attesting_validators: Vec<ValidatorIndex>,
651+
/// The indices of all our validators that are attesting in this slot.
652+
attesting_validator_indices: Vec<ValidatorIndex>,
653+
/// The pubkeys of all our validators that are attesting in this slot, mapped to their
654+
/// attestation committee index.
655+
attesting_validator_committees: HashMap<PublicKeyBytes, u64>,
614656
/// All our validators that are in the sync committee for this slot.
615657
sync_validators: Vec<ValidatorIndex>,
616658
/// All validators that are aggregator for this slot multiple times, and thus require special
@@ -909,6 +951,10 @@ impl<T: SlotClock, E: EthSpec> ValidatorStore for AnchorValidatorStore<T, E> {
909951
}
910952

911953
let (validator, cluster) = self.get_validator_and_cluster(validator_pubkey)?;
954+
let slot_metadata = self.get_slot_metadata(attestation.data().slot).await?;
955+
956+
let validator_attestation_committees =
957+
self.get_attesting_validators_in_committee(&slot_metadata, cluster.committee_id());
912958

913959
let timer =
914960
metrics::start_timer_vec(&metrics::CONSENSUS_TIMES, &[metrics::BEACON_VOTE]);
@@ -928,7 +974,10 @@ impl<T: SlotClock, E: EthSpec> ValidatorStore for AnchorValidatorStore<T, E> {
928974
source: attestation.data().source,
929975
target: attestation.data().target,
930976
},
931-
Box::new(NoDataValidation),
977+
self.create_beacon_vote_validator(
978+
attestation.data().slot,
979+
validator_attestation_committees,
980+
),
932981
start_time,
933982
&cluster,
934983
)
@@ -1246,6 +1295,9 @@ impl<T: SlotClock, E: EthSpec> ValidatorStore for AnchorValidatorStore<T, E> {
12461295
let (validator, cluster) = self.get_validator_and_cluster(*validator_pubkey)?;
12471296
let metadata = self.get_slot_metadata(slot).await?;
12481297

1298+
let validator_attestation_committees =
1299+
self.get_attesting_validators_in_committee(&metadata, cluster.committee_id());
1300+
12491301
let timer =
12501302
metrics::start_timer_vec(&metrics::CONSENSUS_TIMES, &[metrics::BEACON_VOTE]);
12511303
let start_time = self
@@ -1258,7 +1310,7 @@ impl<T: SlotClock, E: EthSpec> ValidatorStore for AnchorValidatorStore<T, E> {
12581310
instance_height: slot.as_usize().into(),
12591311
},
12601312
metadata.beacon_vote.clone(),
1261-
Box::new(NoDataValidation),
1313+
self.create_beacon_vote_validator(slot, validator_attestation_committees),
12621314
start_time,
12631315
&cluster,
12641316
)

anchor/validator_store/src/metadata_service.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,17 @@ impl<E: EthSpec, T: SlotClock + 'static> MetadataService<E, T> {
100100
target: attestation_data.target,
101101
};
102102

103-
let attesting_validators = self
103+
let (attesting_validator_indices, attesting_validator_committees) = self
104104
.duties_service
105105
.attesters(slot)
106106
.into_iter()
107-
.map(|duty| ValidatorIndex(duty.duty.validator_index as usize))
108-
.collect();
107+
.map(|duty| {
108+
(
109+
ValidatorIndex(duty.duty.validator_index as usize),
110+
(duty.duty.pubkey, duty.duty.committee_index),
111+
)
112+
})
113+
.unzip();
109114

110115
let sync_duties = self
111116
.duties_service
@@ -142,7 +147,8 @@ impl<E: EthSpec, T: SlotClock + 'static> MetadataService<E, T> {
142147
let metadata = SlotMetadata {
143148
slot,
144149
beacon_vote,
145-
attesting_validators,
150+
attesting_validator_indices,
151+
attesting_validator_committees,
146152
sync_validators,
147153
multi_sync_aggregators,
148154
};

0 commit comments

Comments
 (0)