iq_cometbft/block/
header.rs

1//! Block headers
2
3use cometbft_proto::Protobuf;
4use cometbft_proto::{
5    types::v1::{BlockId as RawBlockId, Header as RawHeader},
6    version::v1::Consensus as RawConsensusVersion,
7};
8use serde::{Deserialize, Serialize};
9
10use crate::{
11    account, block, chain,
12    crypto::Sha256,
13    merkle::{self, MerkleHash},
14    prelude::*,
15    AppHash, Hash, Time,
16};
17
18/// Block `Header` values contain metadata about the block and about the
19/// consensus, as well as commitments to the data in the current block, the
20/// previous block, and the results returned by the application.
21///
22/// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#header>
23#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(try_from = "RawHeader", into = "RawHeader")]
25pub struct Header {
26    /// Header version
27    pub version: Version,
28
29    /// Chain ID
30    pub chain_id: chain::Id,
31
32    /// Current block height
33    pub height: block::Height,
34
35    /// Current timestamp
36    pub time: Time,
37
38    /// Previous block info
39    pub last_block_id: Option<block::Id>,
40
41    /// Commit from validators from the last block
42    pub last_commit_hash: Option<Hash>,
43
44    /// Merkle root of transaction hashes
45    pub data_hash: Option<Hash>,
46
47    /// Validators for the current block
48    pub validators_hash: Hash,
49
50    /// Validators for the next block
51    pub next_validators_hash: Hash,
52
53    /// Consensus params for the current block
54    pub consensus_hash: Hash,
55
56    /// State after txs from the previous block
57    pub app_hash: AppHash,
58
59    /// Root hash of all results from the txs from the previous block
60    pub last_results_hash: Option<Hash>,
61
62    /// Hash of evidence included in the block
63    pub evidence_hash: Option<Hash>,
64
65    /// Original proposer of the block
66    pub proposer_address: account::Id,
67}
68
69impl Header {
70    /// Computes the hash of this header.
71    #[cfg(feature = "rust-crypto")]
72    pub fn hash(&self) -> Hash {
73        self.hash_with::<crate::crypto::default::Sha256>()
74    }
75
76    /// Hash this header with a Merkle hasher provided by a crypto provider.
77    pub fn hash_with<H>(&self) -> Hash
78    where
79        H: MerkleHash + Sha256 + Default,
80    {
81        // Note that if there is an encoding problem this will
82        // panic (as the golang code would):
83        // https://github.com/cometbft/cometbft/blob/134fe2896275bb926b49743c1e25493f6b24cc31/types/block.go#L393
84        // https://github.com/cometbft/cometbft/blob/134fe2896275bb926b49743c1e25493f6b24cc31/types/encoding_helper.go#L9:6
85
86        let fields_bytes = vec![
87            Protobuf::<RawConsensusVersion>::encode_vec(self.version),
88            self.chain_id.clone().encode_vec(),
89            self.height.encode_vec(),
90            self.time.encode_vec(),
91            Protobuf::<RawBlockId>::encode_vec(self.last_block_id.unwrap_or_default()),
92            self.last_commit_hash.unwrap_or_default().encode_vec(),
93            self.data_hash.unwrap_or_default().encode_vec(),
94            self.validators_hash.encode_vec(),
95            self.next_validators_hash.encode_vec(),
96            self.consensus_hash.encode_vec(),
97            self.app_hash.clone().encode_vec(),
98            self.last_results_hash.unwrap_or_default().encode_vec(),
99            self.evidence_hash.unwrap_or_default().encode_vec(),
100            self.proposer_address.encode_vec(),
101        ];
102
103        Hash::Sha256(merkle::simple_hash_from_byte_vectors::<H>(&fields_bytes))
104    }
105}
106
107/// `Version` contains the protocol version for the blockchain and the
108/// application.
109///
110/// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#version>
111#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
112pub struct Version {
113    /// Block version
114    pub block: u64,
115
116    /// App version
117    pub app: u64,
118}
119
120// =============================================================================
121// Protobuf conversions
122// =============================================================================
123
124cometbft_old_pb_modules! {
125    use super::{Header, Version};
126    use crate::{block, Error};
127    use pb::{
128        types::Header as RawHeader,
129        version::Consensus as RawConsensusVersion,
130    };
131
132    impl Protobuf<RawHeader> for Header {}
133
134    impl TryFrom<RawHeader> for Header {
135        type Error = Error;
136
137        fn try_from(value: RawHeader) -> Result<Self, Self::Error> {
138            // If last block id is unfilled, it is considered nil by Go.
139            let last_block_id = value
140                .last_block_id
141                .map(TryInto::try_into)
142                .transpose()?
143                .filter(|l| l != &block::Id::default());
144            let last_commit_hash = if value.last_commit_hash.is_empty() {
145                None
146            } else {
147                Some(value.last_commit_hash.try_into()?)
148            };
149            let last_results_hash = if value.last_results_hash.is_empty() {
150                None
151            } else {
152                Some(value.last_results_hash.try_into()?)
153            };
154            let height: block::Height = value.height.try_into()?;
155
156            // Todo: fix domain logic
157            // if last_block_id.is_none() && height.value() != 1 {
158            //    return Err(Kind::InvalidHeader.context("last_block_id is null on non-first
159            // height").into());
160            //}
161            if last_block_id.is_some() && height.value() == 1 {
162                return Err(Error::invalid_first_header());
163            }
164            // if last_commit_hash.is_none() && height.value() != 1 {
165            //    return Err(Kind::InvalidHeader.context("last_commit_hash is null on non-first
166            // height").into());
167            //}
168            // if height.value() == 1 && last_commit_hash.is_some() &&
169            // last_commit_hash.as_ref().unwrap() != simple_hash_from_byte_vectors(Vec::new()) {
170            //    return Err(Kind::InvalidFirstHeader.context("last_commit_hash is not empty Merkle tree
171            // on first height").into());
172            //}
173            // if last_results_hash.is_none() && height.value() != 1 {
174            //    return Err(Kind::InvalidHeader.context("last_results_hash is null on non-first
175            // height").into());
176            //}
177            // if last_results_hash.is_some() && height.value() == 1 {
178            //    return Err(Kind::InvalidFirstHeader.context("last_results_hash is not ull on first
179            // height").into());
180            //}
181            Ok(Header {
182                version: value.version.ok_or_else(Error::missing_version)?.into(),
183                chain_id: value.chain_id.try_into()?,
184                height,
185                time: value
186                    .time
187                    .ok_or_else(Error::missing_timestamp)?
188                    .try_into()?,
189                last_block_id,
190                last_commit_hash,
191                data_hash: if value.data_hash.is_empty() {
192                    None
193                } else {
194                    Some(value.data_hash.try_into()?)
195                },
196                validators_hash: value.validators_hash.try_into()?,
197                next_validators_hash: value.next_validators_hash.try_into()?,
198                consensus_hash: value.consensus_hash.try_into()?,
199                app_hash: value.app_hash.try_into()?,
200                last_results_hash,
201                evidence_hash: if value.evidence_hash.is_empty() {
202                    None
203                } else {
204                    Some(value.evidence_hash.try_into()?)
205                }, // Todo: Is it illegal to have evidence of wrongdoing in the first block?
206                proposer_address: value.proposer_address.try_into()?,
207            })
208        }
209    }
210
211    impl From<Header> for RawHeader {
212        fn from(value: Header) -> Self {
213            RawHeader {
214                version: Some(value.version.into()),
215                chain_id: value.chain_id.into(),
216                height: value.height.into(),
217                time: Some(value.time.into()),
218                last_block_id: value.last_block_id.map(Into::into),
219                last_commit_hash: value.last_commit_hash.unwrap_or_default().into(),
220                data_hash: value.data_hash.unwrap_or_default().into(),
221                validators_hash: value.validators_hash.into(),
222                next_validators_hash: value.next_validators_hash.into(),
223                consensus_hash: value.consensus_hash.into(),
224                app_hash: value.app_hash.into(),
225                last_results_hash: value.last_results_hash.unwrap_or_default().into(),
226                evidence_hash: value.evidence_hash.unwrap_or_default().into(),
227                proposer_address: value.proposer_address.into(),
228            }
229        }
230    }
231
232    impl Protobuf<RawConsensusVersion> for Version {}
233
234    impl From<RawConsensusVersion> for Version {
235        fn from(value: RawConsensusVersion) -> Self {
236            Version {
237                block: value.block,
238                app: value.app,
239            }
240        }
241    }
242
243    impl From<Version> for RawConsensusVersion {
244        fn from(value: Version) -> Self {
245            RawConsensusVersion {
246                block: value.block,
247                app: value.app,
248            }
249        }
250    }
251}
252
253mod v1 {
254    use super::{Header, Version};
255    use crate::{block, Error};
256    use cometbft_proto::Protobuf;
257    use cometbft_proto::{
258        types::v1::Header as RawHeader, version::v1::Consensus as RawConsensusVersion,
259    };
260
261    impl Protobuf<RawHeader> for Header {}
262
263    impl TryFrom<RawHeader> for Header {
264        type Error = Error;
265
266        fn try_from(value: RawHeader) -> Result<Self, Self::Error> {
267            // If last block id is unfilled, it is considered nil by Go.
268            let last_block_id = value
269                .last_block_id
270                .map(TryInto::try_into)
271                .transpose()?
272                .filter(|l| l != &block::Id::default());
273            let last_commit_hash = if value.last_commit_hash.is_empty() {
274                None
275            } else {
276                Some(value.last_commit_hash.try_into()?)
277            };
278            let last_results_hash = if value.last_results_hash.is_empty() {
279                None
280            } else {
281                Some(value.last_results_hash.try_into()?)
282            };
283            let height: block::Height = value.height.try_into()?;
284
285            // Todo: fix domain logic
286            // if last_block_id.is_none() && height.value() != 1 {
287            //    return Err(Kind::InvalidHeader.context("last_block_id is null on non-first
288            // height").into());
289            //}
290            if last_block_id.is_some() && height.value() == 1 {
291                return Err(Error::invalid_first_header());
292            }
293            // if last_commit_hash.is_none() && height.value() != 1 {
294            //    return Err(Kind::InvalidHeader.context("last_commit_hash is null on non-first
295            // height").into());
296            //}
297            // if height.value() == 1 && last_commit_hash.is_some() &&
298            // last_commit_hash.as_ref().unwrap() != simple_hash_from_byte_vectors(Vec::new()) {
299            //    return Err(Kind::InvalidFirstHeader.context("last_commit_hash is not empty Merkle tree
300            // on first height").into());
301            //}
302            // if last_results_hash.is_none() && height.value() != 1 {
303            //    return Err(Kind::InvalidHeader.context("last_results_hash is null on non-first
304            // height").into());
305            //}
306            // if last_results_hash.is_some() && height.value() == 1 {
307            //    return Err(Kind::InvalidFirstHeader.context("last_results_hash is not ull on first
308            // height").into());
309            //}
310            Ok(Header {
311                version: value.version.ok_or_else(Error::missing_version)?.into(),
312                chain_id: value.chain_id.try_into()?,
313                height,
314                time: value
315                    .time
316                    .ok_or_else(Error::missing_timestamp)?
317                    .try_into()?,
318                last_block_id,
319                last_commit_hash,
320                data_hash: if value.data_hash.is_empty() {
321                    None
322                } else {
323                    Some(value.data_hash.try_into()?)
324                },
325                validators_hash: value.validators_hash.try_into()?,
326                next_validators_hash: value.next_validators_hash.try_into()?,
327                consensus_hash: value.consensus_hash.try_into()?,
328                app_hash: value.app_hash.try_into()?,
329                last_results_hash,
330                evidence_hash: if value.evidence_hash.is_empty() {
331                    None
332                } else {
333                    Some(value.evidence_hash.try_into()?)
334                }, // Todo: Is it illegal to have evidence of wrongdoing in the first block?
335                proposer_address: value.proposer_address.try_into()?,
336            })
337        }
338    }
339
340    impl From<Header> for RawHeader {
341        fn from(value: Header) -> Self {
342            RawHeader {
343                version: Some(value.version.into()),
344                chain_id: value.chain_id.into(),
345                height: value.height.into(),
346                time: Some(value.time.into()),
347                last_block_id: value.last_block_id.map(Into::into),
348                last_commit_hash: value.last_commit_hash.unwrap_or_default().into(),
349                data_hash: value.data_hash.unwrap_or_default().into(),
350                validators_hash: value.validators_hash.into(),
351                next_validators_hash: value.next_validators_hash.into(),
352                consensus_hash: value.consensus_hash.into(),
353                app_hash: value.app_hash.into(),
354                last_results_hash: value.last_results_hash.unwrap_or_default().into(),
355                evidence_hash: value.evidence_hash.unwrap_or_default().into(),
356                proposer_address: value.proposer_address.into(),
357            }
358        }
359    }
360
361    impl Protobuf<RawConsensusVersion> for Version {}
362
363    impl From<RawConsensusVersion> for Version {
364        fn from(value: RawConsensusVersion) -> Self {
365            Version {
366                block: value.block,
367                app: value.app,
368            }
369        }
370    }
371
372    impl From<Version> for RawConsensusVersion {
373        fn from(value: Version) -> Self {
374            RawConsensusVersion {
375                block: value.block,
376                app: value.app,
377            }
378        }
379    }
380}
381
382mod v1beta1 {
383    use super::Header;
384    use crate::{block, Error};
385    use cometbft_proto::types::v1beta1::Header as RawHeader;
386    use cometbft_proto::Protobuf;
387
388    impl Protobuf<RawHeader> for Header {}
389
390    impl TryFrom<RawHeader> for Header {
391        type Error = Error;
392
393        fn try_from(value: RawHeader) -> Result<Self, Self::Error> {
394            // If last block id is unfilled, it is considered nil by Go.
395            let last_block_id = value
396                .last_block_id
397                .map(TryInto::try_into)
398                .transpose()?
399                .filter(|l| l != &block::Id::default());
400            let last_commit_hash = if value.last_commit_hash.is_empty() {
401                None
402            } else {
403                Some(value.last_commit_hash.try_into()?)
404            };
405            let last_results_hash = if value.last_results_hash.is_empty() {
406                None
407            } else {
408                Some(value.last_results_hash.try_into()?)
409            };
410            let height: block::Height = value.height.try_into()?;
411
412            // Todo: fix domain logic
413            // if last_block_id.is_none() && height.value() != 1 {
414            //    return Err(Kind::InvalidHeader.context("last_block_id is null on non-first
415            // height").into());
416            //}
417            if last_block_id.is_some() && height.value() == 1 {
418                return Err(Error::invalid_first_header());
419            }
420            // if last_commit_hash.is_none() && height.value() != 1 {
421            //    return Err(Kind::InvalidHeader.context("last_commit_hash is null on non-first
422            // height").into());
423            //}
424            // if height.value() == 1 && last_commit_hash.is_some() &&
425            // last_commit_hash.as_ref().unwrap() != simple_hash_from_byte_vectors(Vec::new()) {
426            //    return Err(Kind::InvalidFirstHeader.context("last_commit_hash is not empty Merkle tree
427            // on first height").into());
428            //}
429            // if last_results_hash.is_none() && height.value() != 1 {
430            //    return Err(Kind::InvalidHeader.context("last_results_hash is null on non-first
431            // height").into());
432            //}
433            // if last_results_hash.is_some() && height.value() == 1 {
434            //    return Err(Kind::InvalidFirstHeader.context("last_results_hash is not ull on first
435            // height").into());
436            //}
437            Ok(Header {
438                version: value.version.ok_or_else(Error::missing_version)?.into(),
439                chain_id: value.chain_id.try_into()?,
440                height,
441                time: value
442                    .time
443                    .ok_or_else(Error::missing_timestamp)?
444                    .try_into()?,
445                last_block_id,
446                last_commit_hash,
447                data_hash: if value.data_hash.is_empty() {
448                    None
449                } else {
450                    Some(value.data_hash.try_into()?)
451                },
452                validators_hash: value.validators_hash.try_into()?,
453                next_validators_hash: value.next_validators_hash.try_into()?,
454                consensus_hash: value.consensus_hash.try_into()?,
455                app_hash: value.app_hash.try_into()?,
456                last_results_hash,
457                evidence_hash: if value.evidence_hash.is_empty() {
458                    None
459                } else {
460                    Some(value.evidence_hash.try_into()?)
461                }, // Todo: Is it illegal to have evidence of wrongdoing in the first block?
462                proposer_address: value.proposer_address.try_into()?,
463            })
464        }
465    }
466
467    impl From<Header> for RawHeader {
468        fn from(value: Header) -> Self {
469            RawHeader {
470                version: Some(value.version.into()),
471                chain_id: value.chain_id.into(),
472                height: value.height.into(),
473                time: Some(value.time.into()),
474                last_block_id: value.last_block_id.map(Into::into),
475                last_commit_hash: value.last_commit_hash.unwrap_or_default().into(),
476                data_hash: value.data_hash.unwrap_or_default().into(),
477                validators_hash: value.validators_hash.into(),
478                next_validators_hash: value.next_validators_hash.into(),
479                consensus_hash: value.consensus_hash.into(),
480                app_hash: value.app_hash.into(),
481                last_results_hash: value.last_results_hash.unwrap_or_default().into(),
482                evidence_hash: value.evidence_hash.unwrap_or_default().into(),
483                proposer_address: value.proposer_address.into(),
484            }
485        }
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use super::Header;
492    use crate::test::test_serialization_roundtrip;
493
494    #[test]
495    fn serialization_roundtrip() {
496        let json_data = include_str!("../../tests/support/serialization/block/header.json");
497        test_serialization_roundtrip::<Header>(json_data);
498    }
499
500    #[cfg(feature = "rust-crypto")]
501    mod crypto {
502        use super::*;
503        use crate::{hash::Algorithm, Hash};
504
505        #[test]
506        fn header_hashing() {
507            let expected_hash = Hash::from_hex_upper(
508                Algorithm::Sha256,
509                "F30A71F2409FB15AACAEDB6CC122DFA2525BEE9CAE521721B06BFDCA291B8D56",
510            )
511            .unwrap();
512            let header: Header = serde_json::from_str(include_str!(
513                "../../tests/support/serialization/block/header_with_known_hash.json"
514            ))
515            .unwrap();
516            assert_eq!(expected_hash, header.hash());
517        }
518    }
519}