Skip to content
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

Merged
merged 12 commits into from
Jun 19, 2020
189 changes: 160 additions & 29 deletions light-client/src/operations/voting_power.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,78 @@
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 VotingPower {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me this is a bit misleading as the struct has more information than what I would expect by reading just the name. Also, I think it would be beneficial to implement the TODO about the breaking out of the loop as than this would go probably go away.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that the name is perhaps not the best, but we still need this information to be able to yield a meaningful error in case of lack of trust/signers overlap.

pub total: u64,
pub tallied: u64,
pub trust_threshold: TrustThreshold,
}

impl fmt::Display for VotingPower {
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 voting_power_in(

fn check_enough_trust(
Copy link
Contributor

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() or Error(...)) and it is more or less used as such since the result value in the Ok case is just ignored.

&self,
untrusted_header: &SignedHeader,
untrusted_validators: &ValidatorSet,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are trusted validators

trust_threshold: TrustThreshold,
) -> Result<VotingPower, VerificationError> {
println!("check_validators_overlap");
romac marked this conversation as resolved.
Show resolved Hide resolved
let voting_power =
self.voting_power_of(untrusted_header, untrusted_validators, trust_threshold)?;

if trust_threshold.is_enough_power(voting_power.tallied, voting_power.total) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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 voting_power_of and instead of a value just return a boolean value or something else to tell us if everything is fine or there is some error.

Ok(voting_power)
} else {
Err(VerificationError::NotEnoughTrust(voting_power))
}
}

fn check_signers_overlap(
&self,
untrusted_header: &SignedHeader,
untrusted_validators: &ValidatorSet,
) -> Result<VotingPower, VerificationError> {
println!("check_signers_overlap");
romac marked this conversation as resolved.
Show resolved Hide resolved
let two_thirds = TrustThreshold::new(2, 3).unwrap();
romac marked this conversation as resolved.
Show resolved Hide resolved
let voting_power =
self.voting_power_of(untrusted_header, untrusted_validators, two_thirds)?;

if two_thirds.is_enough_power(voting_power.tallied, voting_power.total) {
Ok(voting_power)
} else {
Err(VerificationError::InsufficientSignersOverlap(voting_power))
}
}

fn voting_power_of(
&self,
signed_header: &SignedHeader,
validators: &ValidatorSet,
) -> Result<u64, BoxError>;
validator_set: &ValidatorSet,
trust_threshold: TrustThreshold,
) -> Result<VotingPower, VerificationError>;
}

#[derive(Copy, Clone, Debug)]
Expand All @@ -24,39 +83,111 @@ impl VotingPowerCalculator for ProdVotingPowerCalculator {
validators.total_power()
}

fn voting_power_in(
fn voting_power_of(
&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<VotingPower, VerificationError> {
let signatures = &signed_header.commit.signatures;

let mut tallied_voting_power = 0_u64;
let mut seen_validators = HashSet::new();

for (idx, signature) in signatures.into_iter().enumerate() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to reference the test vector which exercises this particular edge case?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What edge case are you referring to here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops perhaps I did not comment the correct line and actually there is an informative comment further down Ensure we only count a validator's power once

Copy link
Member Author

@romac romac Jun 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, we currently do not have a test vector which covers this edge case.

cc @Shivani912

Edit: typo

let vote = vote_from_non_absent_signature(signature, idx as u64, &signed_header.commit);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment as above, IIRC this was a bug in the past.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean counting absent signatures? I don't think that's possible since a CommitSig::BlockIDFlagAbsent does not contain any of the data required to craft a Vote (ie. validator_address, timestamp and signature).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True!

let vote = match vote {
Some(vote) => vote,
None => continue, // Ok, some signatures can be absent
};

// Ensure we only count a validator's power once
if seen_validators.contains(&vote.validator_address) {
continue;
} else {
seen_validators.insert(vote.validator_address);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One interesting difference between go code and the implementation here is this if. (go) In case of 2/3 threshold this check is done by comparing the size of the signers validator set and the expected validator set, if both are the same length everything is fine and we don't even do these checks. However in the case where we are interested in trust between two non adjacent headers, then branch should raise an exception.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@melekes What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess raising an error is probably the proper behavior there. Will already change that, and we can always revert it back if that was wrong.


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 when we have enough voting power
romac marked this conversation as resolved.
Show resolved Hide resolved
}

Ok(signed_power)
let voting_power = VotingPower {
total: self.total_power_of(validator_set),
tallied: tallied_voting_power,
trust_threshold,
};

Ok(voting_power)
}
}

fn vote_from_non_absent_signature(
commit_sig: &CommitSig,
validator_index: u64,
commit: &Commit,
) -> Option<Vote> {
let (validator_address, timestamp, signature, block_id) = match commit_sig {
CommitSig::BlockIDFlagAbsent { .. } => return None,
Copy link
Contributor

Choose a reason for hiding this comment

The 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<>

Copy link
Member Author

Choose a reason for hiding this comment

The 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.clone(),
timestamp.clone(),
signature.clone(),
Some(commit.block_id.clone()),
),
CommitSig::BlockIDFlagNil {
validator_address,
timestamp,
signature,
} => (
validator_address.clone(),
timestamp.clone(),
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,
})
}
55 changes: 2 additions & 53 deletions light-client/src/predicates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,52 +144,14 @@ pub trait VerificationPredicates: Send {
Ok(())
}

