tendermint/proposal/
canonical_proposal.rs

1//! CanonicalProposal
2
3use super::Type;
4use crate::{
5    block::{Height, Id as BlockId, Round},
6    chain::Id as ChainId,
7    prelude::*,
8    Time,
9};
10
11/// CanonicalProposal for signing
12#[derive(Clone, PartialEq, Eq)]
13pub struct CanonicalProposal {
14    /// type alias for byte
15    pub msg_type: Type,
16    /// canonicalization requires fixed size encoding here
17    pub height: Height,
18    /// canonicalization requires fixed size encoding here
19    pub round: Round,
20    /// POL round
21    pub pol_round: Option<Round>,
22    /// Block ID
23    pub block_id: Option<BlockId>,
24    /// Timestamp
25    pub timestamp: Option<Time>,
26    /// Chain ID
27    pub chain_id: ChainId,
28}
29
30tendermint_pb_modules! {
31    use crate::{
32        block::{Id as BlockId, Round},
33        chain::Id as ChainId,
34        error::Error,
35        prelude::*,
36    };
37    use super::CanonicalProposal;
38    use pb::types::CanonicalProposal as RawCanonicalProposal;
39
40    impl Protobuf<RawCanonicalProposal> for CanonicalProposal {}
41
42    impl TryFrom<RawCanonicalProposal> for CanonicalProposal {
43        type Error = Error;
44
45        fn try_from(value: RawCanonicalProposal) -> Result<Self, Self::Error> {
46            if value.pol_round < -1 {
47                return Err(Error::negative_pol_round());
48            }
49            let round = Round::try_from(i32::try_from(value.round).map_err(Error::integer_overflow)?)?;
50            let pol_round = match value.pol_round {
51                -1 => None,
52                n => Some(Round::try_from(
53                    i32::try_from(n).map_err(Error::integer_overflow)?,
54                )?),
55            };
56            // If the Hash is empty in BlockId, the BlockId should be empty.
57            // See: https://github.com/informalsystems/tendermint-rs/issues/663
58            let block_id = value.block_id.filter(|i| !i.hash.is_empty());
59            Ok(CanonicalProposal {
60                msg_type: value.r#type.try_into()?,
61                height: value.height.try_into()?,
62                round,
63                pol_round,
64                block_id: block_id.map(TryInto::try_into).transpose()?,
65                timestamp: value.timestamp.map(|t| t.try_into()).transpose()?,
66                chain_id: ChainId::try_from(value.chain_id).unwrap(),
67            })
68        }
69    }
70
71    impl From<CanonicalProposal> for RawCanonicalProposal {
72        fn from(value: CanonicalProposal) -> Self {
73            // If the Hash is empty in BlockId, the BlockId should be empty.
74            // See: https://github.com/informalsystems/tendermint-rs/issues/663
75            let block_id = value.block_id.filter(|i| i != &BlockId::default());
76            RawCanonicalProposal {
77                r#type: value.msg_type.into(),
78                height: value.height.into(),
79                round: i32::from(value.round) as i64,
80                pol_round: match value.pol_round {
81                    None => -1,
82                    Some(p) => i32::from(p) as i64,
83                },
84                block_id: block_id.map(Into::into),
85                timestamp: value.timestamp.map(Into::into),
86                chain_id: value.chain_id.as_str().to_string(),
87            }
88        }
89    }
90}
91
92impl CanonicalProposal {
93    /// Create CanonicalProposal from Proposal
94    pub fn new(proposal: super::Proposal, chain_id: ChainId) -> CanonicalProposal {
95        CanonicalProposal {
96            msg_type: proposal.msg_type,
97            height: proposal.height,
98            round: proposal.round,
99            pol_round: proposal.pol_round,
100            block_id: proposal.block_id,
101            timestamp: proposal.timestamp,
102            chain_id,
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    tendermint_pb_modules! {
110        use pb::types::{
111            CanonicalBlockId as RawCanonicalBlockId,
112            CanonicalPartSetHeader as RawCanonicalPartSetHeader,
113            CanonicalProposal as RawCanonicalProposal,
114        };
115
116        use crate::{
117            prelude::*,
118            proposal::{canonical_proposal::CanonicalProposal, Type},
119        };
120
121        #[test]
122        fn canonical_proposal_domain_checks() {
123            // RawCanonicalProposal with edge cases to test domain knowledge
124            // pol_round = -1 should decode to None
125            // block_id with empty hash should decode to None
126            let proto_cp = RawCanonicalProposal {
127                r#type: 32,
128                height: 2,
129                round: 4,
130                pol_round: -1,
131                block_id: Some(RawCanonicalBlockId {
132                    hash: vec![],
133                    part_set_header: Some(RawCanonicalPartSetHeader {
134                        total: 1,
135                        hash: vec![1],
136                    }),
137                }),
138                timestamp: None,
139                chain_id: "testchain".to_string(),
140            };
141            let cp = CanonicalProposal::try_from(proto_cp).unwrap();
142            assert_eq!(cp.msg_type, Type::Proposal);
143            assert!(cp.pol_round.is_none());
144            assert!(cp.block_id.is_none());
145        }
146    }
147}