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

Domain types for protobuf structs #537

Merged
merged 4 commits into from
Sep 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"light-client",
"light-node",
"proto",
"proto-derive",
"rpc",
"tendermint",
"testgen"
Expand Down
4 changes: 2 additions & 2 deletions light-client/src/operations/voting_power.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ impl VotingPowerCalculator for ProdVotingPowerCalculator {

// 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) {
if let Some(vote) = non_absent_vote(signature, idx as u16, &signed_header.commit) {
Some((signature, vote))
} else {
None
Expand Down Expand Up @@ -179,7 +179,7 @@ impl VotingPowerCalculator for ProdVotingPowerCalculator {
}
}

fn non_absent_vote(commit_sig: &CommitSig, validator_index: u64, commit: &Commit) -> Option<Vote> {
fn non_absent_vote(commit_sig: &CommitSig, validator_index: u16, commit: &Commit) -> Option<Vote> {
let (validator_address, timestamp, signature, block_id) = match commit_sig {
CommitSig::BlockIDFlagAbsent { .. } => return None,
CommitSig::BlockIDFlagCommit {
Expand Down
12 changes: 2 additions & 10 deletions light-client/src/predicates/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,11 @@ impl VerificationError {

impl ErrorExt for VerificationError {
fn not_enough_trust(&self) -> bool {
if let Self::NotEnoughTrust { .. } = self {
true
} else {
false
}
matches!(self,Self::NotEnoughTrust { .. })
}

fn has_expired(&self) -> bool {
if let Self::NotWithinTrustPeriod { .. } = self {
true
} else {
false
}
matches!(self,Self::NotWithinTrustPeriod { .. })
}

fn is_timeout(&self) -> bool {
Expand Down
15 changes: 15 additions & 0 deletions proto-derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "tendermint-proto-derive"
version = "0.1.0"
authors = ["Greg Szabo <[email protected]>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = "1.0"
90 changes: 90 additions & 0 deletions proto-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::spanned::Spanned;

#[proc_macro_derive(DomainType, attributes(rawtype))]
pub fn domaintype(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as syn::DeriveInput);
expand_domaintype(&input)
}

fn expand_domaintype(input: &syn::DeriveInput) -> TokenStream {
let ident = &input.ident;

// Todo: Make this function more robust and easier to read.

let rawtype_attributes = &input
.attrs
.iter()
.filter(|&attr| attr.path.is_ident("rawtype"))
.collect::<Vec<&syn::Attribute>>();
if rawtype_attributes.len() != 1 {
return syn::Error::new(
rawtype_attributes.first().span(),
"exactly one #[rawtype(RawType)] expected",
)
.to_compile_error()
.into();
}

let rawtype_tokens = rawtype_attributes[0]
.tokens
.clone()
.into_iter()
.collect::<Vec<quote::__private::TokenTree>>();
if rawtype_tokens.len() != 1 {
return syn::Error::new(rawtype_attributes[0].span(), "#[rawtype(RawType)] expected")
.to_compile_error()
.into();
}

let rawtype = match &rawtype_tokens[0] {
proc_macro2::TokenTree::Group(group) => group.stream(),
_ => {
return syn::Error::new(
rawtype_tokens[0].span(),
"#[rawtype(RawType)] group expected",
)
.to_compile_error()
.into()
}
};

let gen = quote! {
impl ::tendermint_proto::DomainType<#rawtype> for #ident {

fn encode<B: ::tendermint_proto::bytes::BufMut>(self, buf: &mut B) -> ::std::result::Result<(), ::tendermint_proto::Error> {
use ::tendermint_proto::prost::Message;
#rawtype::from(self).encode(buf).map_err(|e| ::tendermint_proto::Kind::EncodeMessage.context(e).into())
}

fn encode_length_delimited<B: ::tendermint_proto::bytes::BufMut>(self, buf: &mut B) -> ::std::result::Result<(), ::tendermint_proto::Error> {
use ::tendermint_proto::prost::Message;
#rawtype::from(self).encode_length_delimited(buf).map_err(|e| ::tendermint_proto::Kind::EncodeMessage.context(e).into())
}

fn decode<B: ::tendermint_proto::bytes::Buf>(buf: B) -> Result<Self, ::tendermint_proto::Error> {
use ::tendermint_proto::prost::Message;
#rawtype::decode(buf).map_or_else(
|e| ::std::result::Result::Err(::tendermint_proto::Kind::DecodeMessage.context(e).into()),
|t| Self::try_from(t).map_err(|e| ::tendermint_proto::Kind::TryIntoDomainType.context(e).into())
)
}

fn decode_length_delimited<B: ::tendermint_proto::bytes::Buf>(buf: B) -> Result<Self, ::tendermint_proto::Error> {
use ::tendermint_proto::prost::Message;
#rawtype::decode_length_delimited(buf).map_or_else(
|e| ::std::result::Result::Err(::tendermint_proto::Kind::DecodeMessage.context(e).into()),
|t| Self::try_from(t).map_err(|e| ::tendermint_proto::Kind::TryIntoDomainType.context(e).into())
)
}

fn encoded_len(self) -> usize {
use ::tendermint_proto::prost::Message;
#rawtype::from(self).encoded_len()
}

}
};
gen.into()
}
7 changes: 7 additions & 0 deletions proto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ description = """
[package.metadata.docs.rs]
all-features = true

[features]
default = ["tendermint-proto-derive"]

[dependencies]
prost = { version = "0.6" }
prost-types = { version = "0.6" }
tendermint-proto-derive = { path = "../proto-derive", optional = true }
bytes = "0.5.6"
anomaly = "0.2.0"
thiserror = "1.0.20"
27 changes: 27 additions & 0 deletions proto/src/domaintype.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use crate::Error;
use bytes::{Buf, BufMut};
use prost::Message;

/// DomainType trait allows protobuf encoding and decoding for domain types
pub trait DomainType<T: Message + From<Self>>: Sized {
/// Encodes the DomainType into a buffer.
///
/// The DomainType will be consumed.
fn encode<B: BufMut>(self, buf: &mut B) -> Result<(), Error>;

/// Encodes the DomainType with a length-delimiter to a buffer.
///
/// An error will be returned if the buffer does not have sufficient capacity.
fn encode_length_delimited<B: BufMut>(self, buf: &mut B) -> Result<(), Error>;

/// Decodes an instance of the message from a buffer and then converts it into DomainType.
///
/// The entire buffer will be consumed.
fn decode<B: Buf>(buf: B) -> Result<Self, Error>;

/// Decodes a length-delimited instance of the message from the buffer.
fn decode_length_delimited<B: Buf>(buf: B) -> Result<Self, Error>;

/// Returns the encoded length of the message without a length delimiter.
fn encoded_len(self) -> usize;
}
37 changes: 37 additions & 0 deletions proto/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//! This module defines the various errors that be raised during DomainType conversions.

use anomaly::{BoxError, Context};
use thiserror::Error;

/// An error that can be raised by the DomainType conversions.
pub type Error = anomaly::Error<Kind>;

/// Various kinds of errors that can be raised.
#[derive(Clone, Debug, Error)]
pub enum Kind {
/// TryFrom Prost Message failed during decoding
#[error("error converting message type into domain type")]
TryIntoDomainType,

/// encoding prost Message into buffer failed
#[error("error encoding message into buffer")]
EncodeMessage,

/// decoding buffer into prost Message failed
#[error("error decoding buffer into message")]
DecodeMessage,
}

impl Kind {
/// Add a given source error as context for this error kind
///
/// This is typically use with `map_err` as follows:
///
/// ```ignore
/// let x = self.something.do_stuff()
/// .map_err(|e| error::Kind::Config.context(e))?;
/// ```
pub fn context(self, source: impl Into<BoxError>) -> Context<Self> {
Context::new(self, Some(source.into()))
}
}
17 changes: 17 additions & 0 deletions proto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,20 @@ mod tendermint {
}

pub use tendermint::*;

mod domaintype;
pub use domaintype::DomainType;

mod error;
pub use error::{Error, Kind};

// Re-export the bytes and prost crates for use within derived code.
#[doc(hidden)]
pub use bytes;
#[doc(hidden)]
pub use prost;

// Re-export the DomainType derive macro #[derive(DomainType)]
#[cfg(feature = "tendermint-proto-derive")]
#[doc(hidden)]
pub use tendermint_proto_derive::DomainType;
83 changes: 76 additions & 7 deletions proto/tests/unit.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,79 @@
use std::convert::TryFrom;
use tendermint_proto::types::BlockId as RawBlockId;
use tendermint_proto::types::PartSetHeader as RawPartSetHeader;
use tendermint_proto::DomainType;

// Example implementation of a protobuf struct using DomainType.
#[derive(DomainType, Clone)]
#[rawtype(RawBlockId)]
pub struct BlockId {
hash: String,
part_set_header_exists: bool,
}

// DomainTypes MUST have the TryFrom trait to convert from RawTypes.
impl TryFrom<RawBlockId> for BlockId {
type Error = &'static str;

fn try_from(value: RawBlockId) -> Result<Self, Self::Error> {
Ok(BlockId {
hash: String::from_utf8(value.hash)
.map_err(|_| "Could not convert vector to string")?,
part_set_header_exists: value.part_set_header != None,
})
}
}

// DomainTypes MUST be able to convert to RawTypes without errors using the From trait.
impl From<BlockId> for RawBlockId {
fn from(value: BlockId) -> Self {
RawBlockId {
hash: value.hash.into_bytes(),
part_set_header: match value.part_set_header_exists {
true => Some(RawPartSetHeader {
total: 0,
hash: vec![],
}),
false => None,
},
}
}
}

#[test]
pub fn domaintype_struct_example() {
let my_domain_type = BlockId {
hash: "Hello world!".to_string(),
part_set_header_exists: false,
};

let mut wire = vec![];
my_domain_type.clone().encode(&mut wire).unwrap();
assert_eq!(
wire,
vec![10, 12, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]
);
let new_domain_type = BlockId::decode(wire.as_ref()).unwrap();
assert_eq!(new_domain_type.hash, "Hello world!".to_string());
assert_eq!(new_domain_type.part_set_header_exists, false);
assert_eq!(my_domain_type.clone().encoded_len(), 14);
}

#[test]
pub fn import_evidence_info() {
use tendermint_proto::evidence::Info;
let x = Info {
committed: true,
priority: 0,
evidence: None,
pub fn domaintype_struct_length_delimited_example() {
let my_domain_type = BlockId {
hash: "Hello world!".to_string(),
part_set_header_exists: false,
};
assert_eq!(x.committed, true);

let mut wire = vec![];
my_domain_type.encode_length_delimited(&mut wire).unwrap();
assert_eq!(
wire,
vec![14, 10, 12, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]
);

let new_domain_type = BlockId::decode_length_delimited(wire.as_ref()).unwrap();
assert_eq!(new_domain_type.hash, "Hello world!".to_string());
assert_eq!(new_domain_type.part_set_header_exists, false);
}
5 changes: 1 addition & 4 deletions tendermint/src/amino_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,12 @@ pub mod message;
pub mod proposal;
pub mod signature;
pub mod validate;
pub mod version;
pub mod vote;

pub use self::{
block_id::{BlockId, CanonicalBlockId, CanonicalPartSetHeader, PartSetHeader},
ed25519::{PubKeyRequest, PubKeyResponse},
proposal::{SignProposalRequest, SignedProposalResponse},
signature::{SignableMsg, SignedMsgType},
signature::SignableMsg,
validate::ConsensusMessage,
version::ConsensusVersion,
vote::{SignVoteRequest, SignedVoteResponse},
};
Loading