fn has_sufficient_voting_power(
&self,
signed_header: &SignedHeader,
validators: &ValidatorSet,
trust_threshold: &TrustThreshold,
calculator: &dyn VotingPowerCalculator,
) -> Result<(), VerificationError> {
// FIXME: Do not discard underlying error
let total_power = calculator.total_power_of(validators);
let voting_power = calculator
.voting_power_in(signed_header, validators)
.map_err(|e| VerificationError::ImplementationSpecific(e.to_string()))?;

ensure!(
voting_power * trust_threshold.denominator > total_power * trust_threshold.numerator,
VerificationError::InsufficientVotingPower {
total_power,
voting_power,
}
);

Ok(())
}

fn has_sufficient_validators_overlap(
&self,
untrusted_sh: &SignedHeader,
trusted_validators: &ValidatorSet,
trust_threshold: &TrustThreshold,
calculator: &dyn VotingPowerCalculator,
) -> Result<(), VerificationError> {
// FIXME: Do not discard underlying error
let total_power = calculator.total_power_of(trusted_validators);
let voting_power = calculator
.voting_power_in(untrusted_sh, trusted_validators)
.map_err(|e| VerificationError::ImplementationSpecific(e.to_string()))?;

ensure!(
voting_power * trust_threshold.denominator > total_power * trust_threshold.numerator,
VerificationError::InsufficientValidatorsOverlap {
total_power,
signed_power: voting_power,
trust_threshold: *trust_threshold,
}
);

calculator.check_enough_trust(untrusted_sh, trusted_validators, *trust_threshold)?;
Ok(())
}

Expand All @@ -199,20 +161,7 @@ pub trait VerificationPredicates: Send {
untrusted_validators: &ValidatorSet,
calculator: &dyn VotingPowerCalculator,
) -> Result<(), VerificationError> {
// FIXME: Do not discard underlying error
let total_power = calculator.total_power_of(untrusted_validators);
let signed_power = calculator
.voting_power_in(untrusted_sh, untrusted_validators)
.map_err(|e| VerificationError::ImplementationSpecific(e.to_string()))?;

ensure!(
signed_power * 3 > total_power * 2,
VerificationError::InsufficientCommitPower {
total_power,
signed_power,
}
);

calculator.check_signers_overlap(untrusted_sh, untrusted_validators)?;
Ok(())
}

Expand Down
31 changes: 19 additions & 12 deletions light-client/src/predicates/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::errors::ErrorExt;
use crate::types::{Hash, Height, Time, TrustThreshold};
use crate::operations::voting_power::VotingPower;
use crate::types::{Hash, Height, Time, Validator};

/// The various errors which can be raised by the verifier component,
/// when validating or verifying a light block.
Expand All @@ -15,20 +16,26 @@ pub enum VerificationError {
#[error("implementation specific: {0}")]
ImplementationSpecific(String),

#[error("not enough trust because insufficient validators overlap: {0}")]
NotEnoughTrust(VotingPower),

#[error("insufficient signers overlap: {0}")]
InsufficientSignersOverlap(VotingPower),

#[error(
"insufficient validators overlap: total_power={total_power} signed_power={signed_power} trust_threshold={trust_threshold}"
"validators and signatures count do no match: {validators_count} != {signatures_count}"
)]
InsufficientValidatorsOverlap {
total_power: u64,
signed_power: u64,
trust_threshold: TrustThreshold,
ValidatorsAndSignaturesCountMismatch {
validators_count: usize,
signatures_count: usize,
},

#[error("insufficient voting power: total_power={total_power} voting_power={voting_power}")]
InsufficientVotingPower { total_power: u64, voting_power: u64 },

#[error("invalid commit power: total_power={total_power} signed_power={signed_power}")]
InsufficientCommitPower { total_power: u64, signed_power: u64 },
#[error("Couldn't verify signature `{signature:?}` with validator `{validator:?}` on sign_bytes `{sign_bytes:?}`")]
InvalidSignature {
signature: Vec<u8>,
validator: Validator,
sign_bytes: Vec<u8>,
},

#[error("invalid commit: {0}")]
InvalidCommit(String),
Expand Down Expand Up @@ -76,7 +83,7 @@ impl ErrorExt for VerificationError {
/// Whether this error means that the light block
/// cannot be trusted w.r.t. the latest trusted state.
fn not_enough_trust(&self) -> bool {
if let Self::InsufficientValidatorsOverlap { .. } = self {
if let Self::NotEnoughTrust { .. } = self {
true
} else {
false
Expand Down
4 changes: 4 additions & 0 deletions light-client/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use tendermint::{
Commit as TMCommit,
},
lite::TrustThresholdFraction,
validator::Info as TMValidatorInfo,
validator::Set as TMValidatorSet,
};

Expand All @@ -30,6 +31,9 @@ pub type Header = TMHeader;
/// Set of validators
pub type ValidatorSet = TMValidatorSet;

/// Info about a single validator
pub type Validator = TMValidatorInfo;

/// A commit contains the justification (ie. a set of signatures)
/// that a block was consensus, as committed by a set previous block of validators.
pub type Commit = TMCommit;
Expand Down
13 changes: 9 additions & 4 deletions light-client/tests/light_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,11 @@ fn run_test_case(tc: TestCase<LightBlock>) {

latest_trusted = Trusted::new(new_state.signed_header, new_state.next_validators);
}
Err(_) => {
Err(e) => {
dbg!(e);
// if !expects_err {
// dbg!(e);
// }
assert!(expects_err);
}
}
Expand Down Expand Up @@ -240,9 +244,10 @@ fn run_bisection_test(tc: TestBisection<LightBlock>) {
assert!(!expects_err);
}
Err(e) => {
if !expects_err {
dbg!(e);
}
dbg!(e);
// if !expects_err {
// dbg!(e);
// }
assert!(expects_err);
}
}
Expand Down
Loading