iq_cometbft/block/
id.rs

1use core::{
2    fmt::{self, Display},
3    str::{self, FromStr},
4};
5
6use cometbft_proto::types::v1::BlockId as RawBlockId;
7use serde::{Deserialize, Serialize};
8
9use crate::{
10    block::parts::Header as PartSetHeader,
11    error::Error,
12    hash::{Algorithm, Hash},
13    prelude::*,
14};
15
16/// Length of a block ID prefix displayed for debugging purposes
17pub const PREFIX_LENGTH: usize = 10;
18
19/// Block identifiers which contain two distinct Merkle roots of the block,
20/// as well as the number of parts in the block.
21///
22/// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#blockid>
23///
24/// Default implementation is an empty Id as defined by the Go implementation in
25/// <https://github.com/cometbft/cometbft/blob/1635d1339c73ae6a82e062cd2dc7191b029efa14/types/block.go#L1204>.
26///
27/// If the Hash is empty in BlockId, the BlockId should be empty (encoded to None).
28/// This is implemented outside of this struct. Use the Default trait to check for an empty BlockId.
29/// See: <https://github.com/cometbft/cometbft-rs/issues/663>
30#[derive(
31    Serialize, Deserialize, Copy, Clone, Debug, Default, Hash, Eq, PartialEq, PartialOrd, Ord,
32)]
33#[serde(try_from = "RawBlockId", into = "RawBlockId")]
34pub struct Id {
35    /// The block's main hash is the Merkle root of all the fields in the
36    /// block header.
37    pub hash: Hash,
38
39    /// Parts header (if available) is used for secure gossipping of the block
40    /// during consensus. It is the Merkle root of the complete serialized block
41    /// cut into parts.
42    ///
43    /// PartSet is used to split a byteslice of data into parts (pieces) for
44    /// transmission. By splitting data into smaller parts and computing a
45    /// Merkle root hash on the list, you can verify that a part is
46    /// legitimately part of the complete data, and the part can be forwarded
47    /// to other peers before all the parts are known. In short, it's a fast
48    /// way to propagate a large file over a gossip network.
49    ///
50    /// <https://github.com/cometbft/cometbft/wiki/Block-Structure#partset>
51    ///
52    /// PartSetHeader in protobuf is defined as never nil using the gogoproto
53    /// annotations. This does not translate to Rust, but we can indicate this
54    /// in the domain type.
55    pub part_set_header: PartSetHeader,
56}
57
58cometbft_old_pb_modules! {
59    use pb::{
60        types::{
61            BlockId as RawBlockId, CanonicalBlockId as RawCanonicalBlockId,
62            PartSetHeader as RawPartSetHeader,
63        }
64    };
65    use super::Id;
66    use crate::{prelude::*, Error};
67
68    impl Protobuf<RawBlockId> for Id {}
69
70    impl TryFrom<RawBlockId> for Id {
71        type Error = Error;
72
73        fn try_from(value: RawBlockId) -> Result<Self, Self::Error> {
74            if value.part_set_header.is_none() {
75                return Err(Error::invalid_part_set_header(
76                    "part_set_header is None".to_string(),
77                ));
78            }
79            Ok(Self {
80                hash: value.hash.try_into()?,
81                part_set_header: value.part_set_header.unwrap().try_into()?,
82            })
83        }
84    }
85
86    impl From<Id> for RawBlockId {
87        fn from(value: Id) -> Self {
88            // https://github.com/tendermint/tendermint/blob/1635d1339c73ae6a82e062cd2dc7191b029efa14/types/block.go#L1204
89            // The Go implementation encodes a nil value into an empty struct. We try our best to
90            // anticipate an empty struct by using the default implementation which would otherwise be
91            // invalid.
92            if value == Id::default() {
93                RawBlockId {
94                    hash: vec![],
95                    part_set_header: Some(RawPartSetHeader {
96                        total: 0,
97                        hash: vec![],
98                    }),
99                }
100            } else {
101                RawBlockId {
102                    hash: value.hash.into(),
103                    part_set_header: Some(value.part_set_header.into()),
104                }
105            }
106        }
107    }
108
109    impl TryFrom<RawCanonicalBlockId> for Id {
110        type Error = Error;
111
112        fn try_from(value: RawCanonicalBlockId) -> Result<Self, Self::Error> {
113            if value.part_set_header.is_none() {
114                return Err(Error::invalid_part_set_header(
115                    "part_set_header is None".to_string(),
116                ));
117            }
118            Ok(Self {
119                hash: value.hash.try_into()?,
120                part_set_header: value.part_set_header.unwrap().try_into()?,
121            })
122        }
123    }
124
125    impl From<Id> for RawCanonicalBlockId {
126        fn from(value: Id) -> Self {
127            RawCanonicalBlockId {
128                hash: value.hash.as_bytes().to_vec(),
129                part_set_header: Some(value.part_set_header.into()),
130            }
131        }
132    }
133}
134
135mod v1 {
136    use super::Id;
137    use crate::{prelude::*, Error};
138    use cometbft_proto::types::v1::{
139        BlockId as RawBlockId, CanonicalBlockId as RawCanonicalBlockId,
140        PartSetHeader as RawPartSetHeader,
141    };
142    use cometbft_proto::Protobuf;
143
144    impl Protobuf<RawBlockId> for Id {}
145
146    impl TryFrom<RawBlockId> for Id {
147        type Error = Error;
148
149        fn try_from(value: RawBlockId) -> Result<Self, Self::Error> {
150            if value.part_set_header.is_none() {
151                return Err(Error::invalid_part_set_header(
152                    "part_set_header is None".to_string(),
153                ));
154            }
155            Ok(Self {
156                hash: value.hash.try_into()?,
157                part_set_header: value.part_set_header.unwrap().try_into()?,
158            })
159        }
160    }
161
162    impl From<Id> for RawBlockId {
163        fn from(value: Id) -> Self {
164            // https://github.com/cometbft/cometbft/blob/1635d1339c73ae6a82e062cd2dc7191b029efa14/types/block.go#L1204
165            // The Go implementation encodes a nil value into an empty struct. We try our best to
166            // anticipate an empty struct by using the default implementation which would otherwise be
167            // invalid.
168            if value == Id::default() {
169                RawBlockId {
170                    hash: vec![],
171                    part_set_header: Some(RawPartSetHeader {
172                        total: 0,
173                        hash: vec![],
174                    }),
175                }
176            } else {
177                RawBlockId {
178                    hash: value.hash.into(),
179                    part_set_header: Some(value.part_set_header.into()),
180                }
181            }
182        }
183    }
184
185    impl TryFrom<RawCanonicalBlockId> for Id {
186        type Error = Error;
187
188        fn try_from(value: RawCanonicalBlockId) -> Result<Self, Self::Error> {
189            if value.part_set_header.is_none() {
190                return Err(Error::invalid_part_set_header(
191                    "part_set_header is None".to_string(),
192                ));
193            }
194            Ok(Self {
195                hash: value.hash.try_into()?,
196                part_set_header: value.part_set_header.unwrap().try_into()?,
197            })
198        }
199    }
200
201    impl From<Id> for RawCanonicalBlockId {
202        fn from(value: Id) -> Self {
203            RawCanonicalBlockId {
204                hash: value.hash.as_bytes().to_vec(),
205                part_set_header: Some(value.part_set_header.into()),
206            }
207        }
208    }
209}
210
211mod v1beta1 {
212    use super::Id;
213    use crate::{prelude::*, Error};
214    use cometbft_proto::types::v1beta1::{
215        BlockId as RawBlockId, CanonicalBlockId as RawCanonicalBlockId,
216        PartSetHeader as RawPartSetHeader,
217    };
218    use cometbft_proto::Protobuf;
219
220    impl Protobuf<RawBlockId> for Id {}
221
222    impl TryFrom<RawBlockId> for Id {
223        type Error = Error;
224
225        fn try_from(value: RawBlockId) -> Result<Self, Self::Error> {
226            if value.part_set_header.is_none() {
227                return Err(Error::invalid_part_set_header(
228                    "part_set_header is None".to_string(),
229                ));
230            }
231            Ok(Self {
232                hash: value.hash.try_into()?,
233                part_set_header: value.part_set_header.unwrap().try_into()?,
234            })
235        }
236    }
237
238    impl From<Id> for RawBlockId {
239        fn from(value: Id) -> Self {
240            // https://github.com/cometbft/cometbft/blob/1635d1339c73ae6a82e062cd2dc7191b029efa14/types/block.go#L1204
241            // The Go implementation encodes a nil value into an empty struct. We try our best to
242            // anticipate an empty struct by using the default implementation which would otherwise be
243            // invalid.
244            if value == Id::default() {
245                RawBlockId {
246                    hash: vec![],
247                    part_set_header: Some(RawPartSetHeader {
248                        total: 0,
249                        hash: vec![],
250                    }),
251                }
252            } else {
253                RawBlockId {
254                    hash: value.hash.into(),
255                    part_set_header: Some(value.part_set_header.into()),
256                }
257            }
258        }
259    }
260
261    impl TryFrom<RawCanonicalBlockId> for Id {
262        type Error = Error;
263
264        fn try_from(value: RawCanonicalBlockId) -> Result<Self, Self::Error> {
265            if value.part_set_header.is_none() {
266                return Err(Error::invalid_part_set_header(
267                    "part_set_header is None".to_string(),
268                ));
269            }
270            Ok(Self {
271                hash: value.hash.try_into()?,
272                part_set_header: value.part_set_header.unwrap().try_into()?,
273            })
274        }
275    }
276
277    impl From<Id> for RawCanonicalBlockId {
278        fn from(value: Id) -> Self {
279            RawCanonicalBlockId {
280                hash: value.hash.as_bytes().to_vec(),
281                part_set_header: Some(value.part_set_header.into()),
282            }
283        }
284    }
285}
286
287impl Id {
288    /// Get a shortened 12-character prefix of a block ID (ala git)
289    pub fn prefix(&self) -> String {
290        let mut result = self.to_string();
291        result.truncate(PREFIX_LENGTH);
292        result
293    }
294}
295
296// TODO: match gaia serialization? e.g `D2F5991B98D708FD2C25AA2BEBED9358F24177DE:1:C37A55FB95E9`
297impl Display for Id {
298    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299        write!(f, "{}", &self.hash)
300    }
301}
302
303// TODO: match gaia serialization?
304impl FromStr for Id {
305    type Err = Error;
306
307    fn from_str(s: &str) -> Result<Self, Error> {
308        Ok(Self {
309            hash: Hash::from_hex_upper(Algorithm::Sha256, s)?,
310            part_set_header: PartSetHeader::default(),
311        })
312    }
313}
314
315/// Parse `block::Id` from a type
316pub trait ParseId {
317    /// Parse `block::Id`, or return an `Error` if parsing failed
318    fn parse_block_id(&self) -> Result<Id, Error>;
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    const EXAMPLE_SHA256_ID: &str =
326        "26C0A41F3243C6BCD7AD2DFF8A8D83A71D29D307B5326C227F734A1A512FE47D";
327
328    #[test]
329    fn parses_hex_strings() {
330        let id = Id::from_str(EXAMPLE_SHA256_ID).unwrap();
331        assert_eq!(
332            id.hash.as_bytes(),
333            b"\x26\xC0\xA4\x1F\x32\x43\xC6\xBC\xD7\xAD\x2D\xFF\x8A\x8D\x83\xA7\
334              \x1D\x29\xD3\x07\xB5\x32\x6C\x22\x7F\x73\x4A\x1A\x51\x2F\xE4\x7D"
335                .as_ref()
336        );
337    }
338
339    #[test]
340    fn serializes_hex_strings() {
341        let id = Id::from_str(EXAMPLE_SHA256_ID).unwrap();
342        assert_eq!(&id.to_string(), EXAMPLE_SHA256_ID)
343    }
344}