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

Light client: trait impls #36

Merged
merged 21 commits into from
Dec 10, 2019
Merged

Light client: trait impls #36

merged 21 commits into from
Dec 10, 2019

Conversation

liamsi
Copy link
Member

@liamsi liamsi commented Sep 18, 2019

This PR will have implementations of the traits created in #31 and a few tests.

This is just the simplest way to move forward implementing the traits of
the lite package. There are alternatives:
We do not want a create a circular dependency between lite and
tendermint (which does not even compile). To avoid this we could:
1) make lite a module of tendermint, or, 2) replicate a lot of the types
of the tendermint crate in the lite package (e.g. Time, Ids etc), or 3)
have a dependency from tendermint to the lite package only (but then the
trait impls do need to live in the lite crate instead with the types in
the tendermint crate directly).
respective amino types, and, directly encode some
 2DC46AD76277039F1B65FE3C7F2064788B1C12FE701CFE7EC93F751586A48781)

will add a test after #42 gets merged
byteslices.push(evidence_hash_enc.as_slice());
byteslices.push(proposer_address_enc.as_slice());

Hash::Sha256(simple_hash_from_byte_slices(byteslices.as_slice()))
Copy link
Member Author

@liamsi liamsi Sep 26, 2019

Choose a reason for hiding this comment

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

This looks a bit too exhaustive but aims to do exactly the same as: https://github.com/tendermint/tendermint/blob/134fe2896275bb926b49743c1e25493f6b24cc31/types/block.go#L393
(It actually does will add a few tests after #41 is merged)

This could probably be simplified substantially. Maybe, simple_hash_from_byte_slices had a similar logic as a Hasher in golang (Write the slices and then Sum to create the tree once and return the hash)?

Copy link
Member

Choose a reason for hiding this comment

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

Definitely seems excessive.

Good idea re a write based API. Though I don't think you usually see that for Merkle trees? At least the Merkle tree version needs to track the boundaries of each write, where as a normal hasher just concatenates everything together.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

done in e686c45

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.

Great start! I merged some things to master and merged them back into bucky/lite, so we should merge bucky/lite back into here, but then we also need to deal with the change from into_vec() into iter() in the traits ...

tendermint/src/validator.rs Show resolved Hide resolved
byteslices.push(evidence_hash_enc.as_slice());
byteslices.push(proposer_address_enc.as_slice());

Hash::Sha256(simple_hash_from_byte_slices(byteslices.as_slice()))
Copy link
Member

Choose a reason for hiding this comment

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

Definitely seems excessive.

Good idea re a write based API. Though I don't think you usually see that for Merkle trees? At least the Merkle tree version needs to track the boundaries of each write, where as a normal hasher just concatenates everything together.

tendermint/src/lite/types.rs Show resolved Hide resolved
None => None,
},
}),
round: vote.round as i64,
Copy link
Member

Choose a reason for hiding this comment

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

Some reason this doesn't follow the same order as the fields in the struct, ie. the round before the block_id?

Copy link
Member Author

@liamsi liamsi Nov 5, 2019

Choose a reason for hiding this comment

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

Probably not. Thanks, fixed!

