diff --git a/CHANGELOG.md b/CHANGELOG.md index e673a1514..c03492bde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,14 @@ - Add spec for the light client attack evidence handling ([#526]) - Return RFC6962 hash for empty merkle tree ([#498]) - The `tendermint`, `tendermint-rpc`, and `tendermint-light-client` crates now compile to WASM on the `wasm32-unknown-unknown` and `wasm32-wasi` targets ([#463]) +- Implement protobuf encoding/decoding of Tendermint Proto types ([#504]) +- Separate protobuf types from Rust domain types using the DomainType trait ([#535]) [#526]: https://github.com/informalsystems/tendermint-rs/issues/526 [#498]: https://github.com/informalsystems/tendermint-rs/issues/498 [#463]: https://github.com/informalsystems/tendermint-rs/issues/463 +[#504]: https://github.com/informalsystems/tendermint-rs/issues/504 +[#535]: https://github.com/informalsystems/tendermint-rs/issues/535 ## v0.16.0 diff --git a/Cargo.toml b/Cargo.toml index e3cb00264..215febca3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "light-client", "light-node", "proto", + "proto-derive", "rpc", "tendermint", "testgen" diff --git a/light-client/src/operations/voting_power.rs b/light-client/src/operations/voting_power.rs index 915913372..590dc83ba 100644 --- a/light-client/src/operations/voting_power.rs +++ b/light-client/src/operations/voting_power.rs @@ -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 @@ -179,7 +179,7 @@ impl VotingPowerCalculator for ProdVotingPowerCalculator { } } -fn non_absent_vote(commit_sig: &CommitSig, validator_index: u64, commit: &Commit) -> Option { +fn non_absent_vote(commit_sig: &CommitSig, validator_index: u16, commit: &Commit) -> Option { let (validator_address, timestamp, signature, block_id) = match commit_sig { CommitSig::BlockIDFlagAbsent { .. } => return None, CommitSig::BlockIDFlagCommit { diff --git a/proto-derive/Cargo.toml b/proto-derive/Cargo.toml new file mode 100644 index 000000000..a62629ee9 --- /dev/null +++ b/proto-derive/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tendermint-proto-derive" +version = "0.1.0" +authors = ["Greg Szabo "] +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" diff --git a/proto-derive/src/lib.rs b/proto-derive/src/lib.rs new file mode 100644 index 000000000..d1ec31bde --- /dev/null +++ b/proto-derive/src/lib.rs @@ -0,0 +1,95 @@ +//! The DomainType derive macro implements the tendermint_proto::DomainType trait. +//! This implementation uses the Prost library to convert between Raw types and byte streams. +//! +//! Read more about how to use this macro in the DomainType trait definition. + +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::>(); + 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::>(); + 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(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(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(buf: B) -> Result { + 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(buf: B) -> Result { + 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() +} diff --git a/proto/Cargo.toml b/proto/Cargo.toml index 449a6bc51..72ff2ff2d 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -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" +anomaly = "0.2" +thiserror = "1.0" diff --git a/proto/src/domaintype.rs b/proto/src/domaintype.rs new file mode 100644 index 000000000..9a656accc --- /dev/null +++ b/proto/src/domaintype.rs @@ -0,0 +1,79 @@ +//! DomainType trait +//! +//! The DomainType trait allows separation of the data sent on the wire (currently encoded using +//! protobuf) from the structures used in Rust. The structures used to encode/decode from/to the wire +//! are called "Raw" types (they mirror the definitions in the specifications) and the Rust types +//! we use internally are called the "Domain" types. These Domain types can implement additional +//! checks and conversions to consume the incoming data easier for a Rust developer. +//! +//! The benefits include decoding the wire into a struct that is inherently valid as well as hiding +//! the encoding and decoding details from the developer. This latter is important if/when we decide +//! to exchange the underlying Prost library with something else. (Another protobuf implementation +//! or a completely different encoding.) Encoding is not the core product of Tendermint it's a +//! necessary dependency. +//! +//! +//! Decode: bytestream -> Raw -> Domain +//! The `decode` function takes two steps to decode from a bytestream to a DomainType: +//! +//! 1. Decode the bytestream into a Raw type using the Prost library, +//! 2. Transform that Raw type into a Domain type using the TryFrom trait of the DomainType. +//! +//! +//! Encode: Domain -> Raw -> bytestream +//! The `encode` function takes two steps to encode a DomainType into a bytestream: +//! +//! 1. Transform the Domain type into a Raw type using the From trait of the DomainType, +//! 2. Encode the Raw type into a bytestream using the Prost library. +//! +//! +//! Note that in the case of encode, the transformation to Raw type is infallible: +//! Rust structs should always be ready to be encoded to the wire. +//! +//! Note that the Prost library and the TryFrom method have their own set of errors. These are +//! merged into a custom Error type defined in this crate for easier handling. +//! +//! +//! How to implement a DomainType struct: +//! 1. Implement your struct based on your expectations for the developer +//! 2. Add the derive macro `#[derive(DomainType)]` on top of it +//! 3. Add the Raw type as a parameter of the DomainType trait (`[rawtype(MyRawType)]`) +//! 4. Implement the `TryFrom for MyDomainType` trait +//! 5. Implement the `From for MyRawType` trait +//! +//! Note: the `[rawtype()]` parameter is similar to how `serde` implements serialization through a +//! `[serde(with="")]` interim type. +//! + +use crate::Error; +use bytes::{Buf, BufMut}; +use prost::Message; + +/// DomainType trait allows protobuf encoding and decoding for domain types +pub trait DomainType>: Sized { + /// Encodes the DomainType into a buffer. + /// + /// The DomainType will be consumed. + fn encode(self, buf: &mut B) -> Result<(), Error>; + + /// Encodes the DomainType with a length-delimiter to a buffer. + /// + /// The DomainType will be consumed. + /// An error will be returned if the buffer does not have sufficient capacity. + fn encode_length_delimited(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(buf: B) -> Result; + + /// Decodes a length-delimited instance of the message from the buffer. + /// + /// The entire buffer will be consumed. + fn decode_length_delimited(buf: B) -> Result; + + /// Returns the encoded length of the message without a length delimiter. + /// + /// The DomainType will be consumed. + fn encoded_len(self) -> usize; +} diff --git a/proto/src/error.rs b/proto/src/error.rs new file mode 100644 index 000000000..4b6da606d --- /dev/null +++ b/proto/src/error.rs @@ -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; + +/// 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) -> Context { + Context::new(self, Some(source.into())) + } +} diff --git a/proto/src/lib.rs b/proto/src/lib.rs index d90e81deb..f223cf4b8 100644 --- a/proto/src/lib.rs +++ b/proto/src/lib.rs @@ -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; diff --git a/proto/tests/unit.rs b/proto/tests/unit.rs index 9293e45dd..2dbc58b69 100644 --- a/proto/tests/unit.rs +++ b/proto/tests/unit.rs @@ -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 for BlockId { + type Error = &'static str; + + fn try_from(value: RawBlockId) -> Result { + 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 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.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); } diff --git a/tendermint/Cargo.toml b/tendermint/Cargo.toml index 8ea65453e..db5a55f86 100644 --- a/tendermint/Cargo.toml +++ b/tendermint/Cargo.toml @@ -42,8 +42,8 @@ ed25519-dalek = { version = "1", features = ["serde"] } futures = "0.3" k256 = { version = "0.4", optional = true, features = ["ecdsa"] } once_cell = "1.3" -prost-amino = "0.6" -prost-amino-derive = "0.6" +prost = "0.6" +prost-types = "0.6" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_bytes = "0.11" @@ -54,6 +54,7 @@ subtle = "2" subtle-encoding = { version = "0.5", features = ["bech32-preview"] } tai64 = { version = "3", features = ["chrono"] } thiserror = "1" +tendermint-proto = { path = "../proto" } toml = { version = "0.5" } zeroize = { version = "1.1", features = ["zeroize_derive"] } ripemd160 = { version = "0.9", optional = true } diff --git a/tendermint/src/amino_types.rs b/tendermint/src/amino_types.rs index bad6c89e8..417161c04 100644 --- a/tendermint/src/amino_types.rs +++ b/tendermint/src/amino_types.rs @@ -6,51 +6,15 @@ pub mod block_id; pub mod ed25519; pub mod message; -pub mod ping; pub mod proposal; -pub mod remote_error; pub mod signature; -pub mod time; pub mod validate; -pub mod version; pub mod vote; pub use self::{ - block_id::{BlockId, CanonicalBlockId, CanonicalPartSetHeader, PartsSetHeader}, - ed25519::{ - PubKeyRequest, PubKeyResponse, AMINO_NAME as PUBKEY_AMINO_NAME, - AMINO_PREFIX as PUBKEY_PREFIX, - }, - ping::{PingRequest, PingResponse, AMINO_NAME as PING_AMINO_NAME, AMINO_PREFIX as PING_PREFIX}, - proposal::{ - SignProposalRequest, SignedProposalResponse, AMINO_NAME as PROPOSAL_AMINO_NAME, - AMINO_PREFIX as PROPOSAL_PREFIX, - }, - remote_error::RemoteError, - signature::{SignableMsg, SignedMsgType}, - time::TimeMsg, + block_id::{BlockId, CanonicalBlockId, CanonicalPartSetHeader, PartSetHeader}, + ed25519::{PubKeyRequest, PubKeyResponse}, + proposal::{SignProposalRequest, SignedProposalResponse}, + signature::SignableMsg, validate::ConsensusMessage, - version::ConsensusVersion, - vote::{ - SignVoteRequest, SignedVoteResponse, AMINO_NAME as VOTE_AMINO_NAME, - AMINO_PREFIX as VOTE_PREFIX, - }, }; - -use sha2::{Digest, Sha256}; - -/// Compute the Amino prefix for the given registered type name -pub fn compute_prefix(name: &str) -> Vec { - let mut sh = Sha256::default(); - sh.update(name.as_bytes()); - let output = sh.finalize(); - - output - .iter() - .filter(|&x| *x != 0x00) - .skip(3) - .filter(|&x| *x != 0x00) - .cloned() - .take(4) - .collect() -} diff --git a/tendermint/src/amino_types/block_id.rs b/tendermint/src/amino_types/block_id.rs index f7ec00b81..8708710d1 100644 --- a/tendermint/src/amino_types/block_id.rs +++ b/tendermint/src/amino_types/block_id.rs @@ -1,4 +1,4 @@ -use super::validate::{ConsensusMessage, Kind::InvalidHashSize, Kind::NegativeTotal}; +use super::validate::{ConsensusMessage, Kind::InvalidHashSize}; use crate::block::parts; use crate::{ block, @@ -6,30 +6,64 @@ use crate::{ hash, hash::{Hash, SHA256_HASH_SIZE}, }; -use prost_amino_derive::Message; +use std::convert::TryFrom; +use tendermint_proto::types::BlockId as RawBlockId; +use tendermint_proto::types::CanonicalBlockId as RawCanonicalBlockId; +use tendermint_proto::types::CanonicalPartSetHeader as RawCanonicalPartSetHeader; +use tendermint_proto::types::PartSetHeader as RawPartSetHeader; +use tendermint_proto::DomainType; -#[derive(Clone, PartialEq, Message)] +/// BlockID +#[derive(Clone, PartialEq, Debug, DomainType)] +#[rawtype(RawBlockId)] pub struct BlockId { - #[prost_amino(bytes, tag = "1")] pub hash: Vec, - #[prost_amino(message, tag = "2")] - pub parts_header: Option, + pub part_set_header: ::std::option::Option, +} + +impl TryFrom for BlockId { + type Error = Error; + + fn try_from(value: RawBlockId) -> Result { + Ok(BlockId { + hash: value.hash, + part_set_header: match value.part_set_header { + None => None, + Some(raw_part_set_header) => Some(PartSetHeader::try_from(raw_part_set_header)?), + }, + }) + } +} + +impl From for RawBlockId { + fn from(value: BlockId) -> Self { + RawBlockId { + hash: value.hash, + part_set_header: match value.part_set_header { + None => None, + Some(part_set_header) => Some(RawPartSetHeader::from(part_set_header)), + }, + } + } } impl BlockId { - pub fn new(hash: Vec, parts_header: Option) -> Self { - BlockId { hash, parts_header } + pub fn new(hash: Vec, part_set_header: Option) -> Self { + BlockId { + hash, + part_set_header, + } } } impl block::ParseId for BlockId { fn parse_block_id(&self) -> Result { let hash = Hash::new(hash::Algorithm::Sha256, &self.hash)?; - let parts_header = self - .parts_header + let part_set_header = self + .part_set_header .as_ref() - .and_then(PartsSetHeader::parse_parts_header); - Ok(block::Id::new(hash, parts_header)) + .and_then(PartSetHeader::parse_part_set_header); + Ok(block::Id::new(hash, part_set_header)) } } @@ -38,7 +72,7 @@ impl From<&block::Id> for BlockId { let bid_hash = bid.hash.as_bytes(); BlockId::new( bid_hash.to_vec(), - bid.parts.as_ref().map(PartsSetHeader::from), + bid.parts.as_ref().map(PartSetHeader::from), ) } } @@ -49,64 +83,108 @@ impl ConsensusMessage for BlockId { if !self.hash.is_empty() && self.hash.len() != SHA256_HASH_SIZE { return Err(InvalidHashSize.into()); } - self.parts_header + self.part_set_header .as_ref() .map_or(Ok(()), ConsensusMessage::validate_basic) } } -#[derive(Clone, PartialEq, Message)] +#[derive(Clone, PartialEq, DomainType)] +#[rawtype(RawCanonicalBlockId)] pub struct CanonicalBlockId { - #[prost_amino(bytes, tag = "1")] pub hash: Vec, - #[prost_amino(message, tag = "2")] - pub parts_header: Option, + pub part_set_header: Option, +} + +impl TryFrom for CanonicalBlockId { + type Error = Error; + + fn try_from(value: RawCanonicalBlockId) -> Result { + Ok(CanonicalBlockId { + hash: value.hash, + part_set_header: match value.part_set_header { + None => None, + Some(raw_part_set_header) => { + Some(CanonicalPartSetHeader::try_from(raw_part_set_header)?) + } + }, + }) + } +} + +impl From for RawCanonicalBlockId { + fn from(value: CanonicalBlockId) -> Self { + RawCanonicalBlockId { + hash: value.hash, + part_set_header: match value.part_set_header { + None => None, + Some(part_set_header) => Some(part_set_header.into()), + }, + } + } } impl block::ParseId for CanonicalBlockId { fn parse_block_id(&self) -> Result { let hash = Hash::new(hash::Algorithm::Sha256, &self.hash)?; - let parts_header = self - .parts_header + let part_set_header = self + .part_set_header .as_ref() - .and_then(CanonicalPartSetHeader::parse_parts_header); - Ok(block::Id::new(hash, parts_header)) + .and_then(CanonicalPartSetHeader::parse_part_set_header); + Ok(block::Id::new(hash, part_set_header)) } } -#[derive(Clone, PartialEq, Message)] -pub struct PartsSetHeader { - #[prost_amino(int64, tag = "1")] +/// PartsetHeader +#[derive(Clone, PartialEq, Debug, DomainType)] +#[rawtype(RawPartSetHeader)] +pub struct PartSetHeader { pub total: i64, - #[prost_amino(bytes, tag = "2")] pub hash: Vec, } -impl PartsSetHeader { +impl TryFrom for PartSetHeader { + type Error = Error; + + fn try_from(value: RawPartSetHeader) -> Result { + Ok(PartSetHeader { + total: value.total as i64, + hash: value.hash, + }) + } +} + +impl From for RawPartSetHeader { + fn from(value: PartSetHeader) -> Self { + RawPartSetHeader { + total: value.total as u32, + hash: value.hash, + } + } +} + +impl PartSetHeader { pub fn new(total: i64, hash: Vec) -> Self { - PartsSetHeader { total, hash } + PartSetHeader { total, hash } } } -impl From<&parts::Header> for PartsSetHeader { +impl From<&parts::Header> for PartSetHeader { fn from(parts: &parts::Header) -> Self { - PartsSetHeader::new(parts.total as i64, parts.hash.as_bytes().to_vec()) + PartSetHeader::new(parts.total as i64, parts.hash.as_bytes().to_vec()) } } -impl PartsSetHeader { - fn parse_parts_header(&self) -> Option { +impl PartSetHeader { + fn parse_part_set_header(&self) -> Option { Hash::new(hash::Algorithm::Sha256, &self.hash) .map(|hash| block::parts::Header::new(self.total as u64, hash)) .ok() } } -impl ConsensusMessage for PartsSetHeader { +impl ConsensusMessage for PartSetHeader { fn validate_basic(&self) -> Result<(), Error> { - if self.total < 0 { - return Err(NegativeTotal.into()); - } // Hash can be empty in case of POLBlockID.PartsHeader in Proposal. if !self.hash.is_empty() && self.hash.len() != SHA256_HASH_SIZE { return Err(InvalidHashSize.into()); @@ -115,16 +193,35 @@ impl ConsensusMessage for PartsSetHeader { } } -#[derive(Clone, PartialEq, Message)] +#[derive(Clone, PartialEq, DomainType)] +#[rawtype(RawCanonicalPartSetHeader)] pub struct CanonicalPartSetHeader { - #[prost_amino(bytes, tag = "1")] + pub total: u32, pub hash: Vec, - #[prost_amino(int64, tag = "2")] - pub total: i64, +} + +impl TryFrom for CanonicalPartSetHeader { + type Error = Error; + + fn try_from(value: RawCanonicalPartSetHeader) -> Result { + Ok(CanonicalPartSetHeader { + total: value.total, + hash: value.hash, + }) + } +} + +impl From for RawCanonicalPartSetHeader { + fn from(value: CanonicalPartSetHeader) -> Self { + RawCanonicalPartSetHeader { + total: value.total, + hash: value.hash, + } + } } impl CanonicalPartSetHeader { - fn parse_parts_header(&self) -> Option { + fn parse_part_set_header(&self) -> Option { Hash::new(hash::Algorithm::Sha256, &self.hash) .map(|hash| block::parts::Header::new(self.total as u64, hash)) .ok() diff --git a/tendermint/src/amino_types/ed25519.rs b/tendermint/src/amino_types/ed25519.rs index 345b3059a..91253af9e 100644 --- a/tendermint/src/amino_types/ed25519.rs +++ b/tendermint/src/amino_types/ed25519.rs @@ -1,13 +1,14 @@ -use super::compute_prefix; use crate::{ error, public_key::{Ed25519, PublicKey}, Error, }; use anomaly::format_err; -use once_cell::sync::Lazy; -use prost_amino_derive::Message; use std::convert::TryFrom; +use tendermint_proto::crypto::public_key::Sum; +use tendermint_proto::privval::PubKeyRequest as RawPubKeyRequest; +use tendermint_proto::privval::PubKeyResponse as RawPubKeyResponse; +use tendermint_proto::DomainType; // Note:On the golang side this is generic in the sense that it could everything that implements // github.com/tendermint/tendermint/crypto.PubKey @@ -15,19 +16,58 @@ use std::convert::TryFrom; // version. // TODO(ismail): make this more generic (by modifying prost and adding a trait for PubKey) -pub const AMINO_NAME: &str = "tendermint/remotesigner/PubKeyRequest"; -pub static AMINO_PREFIX: Lazy> = Lazy::new(|| compute_prefix(AMINO_NAME)); - -#[derive(Clone, PartialEq, Message)] -#[amino_name = "tendermint/remotesigner/PubKeyResponse"] +/// PubKeyResponse is a response message containing the public key. +#[derive(Clone, PartialEq, Debug, DomainType)] +#[rawtype(RawPubKeyResponse)] pub struct PubKeyResponse { - #[prost_amino(bytes, tag = "1", amino_name = "tendermint/PubKeyEd25519")] - pub pub_key_ed25519: Vec, + pub pub_key: Option, + pub error: Option, +} + +impl TryFrom for PubKeyResponse { + type Error = Error; + + fn try_from(value: RawPubKeyResponse) -> Result { + Ok(PubKeyResponse { + pub_key: value.pub_key, + error: value.error, + }) + } +} + +impl From for RawPubKeyResponse { + fn from(value: PubKeyResponse) -> Self { + RawPubKeyResponse { + pub_key: value.pub_key, + error: value.error, + } + } +} + +/// PubKeyRequest requests the consensus public key from the remote signer. +#[derive(Clone, PartialEq, Debug, DomainType)] +#[rawtype(RawPubKeyRequest)] +pub struct PubKeyRequest { + pub chain_id: String, } -#[derive(Clone, PartialEq, Message)] -#[amino_name = "tendermint/remotesigner/PubKeyRequest"] -pub struct PubKeyRequest {} +impl TryFrom for PubKeyRequest { + type Error = Error; + + fn try_from(value: RawPubKeyRequest) -> Result { + Ok(PubKeyRequest { + chain_id: value.chain_id, + }) + } +} + +impl From for RawPubKeyRequest { + fn from(value: PubKeyRequest) -> Self { + RawPubKeyRequest { + chain_id: value.chain_id, + } + } +} impl TryFrom for PublicKey { type Error = Error; @@ -35,9 +75,16 @@ impl TryFrom for PublicKey { // This does not check if the underlying pub_key_ed25519 has the right size. // The caller needs to make sure that this is actually the case. fn try_from(response: PubKeyResponse) -> Result { - Ed25519::from_bytes(&response.pub_key_ed25519) - .map(Into::into) - .map_err(|_| format_err!(error::Kind::InvalidKey, "malformed Ed25519 key").into()) + match &response + .pub_key + .ok_or_else(|| format_err!(error::Kind::InvalidKey, "empty pubkey"))? + .sum + .ok_or_else(|| format_err!(error::Kind::InvalidKey, "empty sum"))? + { + Sum::Ed25519(b) => Ed25519::from_bytes(b), + } + .map(Into::into) + .map_err(|_| format_err!(error::Kind::InvalidKey, "malformed key").into()) } } @@ -45,7 +92,12 @@ impl From for PubKeyResponse { fn from(public_key: PublicKey) -> PubKeyResponse { match public_key { PublicKey::Ed25519(ref pk) => PubKeyResponse { - pub_key_ed25519: pk.as_bytes().to_vec(), + pub_key: Some(tendermint_proto::crypto::PublicKey { + sum: Some(tendermint_proto::crypto::public_key::Sum::Ed25519( + pk.as_bytes().to_vec(), + )), + }), + error: None, }, #[cfg(feature = "secp256k1")] PublicKey::Secp256k1(_) => panic!("secp256k1 PubKeyResponse unimplemented"), @@ -57,45 +109,33 @@ impl From for PubKeyResponse { mod tests { use super::*; use ed25519_dalek::PUBLIC_KEY_LENGTH; - use prost_amino::Message; use std::convert::TryInto; + use tendermint_proto::DomainType; #[test] fn test_empty_pubkey_msg() { // test-vector generated via the following go code: - // - // -------------------------------------------------------------------- - //package main - // - //import ( - // "fmt" - // - // "github.com/tendermint/go-amino" - // "github.com/tendermint/tendermint/crypto" - // "github.com/tendermint/tendermint/privval" - //) - // - //func main() { - // cdc := amino.NewCodec() - // - // cdc.RegisterInterface((*crypto.PubKey)(nil), nil) - // cdc.RegisterConcrete(crypto.PubKeyEd25519{}, - // "tendermint/PubKeyEd25519", nil) - // cdc.RegisterConcrete(&privval.PubKeyRequest{}, - // "tendermint/remotesigner/PubKeyRequest", nil) - // b, _ := cdc.MarshalBinary(&privval.PubKeyRequest{}) - // fmt.Printf("%#v\n\n", b) - //} - // -------------------------------------------------------------------- - // Output: - // []byte{0x4, 0xcb, 0x94, 0xd6, 0x20} - // - // - - let want = vec![0x4, 0xcb, 0x94, 0xd6, 0x20]; - let msg = PubKeyRequest {}; + /* + import ( + "fmt" + "github.com/tendermint/tendermint/proto/tendermint/privval" + ) + func ed25519_empty() { + pkr := &privval.PubKeyRequest{ + ChainId: "", + } + pbpk, _ := pkr.Marshal() + fmt.Printf("%#v\n", pbpk) + + } + */ + + let want: Vec = vec![]; + let msg = PubKeyRequest { + chain_id: "".to_string(), + }; let mut got = vec![]; - let _have = msg.encode(&mut got); + let _have = msg.clone().encode(&mut got); assert_eq!(got, want); @@ -107,37 +147,50 @@ mod tests { #[test] fn test_ed25519_pubkey_msg() { - // test-vector generated exactly as for test_empty_pubkey_msg - // but with the following modifications: - // cdc.RegisterConcrete(&privval.PubKeyResponse{}, - // "tendermint/remotesigner/PubKeyResponse", nil) - // - // var pubKey [32]byte - // copy(pubKey[:],[]byte{0x79, 0xce, 0xd, 0xe0, 0x43, 0x33, 0x4a, 0xec, 0xe0, 0x8b, 0x7b, - // 0xb5, 0x61, 0xbc, 0xe7, 0xc1, - // 0xd4, 0x69, 0xc3, 0x44, 0x26, 0xec, 0xef, 0xc0, 0x72, 0xa, 0x52, 0x4d, 0x37, 0x32, 0xef, - // 0xed}) - // - // b, _ = cdc.MarshalBinary(&privval.PubKeyResponse{PubKey: crypto.PubKeyEd25519(pubKey)}) - // fmt.Printf("%#v\n\n", b) - // + // test-vector generated from Go + /* + import ( + "fmt" + "github.com/tendermint/tendermint/proto/tendermint/crypto" + "github.com/tendermint/tendermint/proto/tendermint/privval" + ) + + func ed25519_key() { + pkr := &privval.PubKeyResponse{ + PubKey: &crypto.PublicKey{ + Sum: &crypto.PublicKey_Ed25519{Ed25519: []byte{ + 0x79, 0xce, 0xd, 0xe0, 0x43, 0x33, 0x4a, 0xec, 0xe0, + 0x8b, 0x7b, 0xb5, 0x61, 0xbc, 0xe7, 0xc1, 0xd4, 0x69, + 0xc3, 0x44, 0x26, 0xec, 0xef, 0xc0, 0x72, 0xa, 0x52, + 0x4d, 0x37, 0x32, 0xef, 0xed, + }, + }, + }, + Error: nil, + } + pbpk, _ := pkr.Marshal() + fmt.Printf("%#v\n", pbpk) + + } + */ let encoded = vec![ - 0x2b, // length - 0x17, 0xe, 0xd5, 0x7c, // prefix - 0xa, 0x25, 0x16, 0x24, 0xde, 0x64, 0x20, 0x79, 0xce, 0xd, 0xe0, 0x43, 0x33, 0x4a, 0xec, - 0xe0, 0x8b, 0x7b, 0xb5, 0x61, 0xbc, 0xe7, 0xc1, 0xd4, 0x69, 0xc3, 0x44, 0x26, 0xec, - 0xef, 0xc0, 0x72, 0xa, 0x52, 0x4d, 0x37, 0x32, 0xef, 0xed, + 0xa, 0x22, 0xa, 0x20, 0x79, 0xce, 0xd, 0xe0, 0x43, 0x33, 0x4a, 0xec, 0xe0, 0x8b, 0x7b, + 0xb5, 0x61, 0xbc, 0xe7, 0xc1, 0xd4, 0x69, 0xc3, 0x44, 0x26, 0xec, 0xef, 0xc0, 0x72, + 0xa, 0x52, 0x4d, 0x37, 0x32, 0xef, 0xed, ]; let msg = PubKeyResponse { - pub_key_ed25519: vec![ - 0x79, 0xce, 0xd, 0xe0, 0x43, 0x33, 0x4a, 0xec, 0xe0, 0x8b, 0x7b, 0xb5, 0x61, 0xbc, - 0xe7, 0xc1, 0xd4, 0x69, 0xc3, 0x44, 0x26, 0xec, 0xef, 0xc0, 0x72, 0xa, 0x52, 0x4d, - 0x37, 0x32, 0xef, 0xed, - ], + pub_key: Some(tendermint_proto::crypto::PublicKey { + sum: Some(tendermint_proto::crypto::public_key::Sum::Ed25519(vec![ + 0x79, 0xce, 0xd, 0xe0, 0x43, 0x33, 0x4a, 0xec, 0xe0, 0x8b, 0x7b, 0xb5, 0x61, + 0xbc, 0xe7, 0xc1, 0xd4, 0x69, 0xc3, 0x44, 0x26, 0xec, 0xef, 0xc0, 0x72, 0xa, + 0x52, 0x4d, 0x37, 0x32, 0xef, 0xed, + ])), + }), + error: None, }; let mut got = vec![]; - let _have = msg.encode(&mut got); + let _have = msg.clone().encode(&mut got); assert_eq!(got, encoded); @@ -156,11 +209,14 @@ mod tests { ]; let want = PublicKey::Ed25519(Ed25519::from_bytes(&raw_pk).unwrap()); let pk = PubKeyResponse { - pub_key_ed25519: vec![ - 0xaf, 0xf3, 0x94, 0xc5, 0xb7, 0x5c, 0xfb, 0xd, 0xd9, 0x28, 0xe5, 0x8a, 0x92, 0xdd, - 0x76, 0x55, 0x2b, 0x2e, 0x8d, 0x19, 0x6f, 0xe9, 0x12, 0x14, 0x50, 0x80, 0x6b, 0xd0, - 0xd9, 0x3f, 0xd0, 0xcb, - ], + pub_key: Some(tendermint_proto::crypto::PublicKey { + sum: Some(tendermint_proto::crypto::public_key::Sum::Ed25519(vec![ + 0xaf, 0xf3, 0x94, 0xc5, 0xb7, 0x5c, 0xfb, 0xd, 0xd9, 0x28, 0xe5, 0x8a, 0x92, + 0xdd, 0x76, 0x55, 0x2b, 0x2e, 0x8d, 0x19, 0x6f, 0xe9, 0x12, 0x14, 0x50, 0x80, + 0x6b, 0xd0, 0xd9, 0x3f, 0xd0, 0xcb, + ])), + }), + error: None, }; let orig = pk.clone(); let got: PublicKey = pk.try_into().unwrap(); @@ -176,7 +232,8 @@ mod tests { #[should_panic] fn test_empty_into() { let empty_msg = PubKeyResponse { - pub_key_ed25519: vec![], + pub_key: None, + error: None, }; // we expect this to panic: let _got: PublicKey = empty_msg.try_into().unwrap(); diff --git a/tendermint/src/amino_types/message.rs b/tendermint/src/amino_types/message.rs index c10e012fe..283a3ae88 100644 --- a/tendermint/src/amino_types/message.rs +++ b/tendermint/src/amino_types/message.rs @@ -1,10 +1,10 @@ -use prost_amino::encoding::encoded_len_varint; +use prost::encoding::encoded_len_varint; use std::convert::TryInto; -/// Extend the original prost_amino::Message trait with a few helper functions in order to -/// reduce boiler-plate code (and without modifying the prost-amino dependency). -pub trait AminoMessage: prost_amino::Message { - /// Directly amino encode a prost-amino message into a freshly created Vec. +/// Extend the original prost::Message trait with a few helper functions in order to +/// reduce boiler-plate code (and without modifying the prost dependency). +pub trait AminoMessage: prost::Message { + /// Directly encode a prost message into a freshly created Vec. /// This can be useful when passing those bytes directly to a hasher, or, /// to reduce boiler plate code when working with the encoded bytes. /// @@ -19,7 +19,7 @@ pub trait AminoMessage: prost_amino::Message { res } - /// Encode prost-amino message as length delimited. + /// Encode prost message as length delimited. /// /// Warning: Only use this method, if you are in control what will be encoded. /// If there is an encoding error, this method will panic. @@ -34,6 +34,6 @@ pub trait AminoMessage: prost_amino::Message { res } } -impl AminoMessage for M { +impl AminoMessage for M { // blanket impl } diff --git a/tendermint/src/amino_types/ping.rs b/tendermint/src/amino_types/ping.rs deleted file mode 100644 index 983288b4f..000000000 --- a/tendermint/src/amino_types/ping.rs +++ /dev/null @@ -1,14 +0,0 @@ -use super::compute_prefix; -use once_cell::sync::Lazy; -use prost_amino_derive::Message; - -pub const AMINO_NAME: &str = "tendermint/remotesigner/PingRequest"; -pub static AMINO_PREFIX: Lazy> = Lazy::new(|| compute_prefix(AMINO_NAME)); - -#[derive(Clone, PartialEq, Message)] -#[amino_name = "tendermint/remotesigner/PingRequest"] -pub struct PingRequest {} - -#[derive(Clone, PartialEq, Message)] -#[amino_name = "tendermint/remotesigner/PingResponse"] -pub struct PingResponse {} diff --git a/tendermint/src/amino_types/proposal.rs b/tendermint/src/amino_types/proposal.rs index 03529d621..cf766d8e4 100644 --- a/tendermint/src/amino_types/proposal.rs +++ b/tendermint/src/amino_types/proposal.rs @@ -1,9 +1,6 @@ use super::{ block_id::{BlockId, CanonicalBlockId, CanonicalPartSetHeader}, - compute_prefix, - remote_error::RemoteError, - signature::{SignableMsg, SignedMsgType}, - time::TimeMsg, + signature::SignableMsg, validate::{ self, ConsensusMessage, Kind::InvalidMessageType, Kind::MissingConsensusMessage, Kind::NegativeHeight, Kind::NegativePOLRound, Kind::NegativeRound, @@ -14,65 +11,208 @@ use crate::{ chain, consensus, error, }; use bytes::BufMut; -use once_cell::sync::Lazy; -use prost_amino::{EncodeError, Message}; -use prost_amino_derive::Message; -use std::convert::TryFrom; +use prost::{EncodeError, Message}; +use prost_types::Timestamp; +use std::convert::{TryFrom, TryInto}; +use tendermint_proto::privval::RemoteSignerError; +use tendermint_proto::privval::SignProposalRequest as RawSignProposalRequest; +use tendermint_proto::privval::SignedProposalResponse as RawSignedProposalResponse; +use tendermint_proto::types::CanonicalProposal as RawCanonicalProposal; +use tendermint_proto::types::Proposal as RawProposal; +use tendermint_proto::types::SignedMsgType; +use tendermint_proto::DomainType; -#[derive(Clone, PartialEq, Message)] +#[derive(Clone, PartialEq, Debug, DomainType)] +#[rawtype(RawProposal)] pub struct Proposal { - #[prost_amino(uint32, tag = "1")] - pub msg_type: u32, - #[prost_amino(int64)] - pub height: i64, - #[prost_amino(int64)] - pub round: i64, - #[prost_amino(int64)] - pub pol_round: i64, - #[prost_amino(message)] + pub msg_type: u16, + pub height: u32, + pub round: u16, + pub pol_round: Option, pub block_id: Option, - #[prost_amino(message)] - pub timestamp: Option, - #[prost_amino(bytes)] + pub timestamp: Option, pub signature: Vec, } +impl Proposal { + pub fn pol_round_to_i32(&self) -> i32 { + match &self.pol_round { + Some(u) => *u as i32, + None => -1, + } + } +} + +impl TryFrom for Proposal { + type Error = validate::Error; + + fn try_from(value: RawProposal) -> Result { + if value.r#type < 0 { + return Err(InvalidMessageType.into()); + } + if value.height < 0 { + return Err(NegativeHeight.into()); + } + if value.round < 0 { + return Err(NegativeRound.into()); + } + if value.pol_round < -1 { + return Err(NegativePOLRound.into()); + } + let pol_round = match value.pol_round { + -1 => None, + n => Some(n as u16), + }; + let result = Proposal { + msg_type: value.r#type as u16, + height: value.height as u32, + round: value.round as u16, + pol_round, + block_id: match value.block_id { + None => None, + Some(raw_block_id) => Some(BlockId::try_from(raw_block_id).unwrap()), + }, + timestamp: value.timestamp, + signature: value.signature, + }; + result.validate_basic().map(|_| result) + } +} + +impl From for RawProposal { + fn from(value: Proposal) -> Self { + RawProposal { + r#type: value.msg_type as i32, + height: value.height as i64, + round: value.round as i32, + pol_round: value.pol_round_to_i32(), + block_id: match value.block_id { + None => None, + Some(block_id) => Some(block_id.into()), + }, + timestamp: value.timestamp, + signature: value.signature, + } + } +} + // TODO(tony): custom derive proc macro for this e.g. `derive(ParseBlockHeight)` impl block::ParseHeight for Proposal { fn parse_block_height(&self) -> Result { - block::Height::try_from(self.height) + Ok(block::Height::from(self.height)) } } -pub const AMINO_NAME: &str = "tendermint/remotesigner/SignProposalRequest"; -pub static AMINO_PREFIX: Lazy> = Lazy::new(|| compute_prefix(AMINO_NAME)); - -#[derive(Clone, PartialEq, Message)] -#[amino_name = "tendermint/remotesigner/SignProposalRequest"] +/// SignProposalRequest is a request to sign a proposal +#[derive(Clone, PartialEq, Debug, DomainType)] +#[rawtype(RawSignProposalRequest)] pub struct SignProposalRequest { - #[prost_amino(message, tag = "1")] pub proposal: Option, + pub chain_id: String, +} + +impl TryFrom for SignProposalRequest { + type Error = validate::Error; + + fn try_from(value: RawSignProposalRequest) -> Result { + Ok(SignProposalRequest { + proposal: match value.proposal { + None => None, + Some(proposal) => Some(Proposal::try_from(proposal)?), + }, + chain_id: value.chain_id, + }) + } } -#[derive(Clone, PartialEq, Message)] -struct CanonicalProposal { - #[prost_amino(uint32, tag = "1")] - msg_type: u32, /* this is a byte in golang, which is a varint encoded UInt8 (using amino's - * EncodeUvarint) */ - #[prost_amino(sfixed64)] - height: i64, - #[prost_amino(sfixed64)] - round: i64, - #[prost_amino(sfixed64)] - pol_round: i64, - #[prost_amino(message)] - block_id: Option, - #[prost_amino(message)] - timestamp: Option, - #[prost_amino(string)] +impl From for RawSignProposalRequest { + fn from(value: SignProposalRequest) -> Self { + RawSignProposalRequest { + proposal: match value.proposal { + None => None, + Some(proposal) => Some(proposal.into()), + }, + chain_id: value.chain_id, + } + } +} + +#[derive(Clone, PartialEq, DomainType)] +#[rawtype(RawCanonicalProposal)] +pub struct CanonicalProposal { + /// type alias for byte + pub msg_type: u16, + /// canonicalization requires fixed size encoding here + pub height: u32, + /// canonicalization requires fixed size encoding here + pub round: u32, + pub pol_round: Option, + pub block_id: Option, + pub timestamp: Option, pub chain_id: String, } +impl CanonicalProposal { + pub fn pol_round_to_i64(&self) -> i64 { + match &self.pol_round { + None => -1, + Some(u) => *u as i64, + } + } +} + +impl TryFrom for CanonicalProposal { + type Error = validate::Error; + + fn try_from(value: RawCanonicalProposal) -> Result { + if value.r#type < 0 { + return Err(InvalidMessageType.into()); + } + if value.height < 0 { + return Err(NegativeHeight.into()); + } + if value.round < 0 { + return Err(NegativeRound.into()); + } + if value.pol_round < -1 { + return Err(NegativePOLRound.into()); + } + let pol_round = match value.pol_round { + -1 => None, + n => Some(n as u32), + }; + Ok(CanonicalProposal { + msg_type: value.r#type as u16, + height: value.height as u32, + round: value.round as u32, + pol_round, + block_id: match value.block_id { + None => None, + Some(block_id) => Some(block_id.try_into()?), + }, + timestamp: value.timestamp, + chain_id: value.chain_id, + }) + } +} + +impl From for RawCanonicalProposal { + fn from(value: CanonicalProposal) -> Self { + RawCanonicalProposal { + r#type: value.msg_type as i32, + height: value.height as i64, + round: value.round as i64, + pol_round: value.pol_round_to_i64(), + block_id: match value.block_id { + None => None, + Some(block_id) => Some(block_id.into()), + }, + timestamp: value.timestamp, + chain_id: value.chain_id, + } + } +} + impl chain::ParseId for CanonicalProposal { fn parse_chain_id(&self) -> Result { self.chain_id.parse() @@ -81,17 +221,42 @@ impl chain::ParseId for CanonicalProposal { impl block::ParseHeight for CanonicalProposal { fn parse_block_height(&self) -> Result { - block::Height::try_from(self.height) + Ok(block::Height::from(self.height)) } } -#[derive(Clone, PartialEq, Message)] -#[amino_name = "tendermint/remotesigner/SignedProposalResponse"] +/// SignedProposalResponse is response containing a signed proposal or an error +#[derive(Clone, PartialEq, DomainType)] +#[rawtype(RawSignedProposalResponse)] pub struct SignedProposalResponse { - #[prost_amino(message, tag = "1")] pub proposal: Option, - #[prost_amino(message, tag = "2")] - pub err: Option, + pub error: Option, +} + +impl TryFrom for SignedProposalResponse { + type Error = validate::Error; + + fn try_from(value: RawSignedProposalResponse) -> Result { + Ok(SignedProposalResponse { + proposal: match value.proposal { + None => None, + Some(proposal) => Some(Proposal::try_from(proposal)?), + }, + error: value.error, + }) + } +} + +impl From for RawSignedProposalResponse { + fn from(value: SignedProposalResponse) -> Self { + RawSignedProposalResponse { + proposal: match value.proposal { + None => None, + Some(proposal) => Some(proposal.into()), + }, + error: value.error, + } + } } impl SignableMsg for SignProposalRequest { @@ -106,27 +271,27 @@ impl SignableMsg for SignProposalRequest { let proposal = spr.proposal.unwrap(); let cp = CanonicalProposal { chain_id: chain_id.to_string(), - msg_type: SignedMsgType::Proposal.to_u32(), + msg_type: SignedMsgType::Proposal as u16, height: proposal.height, block_id: match proposal.block_id { Some(bid) => Some(CanonicalBlockId { hash: bid.hash, - parts_header: match bid.parts_header { + part_set_header: match bid.part_set_header { Some(psh) => Some(CanonicalPartSetHeader { hash: psh.hash, - total: psh.total, + total: psh.total as u32, }), None => None, }, }), None => None, }, - pol_round: proposal.pol_round, - round: proposal.round, + pol_round: proposal.pol_round.map(|n| n as u32), + round: proposal.round as u32, timestamp: proposal.timestamp, }; - cp.encode_length_delimited(sign_bytes)?; + RawCanonicalProposal::from(cp).encode_length_delimited(sign_bytes)?; Ok(true) } fn set_signature(&mut self, sig: &ed25519::Signature) { @@ -143,11 +308,8 @@ impl SignableMsg for SignProposalRequest { fn consensus_state(&self) -> Option { match self.proposal { Some(ref p) => Some(consensus::State { - height: match block::Height::try_from(p.height) { - Ok(h) => h, - Err(_err) => return None, // TODO(tarcieri): return an error? - }, - round: p.round, + height: block::Height::from(p.height), + round: p.round as i64, step: 3, block_id: { match p.block_id { @@ -164,7 +326,9 @@ impl SignableMsg for SignProposalRequest { } fn height(&self) -> Option { - self.proposal.as_ref().map(|proposal| proposal.height) + self.proposal + .as_ref() + .map(|proposal| proposal.height as i64) } fn msg_type(&self) -> Option { @@ -174,18 +338,9 @@ impl SignableMsg for SignProposalRequest { impl ConsensusMessage for Proposal { fn validate_basic(&self) -> Result<(), validate::Error> { - if self.msg_type != SignedMsgType::Proposal.to_u32() { + if self.msg_type != SignedMsgType::Proposal as u16 { return Err(InvalidMessageType.into()); } - if self.height < 0 { - return Err(NegativeHeight.into()); - } - if self.round < 0 { - return Err(NegativeRound.into()); - } - if self.pol_round < -1 { - return Err(NegativePOLRound.into()); - } // TODO validate proposal's block_id // signature will be missing as the KMS provides it @@ -197,27 +352,27 @@ impl ConsensusMessage for Proposal { #[cfg(test)] mod tests { use super::*; - use crate::amino_types::block_id::PartsSetHeader; + use crate::amino_types::block_id::{BlockId, PartSetHeader}; + use crate::chain::Id; use chrono::{DateTime, Utc}; - use prost_amino::Message; #[test] fn test_serialization() { let dt = "2018-02-11T07:09:22.765Z".parse::>().unwrap(); - let t = TimeMsg { + let t = Timestamp { seconds: dt.timestamp(), nanos: dt.timestamp_subsec_nanos() as i32, }; let proposal = Proposal { - msg_type: SignedMsgType::Proposal.to_u32(), + msg_type: SignedMsgType::Proposal as u16, height: 12345, round: 23456, - pol_round: -1, + pol_round: None, block_id: Some(BlockId { - hash: b"hash".to_vec(), - parts_header: Some(PartsSetHeader { - total: 1_000_000, - hash: b"parts_hash".to_vec(), + hash: b"DEADBEEFDEADBEEFBAFBAFBAFBAFBAFA".to_vec(), + part_set_header: Some(PartSetHeader { + total: 65535, + hash: b"0022446688AACCEE1133557799BBDDFF".to_vec(), }), }), timestamp: Some(t), @@ -225,36 +380,50 @@ mod tests { }; let mut got = vec![]; - let _have = SignProposalRequest { + let request = SignProposalRequest { proposal: Some(proposal), - } - .encode(&mut got); - // test-vector generated via: - // cdc := amino.NewCodec() - // privval.RegisterRemoteSignerMsg(cdc) - // stamp, _ := time.Parse(time.RFC3339Nano, "2018-02-11T07:09:22.765Z") - // data, _ := cdc.MarshalBinaryLengthPrefixed(privval.SignProposalRequest{Proposal: - // &types.Proposal{ Type: types.ProposalType, // 0x20 - // Height: 12345, - // Round: 23456, - // POLRound: -1, - // BlockID: types.BlockID{ - // Hash: []byte("hash"), - // PartsHeader: types.PartSetHeader{ - // Hash: []byte("parts_hash"), - // Total: 1000000, - // }, - // }, - // Timestamp: stamp, - // }}) - // fmt.Println(strings.Join(strings.Split(fmt.Sprintf("%v", data), " "), ", ")) + chain_id: "test_chain_id".to_string(), + }; + let _have = request.sign_bytes(Id::from("test_chain_id"), &mut got); + + // the following vector is generated via: + /* + import ( + "fmt" + prototypes "github.com/tendermint/tendermint/proto/tendermint/types" + "github.com/tendermint/tendermint/types" + "strings" + "time" + ) + func proposalSerialize() { + stamp, _ := time.Parse(time.RFC3339Nano, "2018-02-11T07:09:22.765Z") + proposal := &types.Proposal{ + Type: prototypes.SignedMsgType(prototypes.ProposalType), + Height: 12345, + Round: 23456, + POLRound: -1, + BlockID: types.BlockID{ + Hash: []byte("DEADBEEFDEADBEEFBAFBAFBAFBAFBAFA"), + PartSetHeader: types.PartSetHeader{ + Hash: []byte("0022446688AACCEE1133557799BBDDFF"), + Total: 65535, + }, + }, + Timestamp: stamp, + } + signBytes := types.ProposalSignBytes("test_chain_id",proposal.ToProto()) + fmt.Println(strings.Join(strings.Split(fmt.Sprintf("%v", signBytes), " "), ", ")) + } + */ + let want = vec![ - 66, // len - 189, 228, 152, 226, // prefix - 10, 60, 8, 32, 16, 185, 96, 24, 160, 183, 1, 32, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 1, 42, 24, 10, 4, 104, 97, 115, 104, 18, 16, 8, 192, 132, 61, 18, 10, 112, - 97, 114, 116, 115, 95, 104, 97, 115, 104, 50, 12, 8, 162, 216, 255, 211, 5, 16, 192, - 242, 227, 236, 2, + 136, 1, 8, 32, 17, 57, 48, 0, 0, 0, 0, 0, 0, 25, 160, 91, 0, 0, 0, 0, 0, 0, 32, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 1, 42, 74, 10, 32, 68, 69, 65, 68, 66, 69, 69, + 70, 68, 69, 65, 68, 66, 69, 69, 70, 66, 65, 70, 66, 65, 70, 66, 65, 70, 66, 65, 70, 66, + 65, 70, 65, 18, 38, 8, 255, 255, 3, 18, 32, 48, 48, 50, 50, 52, 52, 54, 54, 56, 56, 65, + 65, 67, 67, 69, 69, 49, 49, 51, 51, 53, 53, 55, 55, 57, 57, 66, 66, 68, 68, 70, 70, 50, + 12, 8, 162, 216, 255, 211, 5, 16, 192, 242, 227, 236, 2, 58, 13, 116, 101, 115, 116, + 95, 99, 104, 97, 105, 110, 95, 105, 100, ]; assert_eq!(got, want) @@ -263,37 +432,39 @@ mod tests { #[test] fn test_deserialization() { let dt = "2018-02-11T07:09:22.765Z".parse::>().unwrap(); - let t = TimeMsg { + let t = Timestamp { seconds: dt.timestamp(), nanos: dt.timestamp_subsec_nanos() as i32, }; let proposal = Proposal { - msg_type: SignedMsgType::Proposal.to_u32(), + msg_type: SignedMsgType::Proposal as u16, height: 12345, round: 23456, timestamp: Some(t), - pol_round: -1, + pol_round: None, block_id: Some(BlockId { - hash: b"hash".to_vec(), - parts_header: Some(PartsSetHeader { - total: 1_000_000, - hash: b"parts_hash".to_vec(), + hash: b"DEADBEEFDEADBEEFBAFBAFBAFBAFBAFA".to_vec(), + part_set_header: Some(PartSetHeader { + total: 65535, + hash: b"0022446688AACCEE1133557799BBDDFF".to_vec(), }), }), signature: vec![], }; let want = SignProposalRequest { proposal: Some(proposal), + chain_id: "test_chain_id".to_string(), }; let data = vec![ - 66, // len - 189, 228, 152, 226, // prefix - 10, 60, 8, 32, 16, 185, 96, 24, 160, 183, 1, 32, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 1, 42, 24, 10, 4, 104, 97, 115, 104, 18, 16, 8, 192, 132, 61, 18, 10, 112, - 97, 114, 116, 115, 95, 104, 97, 115, 104, 50, 12, 8, 162, 216, 255, 211, 5, 16, 192, - 242, 227, 236, 2, + 10, 110, 8, 32, 16, 185, 96, 24, 160, 183, 1, 32, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 1, 42, 74, 10, 32, 68, 69, 65, 68, 66, 69, 69, 70, 68, 69, 65, 68, 66, 69, + 69, 70, 66, 65, 70, 66, 65, 70, 66, 65, 70, 66, 65, 70, 66, 65, 70, 65, 18, 38, 8, 255, + 255, 3, 18, 32, 48, 48, 50, 50, 52, 52, 54, 54, 56, 56, 65, 65, 67, 67, 69, 69, 49, 49, + 51, 51, 53, 53, 55, 55, 57, 57, 66, 66, 68, 68, 70, 70, 50, 12, 8, 162, 216, 255, 211, + 5, 16, 192, 242, 227, 236, 2, 18, 13, 116, 101, 115, 116, 95, 99, 104, 97, 105, 110, + 95, 105, 100, ]; match SignProposalRequest::decode(data.as_ref()) { diff --git a/tendermint/src/amino_types/remote_error.rs b/tendermint/src/amino_types/remote_error.rs deleted file mode 100644 index 1561e9cff..000000000 --- a/tendermint/src/amino_types/remote_error.rs +++ /dev/null @@ -1,32 +0,0 @@ -use prost_amino_derive::Message; - -#[derive(Clone, PartialEq, Message)] -pub struct RemoteError { - #[prost_amino(sint32, tag = "1")] - pub code: i32, - #[prost_amino(string, tag = "2")] - pub description: String, -} - -/// Error codes for remote signer failures -// TODO(tarcieri): add these to Tendermint. See corresponding TODO here: -// -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -#[repr(i32)] -pub enum RemoteErrorCode { - /// Generic error code useful when the others don't apply - RemoteSignerError = 1, - - /// Double signing detected - DoubleSignError = 2, -} - -impl RemoteError { - /// Create a new double signing error with the given message - pub fn double_sign(height: i64) -> Self { - RemoteError { - code: RemoteErrorCode::DoubleSignError as i32, - description: format!("double signing requested at height: {}", height), - } - } -} diff --git a/tendermint/src/amino_types/signature.rs b/tendermint/src/amino_types/signature.rs index 3844381e1..4609eaed9 100644 --- a/tendermint/src/amino_types/signature.rs +++ b/tendermint/src/amino_types/signature.rs @@ -1,7 +1,8 @@ use super::validate; use crate::{chain, consensus}; use bytes::BufMut; -use prost_amino::{DecodeError, EncodeError}; +use prost::EncodeError; +use tendermint_proto::types::SignedMsgType; /// Amino messages which are signable within a Tendermint network pub trait SignableMsg { @@ -19,39 +20,3 @@ pub trait SignableMsg { fn height(&self) -> Option; fn msg_type(&self) -> Option; } - -/// Signed message types. This follows: -/// -#[derive(Copy, Clone, Debug)] -pub enum SignedMsgType { - /// Votes - PreVote, - - /// Commits - PreCommit, - - /// Proposals - Proposal, -} - -impl SignedMsgType { - pub fn to_u32(self) -> u32 { - match self { - // Votes - SignedMsgType::PreVote => 0x01, - SignedMsgType::PreCommit => 0x02, - // Proposals - SignedMsgType::Proposal => 0x20, - } - } - - #[allow(dead_code)] - fn from(data: u32) -> Result { - match data { - 0x01 => Ok(SignedMsgType::PreVote), - 0x02 => Ok(SignedMsgType::PreCommit), - 0x20 => Ok(SignedMsgType::Proposal), - _ => Err(DecodeError::new("Invalid vote type")), - } - } -} diff --git a/tendermint/src/amino_types/time.rs b/tendermint/src/amino_types/time.rs deleted file mode 100644 index 16f3edce2..000000000 --- a/tendermint/src/amino_types/time.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! Timestamps - -use crate::{ - error::Error, - time::{ParseTimestamp, Time}, -}; -use chrono::{TimeZone, Utc}; -use prost_amino_derive::Message; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -#[derive(Clone, PartialEq, Message)] -pub struct TimeMsg { - // TODO(ismail): switch to protobuf's well known type as soon as - // https://github.com/tendermint/go-amino/pull/224 was merged - // and tendermint caught up on the latest amino release. - #[prost_amino(int64, tag = "1")] - pub seconds: i64, - #[prost_amino(int32, tag = "2")] - pub nanos: i32, -} - -impl ParseTimestamp for TimeMsg { - fn parse_timestamp(&self) -> Result { - Ok(Utc.timestamp(self.seconds, self.nanos as u32).into()) - } -} - -impl From