tendermint/
proposal.rs

1//! Proposals from validators
2
3mod canonical_proposal;
4mod msg_type;
5mod sign_proposal;
6
7use bytes::BufMut;
8pub use msg_type::Type;
9pub use sign_proposal::{SignProposalRequest, SignedProposalResponse};
10use tendermint_proto::v0_37::types::CanonicalProposal as RawCanonicalProposal;
11use tendermint_proto::{Error as ProtobufError, Protobuf};
12
13pub use self::canonical_proposal::CanonicalProposal;
14use crate::{
15    block::{Height, Id as BlockId, Round},
16    chain::Id as ChainId,
17    consensus::State,
18    prelude::*,
19    Signature, Time,
20};
21
22/// Proposal
23#[derive(Clone, PartialEq, Eq, Debug)]
24pub struct Proposal {
25    /// Proposal message type
26    pub msg_type: Type,
27    /// Height
28    pub height: Height,
29    /// Round
30    pub round: Round,
31    /// POL Round
32    pub pol_round: Option<Round>,
33    /// Block ID
34    pub block_id: Option<BlockId>,
35    /// Timestamp
36    pub timestamp: Option<Time>,
37    /// Signature
38    pub signature: Option<Signature>,
39}
40
41tendermint_pb_modules! {
42    use super::Proposal;
43    use crate::{Signature, Error, block::Round};
44    use pb::types::Proposal as RawProposal;
45
46    impl Protobuf<RawProposal> for Proposal {}
47
48    impl TryFrom<RawProposal> for Proposal {
49        type Error = Error;
50
51        fn try_from(value: RawProposal) -> Result<Self, Self::Error> {
52            if value.pol_round < -1 {
53                return Err(Error::negative_pol_round());
54            }
55            let pol_round = match value.pol_round {
56                -1 => None,
57                n => Some(Round::try_from(n)?),
58            };
59            Ok(Proposal {
60                msg_type: value.r#type.try_into()?,
61                height: value.height.try_into()?,
62                round: value.round.try_into()?,
63                pol_round,
64                block_id: value.block_id.map(TryInto::try_into).transpose()?,
65                timestamp: value.timestamp.map(|t| t.try_into()).transpose()?,
66                signature: Signature::new(value.signature)?,
67            })
68        }
69    }
70
71    impl From<Proposal> for RawProposal {
72        fn from(value: Proposal) -> Self {
73            RawProposal {
74                r#type: value.msg_type.into(),
75                height: value.height.into(),
76                round: value.round.into(),
77                pol_round: value.pol_round.map_or(-1, Into::into),
78                block_id: value.block_id.map(Into::into),
79                timestamp: value.timestamp.map(Into::into),
80                signature: value.signature.map(|s| s.into_bytes()).unwrap_or_default(),
81            }
82        }
83    }
84}
85
86impl Proposal {
87    /// Create signable bytes from Proposal.
88    pub fn to_signable_bytes<B>(
89        &self,
90        chain_id: ChainId,
91        sign_bytes: &mut B,
92    ) -> Result<bool, ProtobufError>
93    where
94        B: BufMut,
95    {
96        let canonical = CanonicalProposal::new(self.clone(), chain_id);
97        Protobuf::<RawCanonicalProposal>::encode_length_delimited(canonical, sign_bytes)?;
98        Ok(true)
99    }
100
101    /// Create signable vector from Proposal.
102    pub fn into_signable_vec(self, chain_id: ChainId) -> Vec<u8> {
103        let canonical = CanonicalProposal::new(self, chain_id);
104        Protobuf::<RawCanonicalProposal>::encode_length_delimited_vec(canonical)
105    }
106
107    /// Consensus state from this proposal - This doesn't seem to be used anywhere.
108    #[deprecated(
109        since = "0.17.0",
110        note = "This seems unnecessary, please raise it to the team, if you need it."
111    )]
112    pub fn consensus_state(&self) -> State {
113        State {
114            height: self.height,
115            round: self.round,
116            step: 3,
117            block_id: self.block_id,
118        }
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use core::str::FromStr;
125
126    use time::macros::datetime;
127
128    use crate::{
129        block::{parts::Header, Height, Id as BlockId, Round},
130        chain::Id as ChainId,
131        hash::{Algorithm, Hash},
132        prelude::*,
133        proposal::{SignProposalRequest, Type},
134        test::dummy_signature,
135        Proposal,
136    };
137
138    #[test]
139    fn test_serialization() {
140        let dt = datetime!(2018-02-11 07:09:22.765 UTC);
141        let proposal = Proposal {
142            msg_type: Type::Proposal,
143            height: Height::from(12345_u32),
144            round: Round::from(23456_u16),
145            pol_round: None,
146            block_id: Some(BlockId {
147                hash: Hash::from_hex_upper(
148                    Algorithm::Sha256,
149                    "DEADBEEFDEADBEEFBAFBAFBAFBAFBAFADEADBEEFDEADBEEFBAFBAFBAFBAFBAFA",
150                )
151                .unwrap(),
152                part_set_header: Header::new(
153                    65535,
154                    Hash::from_hex_upper(
155                        Algorithm::Sha256,
156                        "0022446688AACCEE1133557799BBDDFF0022446688AACCEE1133557799BBDDFF",
157                    )
158                    .unwrap(),
159                )
160                .unwrap(),
161            }),
162            timestamp: Some(dt.try_into().unwrap()),
163            signature: Some(dummy_signature()),
164        };
165
166        let mut got = vec![];
167
168        let request = SignProposalRequest {
169            proposal,
170            chain_id: ChainId::from_str("test_chain_id").unwrap(),
171        };
172
173        let _have = request.to_signable_bytes(&mut got);
174
175        // the following vector is generated via:
176        // import (
177        // "encoding/hex"
178        // "fmt"
179        // prototypes "github.com/tendermint/tendermint/proto/tendermint/types"
180        // "github.com/tendermint/tendermint/types"
181        // "strings"
182        // "time"
183        // )
184        //
185        // func proposalSerialize() {
186        // stamp, _ := time.Parse(time.RFC3339Nano, "2018-02-11T07:09:22.765Z")
187        // block_hash, _ :=
188        // hex.DecodeString("DEADBEEFDEADBEEFBAFBAFBAFBAFBAFADEADBEEFDEADBEEFBAFBAFBAFBAFBAFA")
189        // part_hash, _ :=
190        // hex.DecodeString("0022446688AACCEE1133557799BBDDFF0022446688AACCEE1133557799BBDDFF")
191        // proposal := &types.Proposal{
192        // Type:     prototypes.SignedMsgType(prototypes.ProposalType),
193        // Height:   12345,
194        // Round:    23456,
195        // POLRound: -1,
196        // BlockID: types.BlockID{
197        // Hash: block_hash,
198        // PartSetHeader: types.PartSetHeader{
199        // Hash:  part_hash,
200        // Total: 65535,
201        // },
202        // },
203        // Timestamp: stamp,
204        // }
205        // signBytes := types.ProposalSignBytes("test_chain_id", proposal.ToProto())
206        // fmt.Println(strings.Join(strings.Split(fmt.Sprintf("%v", signBytes), " "), ", "))
207        // }
208
209        let want = vec![
210            136, 1, 8, 32, 17, 57, 48, 0, 0, 0, 0, 0, 0, 25, 160, 91, 0, 0, 0, 0, 0, 0, 32, 255,
211            255, 255, 255, 255, 255, 255, 255, 255, 1, 42, 74, 10, 32, 222, 173, 190, 239, 222,
212            173, 190, 239, 186, 251, 175, 186, 251, 175, 186, 250, 222, 173, 190, 239, 222, 173,
213            190, 239, 186, 251, 175, 186, 251, 175, 186, 250, 18, 38, 8, 255, 255, 3, 18, 32, 0,
214            34, 68, 102, 136, 170, 204, 238, 17, 51, 85, 119, 153, 187, 221, 255, 0, 34, 68, 102,
215            136, 170, 204, 238, 17, 51, 85, 119, 153, 187, 221, 255, 50, 12, 8, 162, 216, 255, 211,
216            5, 16, 192, 242, 227, 236, 2, 58, 13, 116, 101, 115, 116, 95, 99, 104, 97, 105, 110,
217            95, 105, 100,
218        ];
219
220        assert_eq!(got, want)
221    }
222
223    #[test]
224    // Test proposal encoding with a malformed block ID which is considered null in Go.
225    fn test_encoding_with_empty_block_id() {
226        let dt = datetime!(2018-02-11 07:09:22.765 UTC);
227        let proposal = Proposal {
228            msg_type: Type::Proposal,
229            height: Height::from(12345_u32),
230            round: Round::from(23456_u16),
231            pol_round: None,
232            block_id: Some(BlockId {
233                hash: Hash::from_hex_upper(Algorithm::Sha256, "").unwrap(),
234                part_set_header: Header::new(
235                    65535,
236                    Hash::from_hex_upper(
237                        Algorithm::Sha256,
238                        "0022446688AACCEE1133557799BBDDFF0022446688AACCEE1133557799BBDDFF",
239                    )
240                    .unwrap(),
241                )
242                .unwrap(),
243            }),
244            timestamp: Some(dt.try_into().unwrap()),
245            signature: Some(dummy_signature()),
246        };
247
248        let mut got = vec![];
249
250        let request = SignProposalRequest {
251            proposal,
252            chain_id: ChainId::from_str("test_chain_id").unwrap(),
253        };
254
255        let _have = request.to_signable_bytes(&mut got);
256
257        // the following vector is generated via:
258        // import (
259        // "encoding/hex"
260        // "fmt"
261        // prototypes "github.com/tendermint/tendermint/proto/tendermint/types"
262        // "github.com/tendermint/tendermint/types"
263        // "strings"
264        // "time"
265        // )
266        //
267        // func proposalSerialize() {
268        // stamp, _ := time.Parse(time.RFC3339Nano, "2018-02-11T07:09:22.765Z")
269        // block_hash, _ := hex.DecodeString("")
270        // part_hash, _ :=
271        // hex.DecodeString("0022446688AACCEE1133557799BBDDFF0022446688AACCEE1133557799BBDDFF")
272        // proposal := &types.Proposal{
273        // Type:     prototypes.SignedMsgType(prototypes.ProposalType),
274        // Height:   12345,
275        // Round:    23456,
276        // POLRound: -1,
277        // BlockID: types.BlockID{
278        // Hash: block_hash,
279        // PartSetHeader: types.PartSetHeader{
280        // Hash:  part_hash,
281        // Total: 65535,
282        // },
283        // },
284        // Timestamp: stamp,
285        // }
286        // signBytes := types.ProposalSignBytes("test_chain_id", proposal.ToProto())
287        // fmt.Println(strings.Join(strings.Split(fmt.Sprintf("%v", signBytes), " "), ", "))
288        // }
289
290        let want = vec![
291            102, 8, 32, 17, 57, 48, 0, 0, 0, 0, 0, 0, 25, 160, 91, 0, 0, 0, 0, 0, 0, 32, 255, 255,
292            255, 255, 255, 255, 255, 255, 255, 1, 42, 40, 18, 38, 8, 255, 255, 3, 18, 32, 0, 34,
293            68, 102, 136, 170, 204, 238, 17, 51, 85, 119, 153, 187, 221, 255, 0, 34, 68, 102, 136,
294            170, 204, 238, 17, 51, 85, 119, 153, 187, 221, 255, 50, 12, 8, 162, 216, 255, 211, 5,
295            16, 192, 242, 227, 236, 2, 58, 13, 116, 101, 115, 116, 95, 99, 104, 97, 105, 110, 95,
296            105, 100,
297        ];
298
299        assert_eq!(got, want)
300    }
301
302    tendermint_pb_modules! {
303        use super::*;
304
305        #[test]
306        fn test_deserialization() {
307            let dt = datetime!(2018-02-11 07:09:22.765 UTC);
308            let proposal = Proposal {
309                msg_type: Type::Proposal,
310                height: Height::from(12345_u32),
311                round: Round::from(23456_u16),
312                timestamp: Some(dt.try_into().unwrap()),
313
314                pol_round: None,
315                block_id: Some(BlockId {
316                    hash: Hash::from_hex_upper(
317                        Algorithm::Sha256,
318                        "DEADBEEFDEADBEEFBAFBAFBAFBAFBAFADEADBEEFDEADBEEFBAFBAFBAFBAFBAFA",
319                    )
320                    .unwrap(),
321                    part_set_header: Header::new(
322                        65535,
323                        Hash::from_hex_upper(
324                            Algorithm::Sha256,
325                            "0022446688AACCEE1133557799BBDDFF0022446688AACCEE1133557799BBDDFF",
326                        )
327                        .unwrap(),
328                    )
329                    .unwrap(),
330                }),
331                signature: Some(dummy_signature()),
332            };
333            let want = SignProposalRequest {
334                proposal,
335                chain_id: ChainId::from_str("test_chain_id").unwrap(),
336            };
337
338            let data = vec![
339                10, 176, 1, 8, 32, 16, 185, 96, 24, 160, 183, 1, 32, 255, 255, 255, 255, 255, 255, 255,
340                255, 255, 1, 42, 74, 10, 32, 222, 173, 190, 239, 222, 173, 190, 239, 186, 251, 175,
341                186, 251, 175, 186, 250, 222, 173, 190, 239, 222, 173, 190, 239, 186, 251, 175, 186,
342                251, 175, 186, 250, 18, 38, 8, 255, 255, 3, 18, 32, 0, 34, 68, 102, 136, 170, 204, 238,
343                17, 51, 85, 119, 153, 187, 221, 255, 0, 34, 68, 102, 136, 170, 204, 238, 17, 51, 85,
344                119, 153, 187, 221, 255, 50, 12, 8, 162, 216, 255, 211, 5, 16, 192, 242, 227, 236, 2,
345                58, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
346                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
347                0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 13, 116, 101, 115, 116, 95, 99, 104, 97, 105, 110, 95,
348                105, 100,
349            ];
350
351            let have = <SignProposalRequest as Protobuf<pb::privval::SignProposalRequest>>::decode_vec(&data).unwrap();
352            assert_eq!(have, want);
353        }
354    }
355}