iq_cometbft/
proposal.rs

1//! Proposals from validators
2
3mod canonical_proposal;
4mod msg_type;
5mod sign_proposal;
6
7use bytes::BufMut;
8use cometbft_proto::types::v1::CanonicalProposal as RawCanonicalProposal;
9use cometbft_proto::{Error as ProtobufError, Protobuf};
10pub use msg_type::Type;
11pub use sign_proposal::{SignProposalRequest, SignedProposalResponse};
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
41// =============================================================================
42// Protobuf conversions
43// =============================================================================
44
45cometbft_old_pb_modules! {
46    use super::Proposal;
47    use crate::{Signature, Error, block::Round};
48    use pb::types::Proposal as RawProposal;
49
50    impl Protobuf<RawProposal> for Proposal {}
51
52    impl TryFrom<RawProposal> for Proposal {
53        type Error = Error;
54
55        fn try_from(value: RawProposal) -> Result<Self, Self::Error> {
56            if value.pol_round < -1 {
57                return Err(Error::negative_pol_round());
58            }
59            let pol_round = match value.pol_round {
60                -1 => None,
61                n => Some(Round::try_from(n)?),
62            };
63            Ok(Proposal {
64                msg_type: value.r#type.try_into()?,
65                height: value.height.try_into()?,
66                round: value.round.try_into()?,
67                pol_round,
68                block_id: value.block_id.map(TryInto::try_into).transpose()?,
69                timestamp: value.timestamp.map(|t| t.try_into()).transpose()?,
70                signature: Signature::new(value.signature)?,
71            })
72        }
73    }
74
75    impl From<Proposal> for RawProposal {
76        fn from(value: Proposal) -> Self {
77            RawProposal {
78                r#type: value.msg_type.into(),
79                height: value.height.into(),
80                round: value.round.into(),
81                pol_round: value.pol_round.map_or(-1, Into::into),
82                block_id: value.block_id.map(Into::into),
83                timestamp: value.timestamp.map(Into::into),
84                signature: value.signature.map(|s| s.into_bytes()).unwrap_or_default(),
85            }
86        }
87    }
88}
89
90mod v1 {
91    use super::Proposal;
92    use crate::{block::Round, Error, Signature};
93    use cometbft_proto::types::v1::Proposal as RawProposal;
94
95    impl TryFrom<RawProposal> for Proposal {
96        type Error = Error;
97
98        fn try_from(value: RawProposal) -> Result<Self, Self::Error> {
99            if value.pol_round < -1 {
100                return Err(Error::negative_pol_round());
101            }
102            let pol_round = match value.pol_round {
103                -1 => None,
104                n => Some(Round::try_from(n)?),
105            };
106            Ok(Proposal {
107                msg_type: value.r#type.try_into()?,
108                height: value.height.try_into()?,
109                round: value.round.try_into()?,
110                pol_round,
111                block_id: value.block_id.map(TryInto::try_into).transpose()?,
112                timestamp: value.timestamp.map(|t| t.try_into()).transpose()?,
113                signature: Signature::new(value.signature)?,
114            })
115        }
116    }
117
118    impl From<Proposal> for RawProposal {
119        fn from(value: Proposal) -> Self {
120            RawProposal {
121                r#type: value.msg_type.into(),
122                height: value.height.into(),
123                round: value.round.into(),
124                pol_round: value.pol_round.map_or(-1, Into::into),
125                block_id: value.block_id.map(Into::into),
126                timestamp: value.timestamp.map(Into::into),
127                signature: value.signature.map(|s| s.into_bytes()).unwrap_or_default(),
128            }
129        }
130    }
131}
132
133mod v1beta1 {
134    use super::Proposal;
135    use crate::{block::Round, Error, Signature};
136    use cometbft_proto::types::v1beta1::Proposal as RawProposal;
137
138    impl TryFrom<RawProposal> for Proposal {
139        type Error = Error;
140
141        fn try_from(value: RawProposal) -> Result<Self, Self::Error> {
142            if value.pol_round < -1 {
143                return Err(Error::negative_pol_round());
144            }
145            let pol_round = match value.pol_round {
146                -1 => None,
147                n => Some(Round::try_from(n)?),
148            };
149            Ok(Proposal {
150                msg_type: value.r#type.try_into()?,
151                height: value.height.try_into()?,
152                round: value.round.try_into()?,
153                pol_round,
154                block_id: value.block_id.map(TryInto::try_into).transpose()?,
155                timestamp: value.timestamp.map(|t| t.try_into()).transpose()?,
156                signature: Signature::new(value.signature)?,
157            })
158        }
159    }
160
161    impl From<Proposal> for RawProposal {
162        fn from(value: Proposal) -> Self {
163            RawProposal {
164                r#type: value.msg_type.into(),
165                height: value.height.into(),
166                round: value.round.into(),
167                pol_round: value.pol_round.map_or(-1, Into::into),
168                block_id: value.block_id.map(Into::into),
169                timestamp: value.timestamp.map(Into::into),
170                signature: value.signature.map(|s| s.into_bytes()).unwrap_or_default(),
171            }
172        }
173    }
174}
175
176impl Proposal {
177    /// Create signable bytes from Proposal.
178    pub fn to_signable_bytes<B>(
179        &self,
180        chain_id: ChainId,
181        sign_bytes: &mut B,
182    ) -> Result<bool, ProtobufError>
183    where
184        B: BufMut,
185    {
186        let canonical = CanonicalProposal::new(self.clone(), chain_id);
187        Protobuf::<RawCanonicalProposal>::encode_length_delimited(canonical, sign_bytes)?;
188        Ok(true)
189    }
190
191    /// Create signable vector from Proposal.
192    pub fn into_signable_vec(self, chain_id: ChainId) -> Vec<u8> {
193        let canonical = CanonicalProposal::new(self, chain_id);
194        Protobuf::<RawCanonicalProposal>::encode_length_delimited_vec(canonical)
195    }
196
197    /// Consensus state from this proposal - This doesn't seem to be used anywhere.
198    #[deprecated(
199        since = "0.17.0",
200        note = "This seems unnecessary, please raise it to the team, if you need it."
201    )]
202    pub fn consensus_state(&self) -> State {
203        State {
204            height: self.height,
205            round: self.round,
206            step: 3,
207            block_id: self.block_id,
208        }
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use core::str::FromStr;
215
216    use time::macros::datetime;
217
218    use crate::{
219        block::{parts::Header, Height, Id as BlockId, Round},
220        chain::Id as ChainId,
221        hash::{Algorithm, Hash},
222        prelude::*,
223        proposal::{SignProposalRequest, Type},
224        test::dummy_signature,
225        Proposal,
226    };
227
228    #[test]
229    fn test_serialization() {
230        let dt = datetime!(2018-02-11 07:09:22.765 UTC);
231        let proposal = Proposal {
232            msg_type: Type::Proposal,
233            height: Height::from(12345_u32),
234            round: Round::from(23456_u16),
235            pol_round: None,
236            block_id: Some(BlockId {
237                hash: Hash::from_hex_upper(
238                    Algorithm::Sha256,
239                    "DEADBEEFDEADBEEFBAFBAFBAFBAFBAFADEADBEEFDEADBEEFBAFBAFBAFBAFBAFA",
240                )
241                .unwrap(),
242                part_set_header: Header::new(
243                    65535,
244                    Hash::from_hex_upper(
245                        Algorithm::Sha256,
246                        "0022446688AACCEE1133557799BBDDFF0022446688AACCEE1133557799BBDDFF",
247                    )
248                    .unwrap(),
249                )
250                .unwrap(),
251            }),
252            timestamp: Some(dt.try_into().unwrap()),
253            signature: Some(dummy_signature()),
254        };
255
256        let mut got = vec![];
257
258        let request = SignProposalRequest {
259            proposal,
260            chain_id: ChainId::from_str("test_chain_id").unwrap(),
261        };
262
263        let _have = request.to_signable_bytes(&mut got);
264
265        // the following vector is generated via:
266        // import (
267        // "encoding/hex"
268        // "fmt"
269        // prototypes "github.com/cometbft/cometbft/api/cometbft/types/v1"
270        // "github.com/cometbft/cometbft/types"
271        // "strings"
272        // "time"
273        // )
274        //
275        // func proposalSerialize() {
276        // stamp, _ := time.Parse(time.RFC3339Nano, "2018-02-11T07:09:22.765Z")
277        // block_hash, _ :=
278        // hex.DecodeString("DEADBEEFDEADBEEFBAFBAFBAFBAFBAFADEADBEEFDEADBEEFBAFBAFBAFBAFBAFA")
279        // part_hash, _ :=
280        // hex.DecodeString("0022446688AACCEE1133557799BBDDFF0022446688AACCEE1133557799BBDDFF")
281        // proposal := &types.Proposal{
282        // Type:     prototypes.SignedMsgType(prototypes.ProposalType),
283        // Height:   12345,
284        // Round:    23456,
285        // POLRound: -1,
286        // BlockID: types.BlockID{
287        // Hash: block_hash,
288        // PartSetHeader: types.PartSetHeader{
289        // Hash:  part_hash,
290        // Total: 65535,
291        // },
292        // },
293        // Timestamp: stamp,
294        // }
295        // signBytes := types.ProposalSignBytes("test_chain_id", proposal.ToProto())
296        // fmt.Println(strings.Join(strings.Split(fmt.Sprintf("%v", signBytes), " "), ", "))
297        // }
298
299        let want = vec![
300            136, 1, 8, 32, 17, 57, 48, 0, 0, 0, 0, 0, 0, 25, 160, 91, 0, 0, 0, 0, 0, 0, 32, 255,
301            255, 255, 255, 255, 255, 255, 255, 255, 1, 42, 74, 10, 32, 222, 173, 190, 239, 222,
302            173, 190, 239, 186, 251, 175, 186, 251, 175, 186, 250, 222, 173, 190, 239, 222, 173,
303            190, 239, 186, 251, 175, 186, 251, 175, 186, 250, 18, 38, 8, 255, 255, 3, 18, 32, 0,
304            34, 68, 102, 136, 170, 204, 238, 17, 51, 85, 119, 153, 187, 221, 255, 0, 34, 68, 102,
305            136, 170, 204, 238, 17, 51, 85, 119, 153, 187, 221, 255, 50, 12, 8, 162, 216, 255, 211,
306            5, 16, 192, 242, 227, 236, 2, 58, 13, 116, 101, 115, 116, 95, 99, 104, 97, 105, 110,
307            95, 105, 100,
308        ];
309
310        assert_eq!(got, want)
311    }
312
313    #[test]
314    // Test proposal encoding with a malformed block ID which is considered null in Go.
315    fn test_encoding_with_empty_block_id() {
316        let dt = datetime!(2018-02-11 07:09:22.765 UTC);
317        let proposal = Proposal {
318            msg_type: Type::Proposal,
319            height: Height::from(12345_u32),
320            round: Round::from(23456_u16),
321            pol_round: None,
322            block_id: Some(BlockId {
323                hash: Hash::from_hex_upper(Algorithm::Sha256, "").unwrap(),
324                part_set_header: Header::new(
325                    65535,
326                    Hash::from_hex_upper(
327                        Algorithm::Sha256,
328                        "0022446688AACCEE1133557799BBDDFF0022446688AACCEE1133557799BBDDFF",
329                    )
330                    .unwrap(),
331                )
332                .unwrap(),
333            }),
334            timestamp: Some(dt.try_into().unwrap()),
335            signature: Some(dummy_signature()),
336        };
337
338        let mut got = vec![];
339
340        let request = SignProposalRequest {
341            proposal,
342            chain_id: ChainId::from_str("test_chain_id").unwrap(),
343        };
344
345        let _have = request.to_signable_bytes(&mut got);
346
347        // the following vector is generated via:
348        // import (
349        // "encoding/hex"
350        // "fmt"
351        // prototypes "github.com/cometbft/cometbft/api/cometbft/types/v1"
352        // "github.com/cometbft/cometbft/types"
353        // "strings"
354        // "time"
355        // )
356        //
357        // func proposalSerialize() {
358        // stamp, _ := time.Parse(time.RFC3339Nano, "2018-02-11T07:09:22.765Z")
359        // block_hash, _ := hex.DecodeString("")
360        // part_hash, _ :=
361        // hex.DecodeString("0022446688AACCEE1133557799BBDDFF0022446688AACCEE1133557799BBDDFF")
362        // proposal := &types.Proposal{
363        // Type:     prototypes.SignedMsgType(prototypes.ProposalType),
364        // Height:   12345,
365        // Round:    23456,
366        // POLRound: -1,
367        // BlockID: types.BlockID{
368        // Hash: block_hash,
369        // PartSetHeader: types.PartSetHeader{
370        // Hash:  part_hash,
371        // Total: 65535,
372        // },
373        // },
374        // Timestamp: stamp,
375        // }
376        // signBytes := types.ProposalSignBytes("test_chain_id", proposal.ToProto())
377        // fmt.Println(strings.Join(strings.Split(fmt.Sprintf("%v", signBytes), " "), ", "))
378        // }
379
380        let want = vec![
381            102, 8, 32, 17, 57, 48, 0, 0, 0, 0, 0, 0, 25, 160, 91, 0, 0, 0, 0, 0, 0, 32, 255, 255,
382            255, 255, 255, 255, 255, 255, 255, 1, 42, 40, 18, 38, 8, 255, 255, 3, 18, 32, 0, 34,
383            68, 102, 136, 170, 204, 238, 17, 51, 85, 119, 153, 187, 221, 255, 0, 34, 68, 102, 136,
384            170, 204, 238, 17, 51, 85, 119, 153, 187, 221, 255, 50, 12, 8, 162, 216, 255, 211, 5,
385            16, 192, 242, 227, 236, 2, 58, 13, 116, 101, 115, 116, 95, 99, 104, 97, 105, 110, 95,
386            105, 100,
387        ];
388
389        assert_eq!(got, want)
390    }
391
392    cometbft_old_pb_modules! {
393        use super::*;
394
395        #[test]
396        fn test_deserialization() {
397            let dt = datetime!(2018-02-11 07:09:22.765 UTC);
398            let proposal = Proposal {
399                msg_type: Type::Proposal,
400                height: Height::from(12345_u32),
401                round: Round::from(23456_u16),
402                timestamp: Some(dt.try_into().unwrap()),
403
404                pol_round: None,
405                block_id: Some(BlockId {
406                    hash: Hash::from_hex_upper(
407                        Algorithm::Sha256,
408                        "DEADBEEFDEADBEEFBAFBAFBAFBAFBAFADEADBEEFDEADBEEFBAFBAFBAFBAFBAFA",
409                    )
410                    .unwrap(),
411                    part_set_header: Header::new(
412                        65535,
413                        Hash::from_hex_upper(
414                            Algorithm::Sha256,
415                            "0022446688AACCEE1133557799BBDDFF0022446688AACCEE1133557799BBDDFF",
416                        )
417                        .unwrap(),
418                    )
419                    .unwrap(),
420                }),
421                signature: Some(dummy_signature()),
422            };
423            let want = SignProposalRequest {
424                proposal,
425                chain_id: ChainId::from_str("test_chain_id").unwrap(),
426            };
427
428            let data = vec![
429                10, 176, 1, 8, 32, 16, 185, 96, 24, 160, 183, 1, 32, 255, 255, 255, 255, 255, 255, 255,
430                255, 255, 1, 42, 74, 10, 32, 222, 173, 190, 239, 222, 173, 190, 239, 186, 251, 175,
431                186, 251, 175, 186, 250, 222, 173, 190, 239, 222, 173, 190, 239, 186, 251, 175, 186,
432                251, 175, 186, 250, 18, 38, 8, 255, 255, 3, 18, 32, 0, 34, 68, 102, 136, 170, 204, 238,
433                17, 51, 85, 119, 153, 187, 221, 255, 0, 34, 68, 102, 136, 170, 204, 238, 17, 51, 85,
434                119, 153, 187, 221, 255, 50, 12, 8, 162, 216, 255, 211, 5, 16, 192, 242, 227, 236, 2,
435                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,
436                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,
437                0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 13, 116, 101, 115, 116, 95, 99, 104, 97, 105, 110, 95,
438                105, 100,
439            ];
440
441            let have = <SignProposalRequest as Protobuf<pb::privval::SignProposalRequest>>::decode_vec(&data).unwrap();
442            assert_eq!(have, want);
443        }
444    }
445
446    mod v1 {
447        use super::*;
448        use cometbft_proto::privval::v1 as pb;
449        use cometbft_proto::Protobuf;
450
451        #[test]
452        fn test_deserialization() {
453            let dt = datetime!(2018-02-11 07:09:22.765 UTC);
454            let proposal = Proposal {
455                msg_type: Type::Proposal,
456                height: Height::from(12345_u32),
457                round: Round::from(23456_u16),
458                timestamp: Some(dt.try_into().unwrap()),
459
460                pol_round: None,
461                block_id: Some(BlockId {
462                    hash: Hash::from_hex_upper(
463                        Algorithm::Sha256,
464                        "DEADBEEFDEADBEEFBAFBAFBAFBAFBAFADEADBEEFDEADBEEFBAFBAFBAFBAFBAFA",
465                    )
466                    .unwrap(),
467                    part_set_header: Header::new(
468                        65535,
469                        Hash::from_hex_upper(
470                            Algorithm::Sha256,
471                            "0022446688AACCEE1133557799BBDDFF0022446688AACCEE1133557799BBDDFF",
472                        )
473                        .unwrap(),
474                    )
475                    .unwrap(),
476                }),
477                signature: Some(dummy_signature()),
478            };
479            let want = SignProposalRequest {
480                proposal,
481                chain_id: ChainId::from_str("test_chain_id").unwrap(),
482            };
483
484            let data = vec![
485                10, 176, 1, 8, 32, 16, 185, 96, 24, 160, 183, 1, 32, 255, 255, 255, 255, 255, 255,
486                255, 255, 255, 1, 42, 74, 10, 32, 222, 173, 190, 239, 222, 173, 190, 239, 186, 251,
487                175, 186, 251, 175, 186, 250, 222, 173, 190, 239, 222, 173, 190, 239, 186, 251,
488                175, 186, 251, 175, 186, 250, 18, 38, 8, 255, 255, 3, 18, 32, 0, 34, 68, 102, 136,
489                170, 204, 238, 17, 51, 85, 119, 153, 187, 221, 255, 0, 34, 68, 102, 136, 170, 204,
490                238, 17, 51, 85, 119, 153, 187, 221, 255, 50, 12, 8, 162, 216, 255, 211, 5, 16,
491                192, 242, 227, 236, 2, 58, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
492                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,
493                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 13, 116, 101, 115,
494                116, 95, 99, 104, 97, 105, 110, 95, 105, 100,
495            ];
496
497            let have =
498                <SignProposalRequest as Protobuf<pb::SignProposalRequest>>::decode_vec(&data)
499                    .unwrap();
500            assert_eq!(have, want);
501        }
502    }
503
504    mod v1beta1 {
505        use super::*;
506        use cometbft_proto::privval::v1beta1 as pb;
507        use cometbft_proto::Protobuf;
508
509        #[test]
510        fn test_deserialization() {
511            let dt = datetime!(2018-02-11 07:09:22.765 UTC);
512            let proposal = Proposal {
513                msg_type: Type::Proposal,
514                height: Height::from(12345_u32),
515                round: Round::from(23456_u16),
516                timestamp: Some(dt.try_into().unwrap()),
517
518                pol_round: None,
519                block_id: Some(BlockId {
520                    hash: Hash::from_hex_upper(
521                        Algorithm::Sha256,
522                        "DEADBEEFDEADBEEFBAFBAFBAFBAFBAFADEADBEEFDEADBEEFBAFBAFBAFBAFBAFA",
523                    )
524                    .unwrap(),
525                    part_set_header: Header::new(
526                        65535,
527                        Hash::from_hex_upper(
528                            Algorithm::Sha256,
529                            "0022446688AACCEE1133557799BBDDFF0022446688AACCEE1133557799BBDDFF",
530                        )
531                        .unwrap(),
532                    )
533                    .unwrap(),
534                }),
535                signature: Some(dummy_signature()),
536            };
537            let want = SignProposalRequest {
538                proposal,
539                chain_id: ChainId::from_str("test_chain_id").unwrap(),
540            };
541
542            let data = vec![
543                10, 176, 1, 8, 32, 16, 185, 96, 24, 160, 183, 1, 32, 255, 255, 255, 255, 255, 255,
544                255, 255, 255, 1, 42, 74, 10, 32, 222, 173, 190, 239, 222, 173, 190, 239, 186, 251,
545                175, 186, 251, 175, 186, 250, 222, 173, 190, 239, 222, 173, 190, 239, 186, 251,
546                175, 186, 251, 175, 186, 250, 18, 38, 8, 255, 255, 3, 18, 32, 0, 34, 68, 102, 136,
547                170, 204, 238, 17, 51, 85, 119, 153, 187, 221, 255, 0, 34, 68, 102, 136, 170, 204,
548                238, 17, 51, 85, 119, 153, 187, 221, 255, 50, 12, 8, 162, 216, 255, 211, 5, 16,
549                192, 242, 227, 236, 2, 58, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
550                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,
551                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 13, 116, 101, 115,
552                116, 95, 99, 104, 97, 105, 110, 95, 105, 100,
553            ];
554
555            let have =
556                <SignProposalRequest as Protobuf<pb::SignProposalRequest>>::decode_vec(&data)
557                    .unwrap();
558            assert_eq!(have, want);
559        }
560    }
561}