Vote {
vote_type: vote.vote_type.to_u32(),
height: vote.height.value() as i64, // TODO potential overflow :-/
block_id: Some(BlockId {
Copy link
Member

Choose a reason for hiding this comment

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

Why is block_id an Option here if we're always going to populate it with Some ? Is there a case elsewhere we use a None?

Copy link
Member Author

@liamsi liamsi Nov 5, 2019

Choose a reason for hiding this comment

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

Hmm, I don't remember tbh. These amino types are quite old. Let me check if the encoding matches if remove the Option. I think it was sth related to prost and the #[prost(message)] attribute.

Copy link
Member Author

@liamsi liamsi Nov 5, 2019

Choose a reason for hiding this comment

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

So, it looks like one can simply add the #[prost(message)] for a nested struct if it is an Option. Otherwise, it seems a little more effort is necessary. e.g. I locally changed the field's type to BlockId instead of Option<BlockId>. Then the compiler (due to prost-amino) will complain:

Click here for Rust chatty compiler output
error[E0599]: no method named `as_ref` found for type `amino_types::block_id::BlockId` in the current scope
  --> tendermint/src/amino_types/vote.rs:22:28
   |
22 | #[derive(Clone, PartialEq, Message)]
   |                            ^^^^^^^ method not found in `amino_types::block_id::BlockId`
   | 
  ::: tendermint/src/amino_types/block_id.rs:11:1
   |
11 | pub struct BlockId {
   | ------------------ method `as_ref` not found for this
   |
   = help: items from traits can only be used if the trait is implemented and in scope
   = note: the following trait defines an item `as_ref`, perhaps you need to implement it:
           candidate #1: `std::convert::AsRef`

error[E0308]: mismatched types
  --> tendermint/src/amino_types/vote.rs:22:28
   |
22 | #[derive(Clone, PartialEq, Message)]
   |                            ^^^^^^^
   |                            |
   |                            expected struct `amino_types::block_id::BlockId`, found enum `std::option::Option`
   |                            this match expression has type `amino_types::block_id::BlockId`
   |
   = note: expected type `amino_types::block_id::BlockId`
              found type `std::option::Option<_>`

error[E0599]: no method named `get_or_insert_with` found for type `amino_types::block_id::BlockId` in the current scope
  --> tendermint/src/amino_types/vote.rs:22:28
   |
22 | #[derive(Clone, PartialEq, Message)]
   |                            ^^^^^^^ method not found in `amino_types::block_id::BlockId`
   | 
  ::: tendermint/src/amino_types/block_id.rs:11:1
   |
11 | pub struct BlockId {
   | ------------------ method `get_or_insert_with` not found for this

error[E0308]: mismatched types
  --> tendermint/src/amino_types/vote.rs:22:28
   |
22 | #[derive(Clone, PartialEq, Message)]
   |                            ^^^^^^^ expected struct `amino_types::block_id::BlockId`, found enum `std::option::Option`
   |
   = note: expected type `amino_types::block_id::BlockId`
              found type `std::option::Option<_>`

error: aborting due to 4 previous errors

Some errors have detailed explanations: E0308, E0599.
For more information about an error, try `rustc --explain E0308`.
error: Could not compile `tendermint`.
´´´

</details>

Copy link
Member Author

@liamsi liamsi Nov 5, 2019

Choose a reason for hiding this comment

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

OK, it looks like this can be fixed by adding in a required attribute: https://github.com/danburkert/prost/blob/9551f2852e414df72339634087c46ce5a08e85b2/prost-derive/src/field/message.rs#L14-L56

I'm looking into that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Adding the required attribute, e.g.:

#[prost(message, required, tag = "4")]
pub block_id: BlockId,

makes the compiler happy 😄but breaks some encoding test-vectors 🤔

thread 'amino_types::vote::tests::test_sign_bytes_compatibility' panicked at 'assertion failed: `(left == right)`
  left: `[15, 34, 0, 42, 11, 8, 128, 146, 184, 195, 152, 254, 255, 255, 255, 1]`,
 right: `[13, 42, 11, 8, 128, 146, 184, 195, 152, 254, 255, 255, 255, 1]`', tendermint/src/amino_types/vote.rs:317:9

Hmm, this feels somehow familiar. Will investigate further.

Copy link
Member Author

@liamsi liamsi Nov 5, 2019

Choose a reason for hiding this comment

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

OK, here is the catch:

In golang one can simply not initalize a field. Golang won't complain and implicitly init everything with default values. Then, Amino will treat this as sth. that can be skipped (and hence is optional in the protobuf2 sense which is the default in proto3).

Now on the rust side you have to init a field (unless you make it an option). If you make it an Option, the encoding behaves like amino (None will be not encoded at all). If you have a regular field (e.g. BlockId), and add a required attribute, prost(-amino) will treat as required (in the protobuf2 sense) and signal, we got fiel blah of type blubb but nothing in there (for blockId this is fieldnumber 4 and hence 4 << 3 | 2 = 34 followed by a 0x00 for indicating that this was empty; which is exactly the two bytes difference in above test-vector).

Amino actually does the same - enc. defaults as required as described above - but rolls back if it found out there is nothing (via that zero 0x00) unless told otherwise via writeEmpty (at least it seems to work now):
https://github.com/tendermint/go-amino/blob/fbf776258498dbb3aa6637ca82c7fe5c7255fd2f/binary-encode.go#L496-L524

Alt Text

},
}),
round: vote.round as i64,
timestamp: Some(TimeMsg::from(vote.timestamp)),
Copy link
Member

Choose a reason for hiding this comment

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

Same re why this is an Option

Copy link
Member Author

@liamsi liamsi Nov 5, 2019

Choose a reason for hiding this comment

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

See above. Time is a bit different because we want to encode a particular time if None was passed in ("1970-01-01 00:00:00 +0000 UTC").

// panic (as the golang code would):
// https://github.com/tendermint/tendermint/blob/134fe2896275bb926b49743c1e25493f6b24cc31/types/block.go#L393
// https://github.com/tendermint/tendermint/blob/134fe2896275bb926b49743c1e25493f6b24cc31/types/encoding_helper.go#L9:6
// Instead, handle errors gracefully here.
Copy link
Member

Choose a reason for hiding this comment

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

Why would there ever be an encoding error and what would it mean to handle it gracefully? Seems like the kind of thing where a panic is actually waranted?

Copy link
Member Author

Choose a reason for hiding this comment

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

Why would there ever be an encoding error and what would it mean to handle it gracefully?

I can't think of a scenario where there could be and encoding error here (while encoding for hashing). If there is any case, it still feels wrong to me to panic instead of returning an error and let the caller decide if it is correct to panic (or handle the error in another way).
If we can not hash the header, this probably means a bug in the encoding library (or another bigger problem) and the caller should probably decide to panic anyways. I just feel it isn't the job of the hash method to decide.

@@ -111,3 +191,10 @@ pub struct Version {
)]
pub app: u64,
}

