1use 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(try_from = "RawHeader", into = "RawHeader")]
25pub struct Header {
26 pub version: Version,
28
29 pub chain_id: chain::Id,
31
32 pub height: block::Height,
34
35 pub time: Time,
37
38 pub last_block_id: Option<block::Id>,
40
41 pub last_commit_hash: Option<Hash>,
43
44 pub data_hash: Option<Hash>,
46
47 pub validators_hash: Hash,
49
50 pub next_validators_hash: Hash,
52
53 pub consensus_hash: Hash,
55
56 pub app_hash: AppHash,
58
59 pub last_results_hash: Option<Hash>,
61
62 pub evidence_hash: Option<Hash>,
64
65 pub proposer_address: account::Id,
67}
68
69impl Header {
70 #[cfg(feature = "rust-crypto")]
72 pub fn hash(&self) -> Hash {
73 self.hash_with::<crate::crypto::default::Sha256>()
74 }
75
76 pub fn hash_with<H>(&self) -> Hash
78 where
79 H: MerkleHash + Sha256 + Default,
80 {
81 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#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
112pub struct Version {
113 pub block: u64,
115
116 pub app: u64,
118}
119
120cometbft_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 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 if last_block_id.is_some() && height.value() == 1 {
162 return Err(Error::invalid_first_header());
163 }
164 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 }, 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 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 if last_block_id.is_some() && height.value() == 1 {
291 return Err(Error::invalid_first_header());
292 }
293 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 }, 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 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 if last_block_id.is_some() && height.value() == 1 {
418 return Err(Error::invalid_first_header());
419 }
420 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 }, 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}