-
Notifications
You must be signed in to change notification settings - Fork 224
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
Conversation
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())) |
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.
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)?
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.
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.
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.
Note to myself: add back the test that checks the hash here:
https://github.com/interchainio/tendermint-rs/blob/262e252a04cb7d7b660a829f3240e7196ad955b9/tendermint/tests/rpc.rs#L167-L168
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.
done in e686c45
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.
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 ...
byteslices.push(evidence_hash_enc.as_slice()); | ||
byteslices.push(proposer_address_enc.as_slice()); | ||
|
||
Hash::Sha256(simple_hash_from_byte_slices(byteslices.as_slice())) |
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.
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/amino_types/vote.rs
Outdated
None => None, | ||
}, | ||
}), | ||
round: vote.round as i64, |
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.
Some reason this doesn't follow the same order as the fields in the struct, ie. the round before the block_id?
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.
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 { |
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.
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?
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.
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.
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.
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>
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.
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.
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.
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.
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.
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
}, | ||
}), | ||
round: vote.round as i64, | ||
timestamp: Some(TimeMsg::from(vote.timestamp)), |
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.
Same re why this is an Option
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.
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. |
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.
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?
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.
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![]; |
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.
Should be called bytes_enc
I guess
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.
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 |
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.
So we should be able to dedup?
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.
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.
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.
Added in: 23042bb
tendermint/src/validator.rs
Outdated
if let Ok(sig) = ed25519::Signature::from_bytes(signature) { | ||
return verifier.verify(sign_bytes, &sig).is_ok(); | ||
} | ||
return false; |
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.
redundant?
tendermint/src/vote.rs
Outdated
where | ||
B: BufMut, | ||
{ | ||
// TODO: 1) everytime we encode sth. an error can occur. Change the trait to return a result |
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.
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.
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.
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.
tendermint/src/vote.rs
Outdated
{ | ||
// 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 |
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.
Good point - may have been thinking we'd use something else here, eg. the CanonicalVote, which contains the ChainID
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.
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];
}
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.
Can we wrap the CanonicalVote in something that also contains the ID and the 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.
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.
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.
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 { |
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.
Added this because clippy didn't like to_u8() as u32
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.
Thanks!
- Consistency: Rename encode_bytes to bytes_enc - remove redundant return statement
Note that @yihuang added and impl for lite::Commit (#67) which I've merged (and then slightly modified) in #63 (via 3e987fe and e1a7560). After that the still to-be implemented traits are:
|
@ebuchman Is there anything left to do before we can merge this? |
OK, thanks. I'll merge in a bunch of (reviewed) changes from #63 into |
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.
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()) { |
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.
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.
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.
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.
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.
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?
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.
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.
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 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 |
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.
/// SignedVote is the union of a canoncialized vote, the signature on | |
/// SignedVote is the union of a canonicalized vote, the signature on |
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.
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 👍
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. |
This PR will have implementations of the traits created in #31 and a few tests.