tendermint/vote/
canonical_vote.rs

1use serde::{Deserialize, Serialize};
2use tendermint_proto::v0_37::types::CanonicalVote as RawCanonicalVote;
3
4use crate::{block, chain::Id as ChainId, prelude::*, Time};
5
6/// CanonicalVote is used for protobuf encoding a Vote
7#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
8#[serde(try_from = "RawCanonicalVote", into = "RawCanonicalVote")]
9pub struct CanonicalVote {
10    /// Type of vote (prevote or precommit)
11    pub vote_type: super::Type,
12
13    /// Block height
14    pub height: block::Height,
15
16    /// Round
17    pub round: block::Round,
18
19    /// Block ID
20    //#[serde(deserialize_with = "serializers::parse_non_empty_block_id")] - moved to try_from
21    pub block_id: Option<block::Id>,
22
23    /// Timestamp
24    pub timestamp: Option<Time>,
25
26    /// Chain ID
27    pub chain_id: ChainId,
28}
29
30tendermint_pb_modules! {
31    use super::CanonicalVote;
32    use crate::Error;
33    use crate::{block, chain::Id as ChainId, prelude::*};
34    use pb::types::CanonicalVote as RawCanonicalVote;
35
36    impl Protobuf<RawCanonicalVote> for CanonicalVote {}
37
38    impl TryFrom<RawCanonicalVote> for CanonicalVote {
39        type Error = Error;
40
41        fn try_from(value: RawCanonicalVote) -> Result<Self, Self::Error> {
42            if value.timestamp.is_none() {
43                return Err(Error::missing_timestamp());
44            }
45            let _val: i32 = value.round.try_into().map_err(Error::integer_overflow)?;
46
47            // If the Hash is empty in BlockId, the BlockId should be empty.
48            // See: https://github.com/informalsystems/tendermint-rs/issues/663
49            let block_id = value.block_id.filter(|i| !i.hash.is_empty());
50            Ok(CanonicalVote {
51                vote_type: value.r#type.try_into()?,
52                height: value.height.try_into()?,
53                round: (value.round as i32).try_into()?,
54                block_id: block_id.map(|b| b.try_into()).transpose()?,
55                timestamp: value.timestamp.map(|t| t.try_into()).transpose()?,
56                chain_id: ChainId::try_from(value.chain_id)?,
57            })
58        }
59    }
60
61    impl From<CanonicalVote> for RawCanonicalVote {
62        fn from(value: CanonicalVote) -> Self {
63            // If the Hash is empty in BlockId, the BlockId should be empty.
64            // See: https://github.com/informalsystems/tendermint-rs/issues/663
65            let block_id = value.block_id.filter(|i| i != &block::Id::default());
66            RawCanonicalVote {
67                r#type: value.vote_type.into(),
68                height: value.height.into(),
69                round: value.round.value().into(),
70                block_id: block_id.map(Into::into),
71                timestamp: value.timestamp.map(Into::into),
72                chain_id: value.chain_id.to_string(),
73            }
74        }
75    }
76}
77
78impl CanonicalVote {
79    /// Create CanonicalVote from Vote
80    pub fn new(vote: super::Vote, chain_id: ChainId) -> CanonicalVote {
81        CanonicalVote {
82            vote_type: vote.vote_type,
83            height: vote.height,
84            round: vote.round,
85            block_id: vote.block_id,
86            timestamp: vote.timestamp,
87            chain_id,
88        }
89    }
90}
91
92#[cfg(test)]
93mod tests {
94
95    tendermint_pb_modules! {
96        use tendermint_proto::google::protobuf::Timestamp;
97        use pb::types::{
98            CanonicalBlockId as RawCanonicalBlockId,
99            CanonicalPartSetHeader as RawCanonicalPartSetHeader,
100            CanonicalVote as RawCanonicalVote,
101        };
102        use crate::{
103            prelude::*,
104            vote::{canonical_vote::CanonicalVote, Type},
105        };
106
107        #[test]
108        fn canonical_vote_domain_checks() {
109            // RawCanonicalVote with edge cases to test domain knowledge
110            // block_id with empty hash should decode to None
111            // timestamp at EPOCH is still considered valid time
112            let proto_cp = RawCanonicalVote {
113                r#type: 1,
114                height: 2,
115                round: 4,
116                block_id: Some(RawCanonicalBlockId {
117                    hash: vec![],
118                    part_set_header: Some(RawCanonicalPartSetHeader {
119                        total: 1,
120                        hash: vec![1],
121                    }),
122                }),
123                timestamp: Some(Timestamp {
124                    seconds: 0,
125                    nanos: 0,
126                }),
127                chain_id: "testchain".to_string(),
128            };
129            let cp = CanonicalVote::try_from(proto_cp).unwrap();
130            assert_eq!(cp.vote_type, Type::Prevote);
131            assert!(cp.block_id.is_none());
132            assert!(cp.timestamp.is_some());
133
134            // No timestamp is not acceptable
135            // See: https://github.com/informalsystems/tendermint-rs/issues/649
136            let mut proto_cp: RawCanonicalVote = cp.into();
137            proto_cp.timestamp = None;
138            assert!(CanonicalVote::try_from(proto_cp).is_err());
139        }
140    }
141}