From c2bb7656636560d18c4337b114535e82d10f24e9 Mon Sep 17 00:00:00 2001 From: "Tony Arcieri (iqlusion)" Date: Mon, 2 Nov 2020 08:25:05 -0800 Subject: [PATCH] Vendor `amino_types` (#203) Vendors all Amino types from the `tendermint` crate, so we can continue supporting Amino even though it's been removed from the upstream crate in favor of Protobufs. --- src/amino_types.rs | 62 ++++ src/amino_types/block_id.rs | 156 ++++++++++ src/amino_types/ed25519.rs | 187 ++++++++++++ src/amino_types/message.rs | 39 +++ src/amino_types/ping.rs | 14 + src/amino_types/proposal.rs | 322 +++++++++++++++++++ src/amino_types/remote_error.rs | 32 ++ src/amino_types/signature.rs | 58 ++++ src/amino_types/time.rs | 48 +++ src/amino_types/validate.rs | 28 ++ src/amino_types/version.rs | 22 ++ src/amino_types/vote.rs | 527 ++++++++++++++++++++++++++++++++ src/commands/ledger.rs | 13 +- src/lib.rs | 1 + src/rpc.rs | 138 ++------- src/session.rs | 15 +- tests/integration.rs | 2 +- 17 files changed, 1535 insertions(+), 129 deletions(-) create mode 100644 src/amino_types.rs create mode 100644 src/amino_types/block_id.rs create mode 100644 src/amino_types/ed25519.rs create mode 100644 src/amino_types/message.rs create mode 100644 src/amino_types/ping.rs create mode 100644 src/amino_types/proposal.rs create mode 100644 src/amino_types/remote_error.rs create mode 100644 src/amino_types/signature.rs create mode 100644 src/amino_types/time.rs create mode 100644 src/amino_types/validate.rs create mode 100644 src/amino_types/version.rs create mode 100644 src/amino_types/vote.rs diff --git a/src/amino_types.rs b/src/amino_types.rs new file mode 100644 index 00000000..12e97e04 --- /dev/null +++ b/src/amino_types.rs @@ -0,0 +1,62 @@ +//! Legacy message types serialized using the Amino serialization format +//! + +#![allow(missing_docs)] + +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::{ + Proposal, SignProposalRequest, SignedProposalResponse, AMINO_NAME as PROPOSAL_AMINO_NAME, + AMINO_PREFIX as PROPOSAL_PREFIX, + }, + remote_error::RemoteError, + signature::{SignableMsg, SignedMsgType}, + time::TimeMsg, + validate::ConsensusMessage, + version::ConsensusVersion, + vote::{ + SignVoteRequest, SignedVoteResponse, Vote, AMINO_NAME as VOTE_AMINO_NAME, + AMINO_PREFIX as VOTE_PREFIX, + }, +}; + +use crate::rpc; +use sha2::{Digest, Sha256}; + +/// Tendermint requests +pub trait TendermintRequest: SignableMsg { + fn build_response(self, error: Option) -> rpc::Response; +} + +/// 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/src/amino_types/block_id.rs b/src/amino_types/block_id.rs new file mode 100644 index 00000000..710b4c9e --- /dev/null +++ b/src/amino_types/block_id.rs @@ -0,0 +1,156 @@ +use super::validate::{self, ConsensusMessage, Error::*}; +use crate::proto_types; +use prost_amino_derive::Message; +use tendermint::{ + block::{self, parts}, + error::Error, + hash, + hash::{Hash, SHA256_HASH_SIZE}, +}; + +#[derive(Clone, PartialEq, Message)] +pub struct BlockId { + #[prost_amino(bytes, tag = "1")] + pub hash: Vec, + #[prost_amino(message, tag = "2")] + pub parts_header: Option, +} + +impl BlockId { + pub fn new(hash: Vec, parts_header: Option) -> Self { + BlockId { hash, parts_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 + .as_ref() + .and_then(PartsSetHeader::parse_parts_header); + Ok(block::Id::new(hash, parts_header)) + } +} + +impl From<&block::Id> for BlockId { + fn from(bid: &block::Id) -> Self { + let bid_hash = bid.hash.as_bytes(); + BlockId::new( + bid_hash.to_vec(), + bid.parts.as_ref().map(PartsSetHeader::from), + ) + } +} + +impl From for BlockId { + fn from(block_id: proto_types::BlockId) -> BlockId { + BlockId::new( + block_id.hash, + block_id.part_set_header.map(|psh| PartsSetHeader { + total: psh.total as i64, + hash: psh.hash, + }), + ) + } +} + +impl From for proto_types::BlockId { + fn from(block_id: BlockId) -> proto_types::BlockId { + proto_types::BlockId { + hash: block_id.hash, + part_set_header: block_id.parts_header.map(|psh| proto_types::PartSetHeader { + total: psh.total as u32, + hash: psh.hash, + }), + } + } +} + +impl ConsensusMessage for BlockId { + fn validate_basic(&self) -> Result<(), validate::Error> { + // Hash can be empty in case of POLBlockID in Proposal. + if !self.hash.is_empty() && self.hash.len() != SHA256_HASH_SIZE { + return Err(InvalidHashSize); + } + self.parts_header + .as_ref() + .map_or(Ok(()), ConsensusMessage::validate_basic) + } +} + +#[derive(Clone, PartialEq, Message)] +pub struct CanonicalBlockId { + #[prost_amino(bytes, tag = "1")] + pub hash: Vec, + #[prost_amino(message, tag = "2")] + pub parts_header: Option, +} + +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 + .as_ref() + .and_then(CanonicalPartSetHeader::parse_parts_header); + Ok(block::Id::new(hash, parts_header)) + } +} + +#[derive(Clone, PartialEq, Message)] +pub struct PartsSetHeader { + #[prost_amino(int64, tag = "1")] + pub total: i64, + #[prost_amino(bytes, tag = "2")] + pub hash: Vec, +} + +impl PartsSetHeader { + pub fn new(total: i64, hash: Vec) -> Self { + PartsSetHeader { total, hash } + } +} + +impl From<&parts::Header> for PartsSetHeader { + fn from(parts: &parts::Header) -> Self { + PartsSetHeader::new(parts.total as i64, parts.hash.as_bytes().to_vec()) + } +} + +impl PartsSetHeader { + fn parse_parts_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 { + fn validate_basic(&self) -> Result<(), validate::Error> { + if self.total < 0 { + return Err(NegativeTotal); + } + // 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); + } + Ok(()) + } +} + +#[derive(Clone, PartialEq, Message)] +pub struct CanonicalPartSetHeader { + #[prost_amino(bytes, tag = "1")] + pub hash: Vec, + #[prost_amino(int64, tag = "2")] + pub total: i64, +} + +impl CanonicalPartSetHeader { + fn parse_parts_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/src/amino_types/ed25519.rs b/src/amino_types/ed25519.rs new file mode 100644 index 00000000..ffa0aeea --- /dev/null +++ b/src/amino_types/ed25519.rs @@ -0,0 +1,187 @@ +use super::compute_prefix; +use crate::prelude::*; +use once_cell::sync::Lazy; +use prost_amino_derive::Message; +use std::convert::TryFrom; +use tendermint::{ + error, + public_key::{Ed25519, PublicKey}, + Error, +}; + +// Note:On the golang side this is generic in the sense that it could everything that implements +// github.com/tendermint/tendermint/crypto.PubKey +// While this is meant to be used with different key-types, it currently only uses a PubKeyEd25519 +// 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"] +pub struct PubKeyResponse { + #[prost_amino(bytes, tag = "1", amino_name = "tendermint/PubKeyEd25519")] + pub pub_key_ed25519: Vec, +} + +#[derive(Clone, PartialEq, Message)] +#[amino_name = "tendermint/remotesigner/PubKeyRequest"] +pub struct PubKeyRequest {} + +impl TryFrom for PublicKey { + type Error = Error; + + // 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()) + } +} + +impl From for PubKeyResponse { + fn from(public_key: PublicKey) -> PubKeyResponse { + if let PublicKey::Ed25519(ref pk) = public_key { + PubKeyResponse { + pub_key_ed25519: pk.as_bytes().to_vec(), + } + } else { + unimplemented!( + "PubKeyResponse unimplemented for this type: {:?}", + public_key + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ed25519_dalek::PUBLIC_KEY_LENGTH; + use prost_amino::Message; + use std::convert::TryInto; + + #[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 {}; + let mut got = vec![]; + let _have = msg.encode(&mut got); + + assert_eq!(got, want); + + match PubKeyRequest::decode(want.as_ref()) { + Ok(have) => assert_eq!(have, msg), + Err(err) => panic!(err.to_string()), + } + } + + #[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) + // + 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, + ]; + + 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, + ], + }; + let mut got = vec![]; + let _have = msg.encode(&mut got); + + assert_eq!(got, encoded); + + match PubKeyResponse::decode(encoded.as_ref()) { + Ok(have) => assert_eq!(have, msg), + Err(err) => panic!(err), + } + } + + #[test] + fn test_into() { + let raw_pk: [u8; PUBLIC_KEY_LENGTH] = [ + 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, + ]; + 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, + ], + }; + let orig = pk.clone(); + let got: PublicKey = pk.try_into().unwrap(); + + assert_eq!(got, want); + + // and back: + let round_trip_pk: PubKeyResponse = got.into(); + assert_eq!(round_trip_pk, orig); + } + + #[test] + #[should_panic] + fn test_empty_into() { + let empty_msg = PubKeyResponse { + pub_key_ed25519: vec![], + }; + // we expect this to panic: + let _got: PublicKey = empty_msg.try_into().unwrap(); + } +} diff --git a/src/amino_types/message.rs b/src/amino_types/message.rs new file mode 100644 index 00000000..c10e012f --- /dev/null +++ b/src/amino_types/message.rs @@ -0,0 +1,39 @@ +use prost_amino::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. + /// This can be useful when passing those bytes directly to a hasher, or, + /// to reduce boiler plate code when working with the encoded bytes. + /// + /// Warning: Only use this method, if you are in control what will be encoded. + /// If there is an encoding error, this method will panic. + fn bytes_vec(&self) -> Vec + where + Self: Sized, + { + let mut res = Vec::with_capacity(self.encoded_len()); + self.encode(&mut res).unwrap(); + res + } + + /// Encode prost-amino 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. + fn bytes_vec_length_delimited(&self) -> Vec + where + Self: Sized, + { + let len = self.encoded_len(); + let mut res = + Vec::with_capacity(len + encoded_len_varint(len.try_into().expect("length overflow"))); + self.encode_length_delimited(&mut res).unwrap(); + res + } +} +impl AminoMessage for M { + // blanket impl +} diff --git a/src/amino_types/ping.rs b/src/amino_types/ping.rs new file mode 100644 index 00000000..983288b4 --- /dev/null +++ b/src/amino_types/ping.rs @@ -0,0 +1,14 @@ +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/src/amino_types/proposal.rs b/src/amino_types/proposal.rs new file mode 100644 index 00000000..ddd584e3 --- /dev/null +++ b/src/amino_types/proposal.rs @@ -0,0 +1,322 @@ +use super::{ + block_id::{BlockId, CanonicalBlockId, CanonicalPartSetHeader}, + compute_prefix, + remote_error::RemoteError, + signature::{SignableMsg, SignedMsgType}, + time::TimeMsg, + validate::{self, ConsensusMessage, Error::*}, + TendermintRequest, +}; +use crate::rpc; +use bytes::BufMut; +use ed25519_dalek as ed25519; +use once_cell::sync::Lazy; +use prost_amino::{EncodeError, Message}; +use prost_amino_derive::Message; +use std::convert::TryFrom; +use tendermint::{ + block::{self, ParseId}, + chain, consensus, error, +}; + +#[derive(Clone, PartialEq, Message)] +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 block_id: Option, + #[prost_amino(message)] + pub timestamp: Option, + #[prost_amino(bytes)] + pub signature: Vec, +} + +// 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) + } +} + +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"] +pub struct SignProposalRequest { + #[prost_amino(message, tag = "1")] + pub proposal: Option, +} + +#[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)] + pub chain_id: String, +} + +impl chain::ParseId for CanonicalProposal { + fn parse_chain_id(&self) -> Result { + self.chain_id.parse() + } +} + +impl block::ParseHeight for CanonicalProposal { + fn parse_block_height(&self) -> Result { + block::Height::try_from(self.height) + } +} + +#[derive(Clone, PartialEq, Message)] +#[amino_name = "tendermint/remotesigner/SignedProposalResponse"] +pub struct SignedProposalResponse { + #[prost_amino(message, tag = "1")] + pub proposal: Option, + #[prost_amino(message, tag = "2")] + pub err: Option, +} + +impl SignableMsg for SignProposalRequest { + fn sign_bytes(&self, chain_id: chain::Id, sign_bytes: &mut B) -> Result + where + B: BufMut, + { + let mut spr = self.clone(); + if let Some(ref mut pr) = spr.proposal { + pr.signature = vec![]; + } + let proposal = spr.proposal.unwrap(); + let cp = CanonicalProposal { + chain_id: chain_id.to_string(), + msg_type: SignedMsgType::Proposal.to_u32(), + height: proposal.height, + block_id: match proposal.block_id { + Some(bid) => Some(CanonicalBlockId { + hash: bid.hash, + parts_header: match bid.parts_header { + Some(psh) => Some(CanonicalPartSetHeader { + hash: psh.hash, + total: psh.total, + }), + None => None, + }, + }), + None => None, + }, + pol_round: proposal.pol_round, + round: proposal.round, + timestamp: proposal.timestamp, + }; + + cp.encode_length_delimited(sign_bytes)?; + Ok(true) + } + fn set_signature(&mut self, sig: &ed25519::Signature) { + if let Some(ref mut prop) = self.proposal { + prop.signature = sig.as_ref().to_vec(); + } + } + fn validate(&self) -> Result<(), validate::Error> { + match self.proposal { + Some(ref p) => p.validate_basic(), + None => Err(MissingConsensusMessage), + } + } + 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, + step: 3, + block_id: { + match p.block_id { + Some(ref b) => match b.parse_block_id() { + Ok(id) => Some(id), + Err(_) => None, + }, + None => None, + } + }, + }), + None => None, + } + } + + fn height(&self) -> Option { + self.proposal.as_ref().map(|proposal| proposal.height) + } + + fn msg_type(&self) -> Option { + Some(SignedMsgType::Proposal) + } +} + +impl TendermintRequest for SignProposalRequest { + fn build_response(self, error: Option) -> rpc::Response { + let response = if let Some(e) = error { + SignedProposalResponse { + proposal: None, + err: Some(e), + } + } else { + SignedProposalResponse { + proposal: self.proposal, + err: None, + } + }; + + rpc::Response::SignedProposal(response) + } +} + +impl ConsensusMessage for Proposal { + fn validate_basic(&self) -> Result<(), validate::Error> { + if self.msg_type != SignedMsgType::Proposal.to_u32() { + return Err(InvalidMessageType); + } + if self.height < 0 { + return Err(NegativeHeight); + } + if self.round < 0 { + return Err(NegativeRound); + } + if self.pol_round < -1 { + return Err(NegativePOLRound); + } + // TODO validate proposal's block_id + + // signature will be missing as the KMS provides it + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::amino_types::block_id::PartsSetHeader; + 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 { + seconds: dt.timestamp(), + nanos: dt.timestamp_subsec_nanos() as i32, + }; + let proposal = Proposal { + msg_type: SignedMsgType::Proposal.to_u32(), + height: 12345, + round: 23456, + pol_round: -1, + block_id: Some(BlockId { + hash: b"hash".to_vec(), + parts_header: Some(PartsSetHeader { + total: 1_000_000, + hash: b"parts_hash".to_vec(), + }), + }), + timestamp: Some(t), + signature: vec![], + }; + let mut got = vec![]; + + let _have = 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), " "), ", ")) + 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, + ]; + + assert_eq!(got, want) + } + + #[test] + fn test_deserialization() { + let dt = "2018-02-11T07:09:22.765Z".parse::>().unwrap(); + let t = TimeMsg { + seconds: dt.timestamp(), + nanos: dt.timestamp_subsec_nanos() as i32, + }; + let proposal = Proposal { + msg_type: SignedMsgType::Proposal.to_u32(), + height: 12345, + round: 23456, + timestamp: Some(t), + + pol_round: -1, + block_id: Some(BlockId { + hash: b"hash".to_vec(), + parts_header: Some(PartsSetHeader { + total: 1_000_000, + hash: b"parts_hash".to_vec(), + }), + }), + signature: vec![], + }; + let want = SignProposalRequest { + proposal: Some(proposal), + }; + + 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, + ]; + + match SignProposalRequest::decode(data.as_ref()) { + Ok(have) => assert_eq!(have, want), + Err(err) => panic!(err.to_string()), + } + } +} diff --git a/src/amino_types/remote_error.rs b/src/amino_types/remote_error.rs new file mode 100644 index 00000000..1561e9cf --- /dev/null +++ b/src/amino_types/remote_error.rs @@ -0,0 +1,32 @@ +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/src/amino_types/signature.rs b/src/amino_types/signature.rs new file mode 100644 index 00000000..bcd4f4c1 --- /dev/null +++ b/src/amino_types/signature.rs @@ -0,0 +1,58 @@ +use super::validate; +use bytes::BufMut; +use ed25519_dalek as ed25519; +use prost_amino::{DecodeError, EncodeError}; +use tendermint::{chain, consensus}; + +/// Amino messages which are signable within a Tendermint network +pub trait SignableMsg { + /// Sign this message as bytes + fn sign_bytes( + &self, + chain_id: chain::Id, + sign_bytes: &mut B, + ) -> Result; + + /// Set the Ed25519 signature on the underlying message + fn set_signature(&mut self, sig: &ed25519::Signature); + fn validate(&self) -> Result<(), validate::Error>; + fn consensus_state(&self) -> Option; + 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/src/amino_types/time.rs b/src/amino_types/time.rs new file mode 100644 index 00000000..cbb957aa --- /dev/null +++ b/src/amino_types/time.rs @@ -0,0 +1,48 @@ +//! Timestamps + +use chrono::{TimeZone, Utc}; +use prost_amino_derive::Message; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tendermint::{ + error::Error, + time::{ParseTimestamp, Time}, +}; + +#[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