fn encode_bytes(bytes: &[u8]) -> Vec<u8> {
let mut chain_id_enc = vec![];
Copy link
Member

Choose a reason for hiding this comment

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

Should be called bytes_enc I guess

Copy link
Member Author

Choose a reason for hiding this comment

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

All other encoding methods are named encode_*. But it is a private helper function. Happy to rename it.

type Validator = Info;

fn hash(&self) -> Hash {
// TODO almost the same as above's pub fn hash(self) -> merkle::Hash
Copy link
Member

Choose a reason for hiding this comment

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

So we should be able to dedup?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, we can actually just delete the pub fn hash(self) -> merkle::Hash from impl Set. As far as I can see, it isn't used anywhere.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added in: 23042bb

if let Ok(sig) = ed25519::Signature::from_bytes(signature) {
return verifier.verify(sign_bytes, &sig).is_ok();
}
return false;
Copy link
Member

Choose a reason for hiding this comment

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

redundant?

where
B: BufMut,
{
// TODO: 1) everytime we encode sth. an error can occur. Change the trait to return a result
Copy link
Member

Choose a reason for hiding this comment

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

Why should we ever have an error here? Assuming we control what's being encoded. And how would we handle it gracefully? Again, seems there might not be a choice but to panic.

Copy link
Member Author

Choose a reason for hiding this comment

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

Same reason as above. Yes, probably we should panic if can't encode this but I would prefer to leave it to the caller to decide (especially as this aims to be a generic trait). But maybe I'm overthinking this.

{
// TODO: 1) everytime we encode sth. an error can occur. Change the trait to return a result
// instead to enable proper error handling.
// 2) Figure out where the chain_id should come from (if sign_bytes remains on Vote, the
Copy link
Member

Choose a reason for hiding this comment

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

Good point - may have been thinking we'd use something else here, eg. the CanonicalVote, which contains the ChainID

Copy link
Member Author

@liamsi liamsi Nov 5, 2019

Choose a reason for hiding this comment

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

So, if you are suggesting to implement the trait methods directly on CanonicalVote: indeed, this would solve that issue with the chain_id but the CanonicalVote does not contain the validator address, nor does it contain the signature. Both are needed to implement the current trait:

pub trait Vote {
    fn validator_id(&self) -> Id;
    fn sign_bytes(&self) -> Vec<u8>;
    fn signature(&self) -> &[u8];
}

Copy link
Member

Choose a reason for hiding this comment

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

Can we wrap the CanonicalVote in something that also contains the ID and the signature?

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 think that makes sense. Will create a separate struct tomorrow. Feel free to merge this (if you want), I won't forget this change.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done in c941c95. Name of the new wrapping type SignedVote.

@@ -92,6 +121,11 @@ impl Type {
pub fn to_u8(self) -> u8 {
self as u8
}

/// Serialize this type as a 32-bit unsigned integer
pub fn to_u32(self) -> u32 {
Copy link
Member

Choose a reason for hiding this comment

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

Added this because clippy didn't like to_u8() as u32

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks!

@liamsi liamsi marked this pull request as ready for review November 5, 2019 19:28
@liamsi
Copy link
Member Author

liamsi commented Nov 13, 2019

Note that @yihuang added and impl for lite::Commit (#67) which I've merged (and then slightly modified) in #63 (via 3e987fe and e1a7560).
I can "backport" this impl into this branch and we can deal with the changes of #63 separately.

After that the still to-be implemented traits are:

@liamsi
Copy link
Member Author

liamsi commented Nov 19, 2019

@ebuchman Is there anything left to do before we can merge this?

@ebuchman
Copy link
Member

@liamsi I'm unfortunately out of commission this week so will leave it to you. Thanks @yihuang for your contributions, much appreciated!!

@liamsi
Copy link
Member Author

liamsi commented Nov 27, 2019

OK, thanks. I'll merge in a bunch of (reviewed) changes from #63 into lite_impl. Will add more tests and if everything looks OK, merge all this into your branch bucky/lite. Think we should still do another review on bucky/lite -> master. Then we have a proper (skipping) light client to use.

@liamsi liamsi changed the title Trait impls Light client: trait impls Nov 27, 2019
@liamsi
Copy link
Member Author

liamsi commented Nov 28, 2019

Actually, no. Will merge your intial traits PR #31 to master and base the follow-up PRs (this one #36 and #63) to master too.

@liamsi liamsi changed the base branch from bucky/lite to master November 28, 2019 19:11
Copy link
Contributor

@ancazamfir ancazamfir 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! A couple minor comments.

@@ -127,7 +127,8 @@ where
};

// check vote is valid from validator
if !val.verify_signature(vote.sign_bytes(), vote.signature()) {
let sign_bytes = vote.sign_bytes();
if !val.verify_signature(&sign_bytes, vote.signature()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Lines 121-133 are duplicated in verify_commit_trusting() and here. Maybe a function that takes a validator and a vote and returns error be useful to both? I know in verify_commit_trusting() we do an extra check but that could be done before or after.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch. There is a subtle difference in those lines though: in one case the validator is looked up in the set in the other it part of the looped elements (also some of the lines are part of the control flow via None => continue,). This could be deduplicated (completely) if we do not zip the iterators and do a lookup in both cases. I'll leave this as is for now.

Copy link
Contributor

Choose a reason for hiding this comment

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

There is a subtle difference in those lines though: in one case the validator is looked up in the set in the other it part of the looped elements (also some of the lines are part of the control flow via None => continue,).

Yes, I saw that but was thinking that once there is a validator and a vote the checks should be the same (most the loop body). But I looked closer and it's not much left in common to justify a separate function.

Another question, for verify_commit_full() we do assume: The vals and commit have a 1-to-1 correspondence; is this checked elsewhere? I was looking for a check that verifies that val.address == vote.validator_address. Or val.verify_signature() will take care of it?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, for vals_iter.zip(commit_iter), I believe this works as we want as long as there is no nil validator in the set (I think this is the case but I have a hard time navigating the Rust code :( ) ...otherwise according to the doc: If the first iterator returns None, zip will short-circuit and next will not be called on the second iterator.

Copy link
Member Author

Choose a reason for hiding this comment

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

The only check is that length matches. Also AFAIR the commits / validators are sorted. That means if the the len matches there is the desired correspondence. But I'll double check and follow up in #63. It seems this is more confusing than helpful maybe we should deduplicate and do the additional lookups instead.

@@ -67,6 +69,47 @@ impl Vote {
}
}

/// SignedVote is the union of a canoncialized vote, the signature on
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// SignedVote is the union of a canoncialized vote, the signature on
/// SignedVote is the union of a canonicalized vote, the signature on

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.

There is a notable lack of tests but this still represents good progress! In the name of keeping things moving along I would recommend merging this for now and following up with tests in the next PR 👍

@liamsi
Copy link
Member Author

liamsi commented Dec 10, 2019

Thanks for the review @brapse and @ancazamfir! I would also like to merge this as is. A few tests are in a follow-up PR #63 which I didn't merge in here as this PR would grow quite large.

@liamsi liamsi merged commit b30991f into master Dec 10, 2019
liamsi added a commit that referenced this pull request Dec 10, 2019
…p), and fmt:

 - #77 introduced an fmt issue
 - #36 was not up-to date regarding changes which were merged to master (renaming
   of simple_merkle)
 - new upstream release of made build of master fail (solution: explicitly add it
 ed25519-dalek (1.0.0-pre.3) with rand feature fixes that
@liamsi liamsi mentioned this pull request Dec 10, 2019
ebuchman pushed a commit that referenced this pull request Dec 10, 2019
…p), and fmt: (#83)

- #77 introduced an fmt issue
 - #36 was not up-to date regarding changes which were merged to master (renaming
   of simple_merkle)
 - new upstream release of made build of master fail (solution: explicitly add it
 ed25519-dalek (1.0.0-pre.3) with rand feature fixes that
@liamsi liamsi deleted the lite_impl branch December 10, 2019 21:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants