-
Notifications
You must be signed in to change notification settings - Fork 216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix VotingPowerCalculator::voting_power_in
#306
Changes from all commits
208cf97
024a73d
c828155
319c9f2
e9c51a0
90c32ba
8c0c5e6
7e3b537
ef3660c
30d680f
2f45b73
fc5e89f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,76 @@ | ||
use crate::{ | ||
bail, | ||
predicates::errors::VerificationError, | ||
types::{SignedHeader, ValidatorSet}, | ||
types::{Commit, SignedHeader, TrustThreshold, ValidatorSet}, | ||
}; | ||
|
||
use anomaly::BoxError; | ||
use serde::{Deserialize, Serialize}; | ||
use std::collections::HashSet; | ||
use std::fmt; | ||
|
||
use tendermint::block::CommitSig; | ||
use tendermint::lite::types::TrustThreshold as _; | ||
use tendermint::lite::types::ValidatorSet as _; | ||
use tendermint::vote::{SignedVote, Vote}; | ||
|
||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] | ||
pub struct VotingPowerTally { | ||
pub total: u64, | ||
pub tallied: u64, | ||
pub trust_threshold: TrustThreshold, | ||
} | ||
|
||
impl fmt::Display for VotingPowerTally { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
write!( | ||
f, | ||
"VotingPower(total={} tallied={} trust_threshold={})", | ||
self.total, self.tallied, self.trust_threshold | ||
) | ||
} | ||
} | ||
|
||
pub trait VotingPowerCalculator: Send { | ||
fn total_power_of(&self, validators: &ValidatorSet) -> u64; | ||
|
||
fn check_enough_trust( | ||
&self, | ||
untrusted_header: &SignedHeader, | ||
untrusted_validators: &ValidatorSet, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are trusted validators |
||
trust_threshold: TrustThreshold, | ||
) -> Result<(), VerificationError> { | ||
let voting_power = | ||
self.voting_power_in(untrusted_header, untrusted_validators, trust_threshold)?; | ||
|
||
if trust_threshold.is_enough_power(voting_power.tallied, voting_power.total) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mentioned in one of the comments above, I would probably make more sense to just check this in the |
||
Ok(()) | ||
} else { | ||
Err(VerificationError::NotEnoughTrust(voting_power)) | ||
} | ||
} | ||
|
||
fn check_signers_overlap( | ||
&self, | ||
untrusted_header: &SignedHeader, | ||
untrusted_validators: &ValidatorSet, | ||
) -> Result<(), VerificationError> { | ||
let trust_threshold = TrustThreshold::TWO_THIRDS; | ||
let voting_power = | ||
self.voting_power_in(untrusted_header, untrusted_validators, trust_threshold)?; | ||
|
||
if trust_threshold.is_enough_power(voting_power.tallied, voting_power.total) { | ||
Ok(()) | ||
} else { | ||
Err(VerificationError::InsufficientSignersOverlap(voting_power)) | ||
} | ||
} | ||
|
||
fn voting_power_in( | ||
&self, | ||
signed_header: &SignedHeader, | ||
validators: &ValidatorSet, | ||
) -> Result<u64, BoxError>; | ||
validator_set: &ValidatorSet, | ||
trust_threshold: TrustThreshold, | ||
) -> Result<VotingPowerTally, VerificationError>; | ||
} | ||
|
||
#[derive(Copy, Clone, Debug)] | ||
|
@@ -27,36 +84,105 @@ impl VotingPowerCalculator for ProdVotingPowerCalculator { | |
fn voting_power_in( | ||
&self, | ||
signed_header: &SignedHeader, | ||
validators: &ValidatorSet, | ||
) -> Result<u64, BoxError> { | ||
// NOTE: We don't know the validators that committed this block, | ||
// so we have to check for each vote if its validator is already known. | ||
let mut signed_power = 0_u64; | ||
|
||
for vote in &signed_header.signed_votes() { | ||
romac marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Only count if this vote is from a known validator. | ||
// TODO: we still need to check that we didn't see a vote from this validator twice ... | ||
let val_id = vote.validator_id(); | ||
let val = match validators.validator(val_id) { | ||
Some(v) => v, | ||
None => continue, | ||
validator_set: &ValidatorSet, | ||
trust_threshold: TrustThreshold, | ||
) -> Result<VotingPowerTally, VerificationError> { | ||
let signatures = &signed_header.commit.signatures; | ||
|
||
let mut tallied_voting_power = 0_u64; | ||
let mut seen_validators = HashSet::new(); | ||
|
||
// Get non-absent votes from the signatures | ||
let non_absent_votes = signatures.iter().enumerate().flat_map(|(idx, signature)| { | ||
if let Some(vote) = non_absent_vote(signature, idx as u64, &signed_header.commit) { | ||
Some((signature, vote)) | ||
} else { | ||
None | ||
} | ||
}); | ||
|
||
for (signature, vote) in non_absent_votes { | ||
// Ensure we only count a validator's power once | ||
if seen_validators.contains(&vote.validator_address) { | ||
bail!(VerificationError::DuplicateValidator( | ||
vote.validator_address | ||
)); | ||
} else { | ||
seen_validators.insert(vote.validator_address); | ||
} | ||
|
||
let validator = match validator_set.validator(vote.validator_address) { | ||
Some(validator) => validator, | ||
None => continue, // Cannot find matching validator, so we skip the vote | ||
}; | ||
|
||
// check vote is valid from validator | ||
let sign_bytes = vote.sign_bytes(); | ||
let signed_vote = SignedVote::new( | ||
(&vote).into(), | ||
signed_header.header.chain_id.as_str(), | ||
vote.validator_address, | ||
vote.signature, | ||
); | ||
|
||
if !val.verify_signature(&sign_bytes, vote.signature()) { | ||
bail!(VerificationError::ImplementationSpecific(format!( | ||
"Couldn't verify signature {:?} with validator {:?} on sign_bytes {:?}", | ||
vote.signature(), | ||
val, | ||
// Check vote is valid | ||
let sign_bytes = signed_vote.sign_bytes(); | ||
if !validator.verify_signature(&sign_bytes, signed_vote.signature()) { | ||
bail!(VerificationError::InvalidSignature { | ||
signature: signed_vote.signature().to_vec(), | ||
validator, | ||
sign_bytes, | ||
))); | ||
}); | ||
} | ||
|
||
// If the vote is neither absent nor nil, tally its power | ||
if signature.is_commit() { | ||
tallied_voting_power += validator.power(); | ||
} else { | ||
// It's OK. We include stray signatures (~votes for nil) | ||
// to measure validator availability. | ||
} | ||
|
||
signed_power += val.power(); | ||
// TODO: Break out of the loop when we have enough voting power. | ||
// See https://github.com/informalsystems/tendermint-rs/issues/235 | ||
} | ||
|
||
Ok(signed_power) | ||
let voting_power = VotingPowerTally { | ||
total: self.total_power_of(validator_set), | ||
tallied: tallied_voting_power, | ||
trust_threshold, | ||
}; | ||
|
||
Ok(voting_power) | ||
} | ||
} | ||
|
||
fn non_absent_vote(commit_sig: &CommitSig, validator_index: u64, commit: &Commit) -> Option<Vote> { | ||
let (validator_address, timestamp, signature, block_id) = match commit_sig { | ||
CommitSig::BlockIDFlagAbsent { .. } => return None, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't we panic here because method assumes non_absent signature? then we won't need the Option<> There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is actually the one doing the filtering for non absent signatures, hence the need for the Option. I guess the name isn't great, I am open to suggestions :) |
||
CommitSig::BlockIDFlagCommit { | ||
validator_address, | ||
timestamp, | ||
signature, | ||
} => ( | ||
*validator_address, | ||
*timestamp, | ||
signature.clone(), | ||
Some(commit.block_id.clone()), | ||
), | ||
CommitSig::BlockIDFlagNil { | ||
validator_address, | ||
timestamp, | ||
signature, | ||
} => (*validator_address, *timestamp, signature.clone(), None), | ||
}; | ||
|
||
Some(Vote { | ||
vote_type: tendermint::vote::Type::Precommit, | ||
height: commit.height, | ||
round: commit.round, | ||
block_id, | ||
timestamp, | ||
validator_address, | ||
validator_index, | ||
signature, | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name and the result of this function were for me a bit misleading as the name would suggest a boolean value (or in this case
Ok()
orError(...)
) and it is more or less used as such since the result value in the Ok case is just ignored.