cometbft_rpc/endpoint/
consensus_state.rs

1//! `/consensus_state` endpoint JSON-RPC wrapper
2
3use core::{fmt, str::FromStr};
4
5use cometbft::{
6    account,
7    block::{Height, Round},
8    hash, vote, Hash, Time,
9};
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11use subtle_encoding::hex;
12
13use crate::{dialect::Dialect, prelude::*, request::RequestMessage, Error, Method};
14
15// From <https://github.com/cometbft/cometbft/blob/e820e68acd69737cfb63bc9ccca5f5450a42b5cf/types/vote.go#L16>
16const NIL_VOTE_STR: &str = "nil-Vote";
17
18/// Get the current consensus state.
19#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
20pub struct Request;
21
22impl Request {
23    pub fn new() -> Self {
24        Self {}
25    }
26}
27
28impl RequestMessage for Request {
29    fn method(&self) -> Method {
30        Method::ConsensusState
31    }
32}
33
34impl<S: Dialect> crate::Request<S> for Request {
35    type Response = Response;
36}
37
38impl<S: Dialect> crate::SimpleRequest<S> for Request {
39    type Output = Response;
40}
41
42/// The current consensus state (UNSTABLE).
43///
44/// Currently based on <https://github.com/cometbft/cometbft/blob/e820e68acd69737cfb63bc9ccca5f5450a42b5cf/consensus/types/round_state.go#L97>
45#[derive(Clone, Debug, Deserialize, Serialize)]
46pub struct Response {
47    pub round_state: RoundState,
48}
49
50impl crate::Response for Response {}
51
52/// The state of a particular consensus round.
53#[derive(Clone, Debug, Deserialize, Serialize)]
54pub struct RoundState {
55    #[serde(alias = "height/round/step")]
56    pub height_round_step: HeightRoundStep,
57
58    #[serde(with = "cometbft::serializers::time")]
59    pub start_time: Time,
60
61    #[serde(with = "hash::allow_empty")]
62    pub proposal_block_hash: Hash,
63
64    #[serde(with = "hash::allow_empty")]
65    pub locked_block_hash: Hash,
66
67    #[serde(with = "hash::allow_empty")]
68    pub valid_block_hash: Hash,
69
70    pub height_vote_set: Vec<RoundVotes>,
71
72    pub proposer: ValidatorInfo,
73}
74
75/// A compound object indicating a height, round and step for consensus state.
76#[derive(Clone, Debug)]
77pub struct HeightRoundStep {
78    /// Current block height
79    pub height: Height,
80    /// Current consensus round
81    pub round: Round,
82    /// Current consensus step
83    pub step: i8,
84}
85
86impl Serialize for HeightRoundStep {
87    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
88    where
89        S: Serializer,
90    {
91        let hrs = format!(
92            "{}/{}/{}",
93            self.height.value(),
94            self.round.value(),
95            self.step
96        );
97        serializer.serialize_str(&hrs)
98    }
99}
100
101impl<'de> Deserialize<'de> for HeightRoundStep {
102    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
103    where
104        D: Deserializer<'de>,
105    {
106        let s = String::deserialize(deserializer)?;
107        let hrs: Vec<&str> = s.split('/').collect();
108        if hrs.len() != 3 {
109            return Err(serde::de::Error::custom(format!(
110                "expected 3 components to height/round/step field, but got {}",
111                hrs.len()
112            )));
113        }
114        let height = Height::from_str(hrs[0]).map_err(serde::de::Error::custom)?;
115        let round = Round::from_str(hrs[1]).map_err(serde::de::Error::custom)?;
116        let step = i8::from_str(hrs[2]).map_err(serde::de::Error::custom)?;
117        Ok(Self {
118            height,
119            round,
120            step,
121        })
122    }
123}
124
125/// Details of all votes for a particular consensus round.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct RoundVotes {
128    // A CometBFT node currently serializes this particular field as an
129    // integer and not a string (unlike that which is expected from the `Round`
130    // type).
131    pub round: u32,
132    pub prevotes: Vec<RoundVote>,
133    pub prevotes_bit_array: String,
134    pub precommits: Vec<RoundVote>,
135    pub precommits_bit_array: String,
136}
137
138/// Details of a single vote from a particular consensus round.
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub enum RoundVote {
141    Nil,
142    Vote(VoteSummary),
143}
144
145impl Serialize for RoundVote {
146    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
147    where
148        S: Serializer,
149    {
150        match self {
151            RoundVote::Nil => serializer.serialize_str(NIL_VOTE_STR),
152            RoundVote::Vote(summary) => serializer.serialize_str(&summary.to_string()),
153        }
154    }
155}
156
157impl<'de> Deserialize<'de> for RoundVote {
158    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
159    where
160        D: Deserializer<'de>,
161    {
162        let s = String::deserialize(deserializer)?;
163        if s == NIL_VOTE_STR {
164            Ok(Self::Nil)
165        } else {
166            Ok(Self::Vote(
167                VoteSummary::from_str(&s).map_err(serde::de::Error::custom)?,
168            ))
169        }
170    }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct VoteSummary {
175    pub validator_index: i32,
176    pub validator_address_fingerprint: Fingerprint,
177    pub height: Height,
178    pub round: Round,
179    pub vote_type: vote::Type,
180    pub block_id_hash_fingerprint: Fingerprint,
181    pub signature_fingerprint: Fingerprint,
182    pub timestamp: Time,
183}
184
185impl FromStr for VoteSummary {
186    type Err = Error;
187
188    fn from_str(s: &str) -> Result<Self, Self::Err> {
189        let parts: Vec<&str> = s
190            .strip_prefix("Vote{")
191            .ok_or_else(|| {
192                Error::client_internal(
193                    "invalid format for consensus state vote summary string".to_string(),
194                )
195            })?
196            .strip_suffix('}')
197            .ok_or_else(|| {
198                Error::client_internal(
199                    "invalid format for consensus state vote summary string".to_string(),
200                )
201            })?
202            .split(' ')
203            .collect();
204        if parts.len() != 6 {
205            return Err(Error::client_internal(format!(
206                "expected 6 parts to a consensus state vote summary, but got {}",
207                parts.len()
208            )));
209        }
210        let validator: Vec<&str> = parts[0].split(':').collect();
211        if validator.len() != 2 {
212            return Err(Error::client_internal(format!(
213                "failed to parse validator info for consensus state vote summary: {}",
214                parts[0],
215            )));
216        }
217        let height_round_type: Vec<&str> = parts[1].split('/').collect();
218        if height_round_type.len() != 3 {
219            return Err(Error::client_internal(format!(
220                "failed to parse height/round/type for consensus state vote summary: {}",
221                parts[1]
222            )));
223        }
224
225        let validator_index = i32::from_str(validator[0]).map_err(|e| {
226            Error::client_internal(format!(
227                "failed to parse validator index from consensus state vote summary: {} ({})",
228                e, validator[0],
229            ))
230        })?;
231        let validator_address_fingerprint =
232            Fingerprint::from_str(validator[1]).map_err(|e| {
233                Error::client_internal(format!(
234                    "failed to parse validator address fingerprint from consensus state vote summary: {e}"
235                ))
236            })?;
237        let height = Height::from_str(height_round_type[0]).map_err(|e| {
238            Error::client_internal(format!(
239                "failed to parse height from consensus state vote summary: {e}"
240            ))
241        })?;
242        let round = Round::from_str(height_round_type[1]).map_err(|e| {
243            Error::client_internal(format!(
244                "failed to parse round from consensus state vote summary: {e}"
245            ))
246        })?;
247        let vote_type_parts: Vec<&str> = height_round_type[2].split('(').collect();
248        if vote_type_parts.len() != 2 {
249            return Err(Error::client_internal(format!(
250                "invalid structure for vote type in consensus state vote summary: {}",
251                height_round_type[2]
252            )));
253        }
254        let vote_type_str = vote_type_parts[1].trim_end_matches(')');
255        let vote_type = vote::Type::from_str(vote_type_str).map_err(|e| {
256            Error::client_internal(format!(
257                "failed to parse vote type from consensus state vote summary: {e} ({vote_type_str})"
258            ))
259        })?;
260        let block_id_hash_fingerprint = Fingerprint::from_str(parts[2]).map_err(|e| {
261            Error::client_internal(format!(
262                "failed to parse block ID hash fingerprint from consensus state vote summary: {e}"
263            ))
264        })?;
265        let signature_fingerprint = Fingerprint::from_str(parts[3]).map_err(|e| {
266            Error::client_internal(format!(
267                "failed to parse signature fingerprint from consensus state vote summary: {e}"
268            ))
269        })?;
270        let timestamp = Time::parse_from_rfc3339(parts[5]).map_err(|e| {
271            Error::client_internal(format!(
272                "failed to parse timestamp from consensus state vote summary: {e}"
273            ))
274        })?;
275
276        Ok(Self {
277            validator_index,
278            validator_address_fingerprint,
279            height,
280            round,
281            vote_type,
282            block_id_hash_fingerprint,
283            signature_fingerprint,
284            timestamp,
285        })
286    }
287}
288
289impl fmt::Display for VoteSummary {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        write!(
292            f,
293            "Vote{{{}:{} {}/{:02}/{}({}) {} {} @ {}}}",
294            self.validator_index,
295            self.validator_address_fingerprint,
296            self.height,
297            self.round.value(),
298            i32::from(self.vote_type),
299            self.vote_type,
300            self.block_id_hash_fingerprint,
301            self.signature_fingerprint,
302            self.timestamp,
303        )
304    }
305}
306
307#[derive(Debug, Clone, PartialEq, Eq)]
308pub struct Fingerprint(Vec<u8>);
309
310impl FromStr for Fingerprint {
311    type Err = Error;
312
313    fn from_str(s: &str) -> Result<Self, Self::Err> {
314        Ok(Self(hex::decode_upper(s).map_err(|e| {
315            Error::client_internal(format!(
316                "failed to parse fingerprint as an uppercase hexadecimal string: {e}"
317            ))
318        })?))
319    }
320}
321
322impl fmt::Display for Fingerprint {
323    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324        let hex_bytes = hex::encode_upper(&self.0);
325        let hex_string = String::from_utf8(hex_bytes).unwrap();
326        write!(f, "{hex_string}")
327    }
328}
329
330impl AsRef<[u8]> for Fingerprint {
331    fn as_ref(&self) -> &[u8] {
332        &self.0
333    }
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct ValidatorInfo {
338    pub address: account::Id,
339    pub index: i32,
340}
341
342#[cfg(test)]
343mod test {
344    use lazy_static::lazy_static;
345
346    use super::*;
347
348    lazy_static! {
349        // An array of (received, deserialized, serialized) vote summaries
350        static ref TEST_VOTE_SUMMARIES: Vec<(String, VoteSummary, String)> = vec![
351            (
352                "Vote{0:000001E443FD 1262197/00/1(Prevote) 634ADAF1F402 7BB974E1BA40 @ 2019-08-01T11:52:35.513572509Z}".to_owned(),
353                VoteSummary {
354                    validator_index: 0,
355                    validator_address_fingerprint: Fingerprint(vec![0, 0, 1, 228, 67, 253]),
356                    height: Height::from(1262197_u32),
357                    round: Round::from(0_u8),
358                    vote_type: vote::Type::Prevote,
359                    block_id_hash_fingerprint: Fingerprint(vec![99, 74, 218, 241, 244, 2]),
360                    signature_fingerprint: Fingerprint(vec![123, 185, 116, 225, 186, 64]),
361                    timestamp: "2019-08-01T11:52:35.513572509Z".parse().unwrap(),
362                },
363                "Vote{0:000001E443FD 1262197/00/1(Prevote) 634ADAF1F402 7BB974E1BA40 @ 2019-08-01T11:52:35.513572509Z}".to_owned(),
364            ),
365            (
366                // See https://github.com/informalsystems/tendermint-rs/issues/836
367                "Vote{0:2DA21E474F57 384/00/SIGNED_MSG_TYPE_PREVOTE(Prevote) 8FA9FD23F590 2987C33E8F87 @ 2021-03-25T12:12:03.693870115Z}".to_owned(),
368                VoteSummary {
369                    validator_index: 0,
370                    validator_address_fingerprint: Fingerprint(vec![45, 162, 30, 71, 79, 87]),
371                    height: Height::from(384_u32),
372                    round: Round::from(0_u8),
373                    vote_type: vote::Type::Prevote,
374                    block_id_hash_fingerprint: Fingerprint(vec![143, 169, 253, 35, 245, 144]),
375                    signature_fingerprint: Fingerprint(vec![41, 135, 195, 62, 143, 135]),
376                    timestamp: "2021-03-25T12:12:03.693870115Z".parse().unwrap(),
377                },
378                "Vote{0:2DA21E474F57 384/00/1(Prevote) 8FA9FD23F590 2987C33E8F87 @ 2021-03-25T12:12:03.693870115Z}".to_owned(),
379            )
380        ];
381    }
382
383    #[test]
384    fn deserialize_vote_summary() {
385        for (vote_summary_str, expected, _) in TEST_VOTE_SUMMARIES.iter() {
386            let actual = VoteSummary::from_str(vote_summary_str);
387            assert!(actual.is_ok(), "{}", vote_summary_str);
388            let actual = actual.unwrap();
389            assert_eq!(expected.clone(), actual);
390        }
391    }
392
393    #[test]
394    fn serialize_vote_summary() {
395        for (_, vote_summary, expected) in TEST_VOTE_SUMMARIES.iter() {
396            let actual = vote_summary.to_string();
397            assert_eq!(expected.clone(), actual);
398        }
399    }
400}