celestia_tendermint/
proposal.rs

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