iq_cometbft/consensus/
state.rs

1//! Tendermint consensus state
2
3pub use core::{cmp::Ordering, fmt};
4
5use serde::{Deserialize, Serialize};
6
7pub use crate::block;
8use crate::prelude::*;
9use crate::serializers;
10
11/// Placeholder string to show when block ID is absent. Syntax from:
12/// <https://cometbft.com/docs/spec/consensus/consensus.html>
13pub const NIL_PLACEHOLDER: &str = "<nil>";
14
15/// Tendermint consensus state
16// Serde serialization for KMS state file read/write.
17// https://github.com/cometbft/cometbft-rs/issues/675
18#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
19pub struct State {
20    /// Current block height
21    pub height: block::Height,
22
23    /// Current consensus round
24    pub round: block::Round,
25
26    /// Current consensus step
27    pub step: i8,
28
29    /// Block ID being proposed (if available)
30    #[serde(with = "serializers::optional")]
31    pub block_id: Option<block::Id>,
32}
33
34impl State {
35    /// Get short prefix of the block ID for debugging purposes (ala git)
36    pub fn block_id_prefix(&self) -> String {
37        self.block_id
38            .as_ref()
39            .map(block::Id::prefix)
40            .unwrap_or_else(|| NIL_PLACEHOLDER.to_owned())
41    }
42}
43
44impl fmt::Display for State {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        write!(f, "{}/{}/{}", self.height, self.round, self.step)
47    }
48}
49
50impl Ord for State {
51    fn cmp(&self, other: &State) -> Ordering {
52        match self.height.cmp(&other.height) {
53            Ordering::Greater => Ordering::Greater,
54            Ordering::Less => Ordering::Less,
55            Ordering::Equal => match self.round.cmp(&other.round) {
56                Ordering::Greater => Ordering::Greater,
57                Ordering::Less => Ordering::Less,
58                Ordering::Equal => self.step.cmp(&other.step),
59            },
60        }
61    }
62}
63
64impl PartialOrd for State {
65    fn partial_cmp(&self, other: &State) -> Option<Ordering> {
66        Some(self.cmp(other))
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use core::str::FromStr;
73
74    use super::State;
75    use crate::{block, Hash};
76
77    #[test]
78    fn state_ord_test() {
79        let new = State {
80            height: block::Height::from(9001_u32),
81            round: block::Round::default(),
82            step: 0,
83            block_id: None,
84        };
85
86        let old = State {
87            height: block::Height::from(1001_u32),
88            round: block::Round::from(1_u16),
89            step: 0,
90            block_id: None,
91        };
92
93        let older = State {
94            height: block::Height::from(1001_u32),
95            round: block::Round::default(),
96            step: 0,
97            block_id: None,
98        };
99
100        let oldest = State {
101            height: block::Height::default(),
102            round: block::Round::default(),
103            step: 0,
104            block_id: None,
105        };
106
107        assert!(old < new);
108        assert!(older < old);
109        assert!(oldest < older);
110        assert!(oldest < new);
111    }
112
113    #[test]
114    fn state_deser_update_null_test() {
115        // Testing that block_id == null is correctly deserialized.
116        let state_json_string = r#"{
117            "height": "5",
118            "round": "1",
119            "step": 6,
120            "block_id": null
121        }"#;
122        let state: State = State {
123            height: block::Height::from(5_u32),
124            round: block::Round::from(1_u16),
125            step: 6,
126            block_id: None,
127        };
128        let state_from_json: State = serde_json::from_str(state_json_string).unwrap();
129        assert_eq!(state_from_json, state);
130    }
131
132    #[test]
133    fn state_deser_update_total_test() {
134        // Testing, if total is correctly deserialized from string.
135        // Note that we use 'parts' to test backwards compatibility.
136        let state_json_string = r#"{
137            "height": "5",
138            "round": "1",
139            "step": 6,
140            "block_id": {
141              "hash": "1234567890123456789012345678901234567890123456789012345678901234",
142              "parts": {
143                  "total": "1",
144                  "hash": "1234567890123456789012345678901234567890123456789012345678901234"
145              }
146            }
147        }"#;
148        let state: State = State {
149            height: block::Height::from(5_u32),
150            round: block::Round::from(1_u16),
151            step: 6,
152            block_id: Some(block::Id {
153                hash: Hash::from_str(
154                    "1234567890123456789012345678901234567890123456789012345678901234",
155                )
156                .unwrap(),
157                part_set_header: block::parts::Header::new(
158                    1,
159                    Hash::from_str(
160                        "1234567890123456789012345678901234567890123456789012345678901234",
161                    )
162                    .unwrap(),
163                )
164                .unwrap(),
165            }),
166        };
167        let state_from_json: State = serde_json::from_str(state_json_string).unwrap();
168        assert_eq!(state_from_json, state);
169    }
170}