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
Merged

Conversation

romac
Copy link
Member

@romac romac commented Jun 8, 2020

Fix #282
Fix #335

Notes

  • Needs to throw a proper error in some places
  • Trust threshold is currently hardcoded and should be rather passed in
  • The method should likely be renamed or its signature should be changed to reflect the new behavior
  • Most of logic and comments are taken from the Go code, so perhaps some attribution should be added as well
  • Tests are failing because the signatures currently fail to verify, likely because of mismatch with their validator

  • Referenced an issue explaining the need for the change
  • Updated all relevant documentation in docs
  • Updated all code comments where relevant
  • Wrote tests
  • Updated CHANGES.md

Base automatically changed from romain/light-client-followup to master June 9, 2020 10:29
Copy link
Contributor

@melekes melekes left a comment

Choose a reason for hiding this comment

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

lgtm

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 :)

light-client/tests/light_client.rs Outdated Show resolved Hide resolved
- Provide two helper methods for checking validators overlap and if
  there is enough trust w.r.t. the trusted state.
- Only count `Commit` signatures towards the voting power.
- Do not rely on `signed_votes` method to get the votes.
@romac romac changed the title Rewrite VotingPowerCalculator::voting_power_in Fix VotingPowerCalculator::voting_power_in Jun 18, 2020
@romac
Copy link
Member Author

romac commented Jun 18, 2020

I pushed a new version which should now be doing the right thing w.r.t. checking trust vs validators signers, but still relies on validator addresses. I guess we can address the latter in a future PR once we are convinced that this version is correct.

@romac
Copy link
Member Author

romac commented Jun 18, 2020

It would be great if someone could run the tests of the light-client crate and inspect the output to ensure that errors that are raised are indeed the ones we expect from the JSON file. I did so myself

$ cd light-client/
$ cargo test -- --nocapture --test-threads 1

I already did so myself, and I am not too sure about this test:

Running light client against bisection test-file: ./tests/support/bisection/single_peer/not_enough_commits.json
  - Case: Trusted height=1, fails at height 6 because more than one-third (trust level) vals didn't sign
[light-client/tests/light_client.rs:247] e = Error(
    Context {
        kind: InvalidLightBlock(
            InvalidNextValidatorSet {
                header_next_validators_hash: Hash::Sha256(75B9F27F25F3515EF7CA61DD42C272CCC280C537E6A9ED493710EB361CB90B0D),
                next_validators_hash: Hash::Sha256(26952B5D784A1564D167DF98D2D37376B5E77771928256D25E6FF9AE3AD11564),
            },
        ),
...

This test is passing because we expect an error, but I don't think that that is the right one, except if there's perhaps more than one error encoded in the JSON test. In particular, I would have expected the test to result in either NotEnoughTrust or InsufficientValidatorsOverlap but we are getting InvalidNextValidatorSet instead.

@Shivani912 Is that something you could look into?

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 mut seen_validators = HashSet::new();

for (idx, signature) in signatures.into_iter().enumerate() {
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!

@Shivani912
Copy link
Contributor

It would be great if someone could run the tests of the light-client crate and inspect the output to ensure that errors that are raised are indeed the ones we expect from the JSON file. I did so myself

$ cd light-client/
$ cargo test -- --nocapture --test-threads 1

I already did so myself, and I am not too sure about this test:

Running light client against bisection test-file: ./tests/support/bisection/single_peer/not_enough_commits.json
  - Case: Trusted height=1, fails at height 6 because more than one-third (trust level) vals didn't sign
[light-client/tests/light_client.rs:247] e = Error(
    Context {
        kind: InvalidLightBlock(
            InvalidNextValidatorSet {
                header_next_validators_hash: Hash::Sha256(75B9F27F25F3515EF7CA61DD42C272CCC280C537E6A9ED493710EB361CB90B0D),
                next_validators_hash: Hash::Sha256(26952B5D784A1564D167DF98D2D37376B5E77771928256D25E6FF9AE3AD11564),
            },
        ),
...

This test is passing because we expect an error, but I don't think that that is the right one, except if there's perhaps more than one error encoded in the JSON test. In particular, I would have expected the test to result in either NotEnoughTrust or InsufficientValidatorsOverlap but we are getting InvalidNextValidatorSet instead.

@Shivani912 Is that something you could look into?

@romac There's a bug in this one (issue #290) I'm working on it and will do a PR once I fix it!

I feel we really need to have checks (in the test itself) on errors even if the tests pass. I tried to do this at the end of Q1/beginning of Q2 but it seemed it was a little too early to be able to do so. Think this should be on priority for next quarter?

Copy link
Contributor

@OStevan OStevan left a comment

Choose a reason for hiding this comment

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

@romac thanks for asking me to review. I wrote what I noticed about differences between the go implementation and the one here. Also, I wrote some comments mostly around VotingPower, however I might be missing some context so those could just be wrong.

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.


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.

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.

Comment on lines 102 to 106
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.

@romac
Copy link
Member Author

romac commented Jun 19, 2020

@romac There's a bug in this one (issue #290) I'm working on it and will do a PR once I fix it!

Awesome, thanks :)

I feel we really need to have checks (in the test itself) on errors even if the tests pass. I tried to do this at the end of Q1/beginning of Q2 but it seemed it was a little too early to be able to do so. Think this should be on priority for next quarter?

Yes, that would definitely increase our confidence in the tests and thus the correctness of implementation :)

Copy link
Contributor

@brapse brapse left a comment

Choose a reason for hiding this comment

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

I think this is good to go with follow up unit tests coming when we have the fixtures lined up.

@brapse brapse merged commit 8ab1989 into master Jun 19, 2020
@brapse brapse deleted the romain/voting-power-in branch June 19, 2020 15:16
Copy link
Member

@ebuchman ebuchman left a comment

Choose a reason for hiding this comment

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

Looks good! I think we still want to break the voting_power_in into two functions, one for check_signers and one for check_enough_trust, and neither should depend on vote.validator_address, but I'll follow up in another issue about that

fn check_enough_trust(
&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

@romac
Copy link
Member Author

romac commented Jun 25, 2020

@ebuchman

I think we still want to break the voting_power_in into two functions, one for check_signers and one for check_enough_trust

It would be great if you could describe the differences between the two versions somewhere.

and neither should depend on vote.validator_address

I initially tried to fix that as well but eventually chose to focus on fixing the computation itself, and improve it later.

@ebuchman
Copy link
Member

Done in #377

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
7 participants