use serde::{Deserialize, Serialize};
use tendermint_proto::v0_37::{
types::{BlockId as RawBlockId, Header as RawHeader},
version::Consensus as RawConsensusVersion,
};
use tendermint_proto::Protobuf;
use crate::{
account, block, chain,
crypto::Sha256,
merkle::{self, MerkleHash},
prelude::*,
AppHash, Hash, Time,
};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "RawHeader", into = "RawHeader")]
pub struct Header {
pub version: Version,
pub chain_id: chain::Id,
pub height: block::Height,
pub time: Time,
pub last_block_id: Option<block::Id>,
pub last_commit_hash: Option<Hash>,
pub data_hash: Option<Hash>,
pub validators_hash: Hash,
pub next_validators_hash: Hash,
pub consensus_hash: Hash,
pub app_hash: AppHash,
pub last_results_hash: Option<Hash>,
pub evidence_hash: Option<Hash>,
pub proposer_address: account::Id,
}
impl Header {
#[cfg(feature = "rust-crypto")]
pub fn hash(&self) -> Hash {
self.hash_with::<crate::crypto::default::Sha256>()
}
pub fn hash_with<H>(&self) -> Hash
where
H: MerkleHash + Sha256 + Default,
{
let fields_bytes = vec![
Protobuf::<RawConsensusVersion>::encode_vec(self.version),
self.chain_id.clone().encode_vec(),
self.height.encode_vec(),
self.time.encode_vec(),
Protobuf::<RawBlockId>::encode_vec(self.last_block_id.unwrap_or_default()),
self.last_commit_hash.unwrap_or_default().encode_vec(),
self.data_hash.unwrap_or_default().encode_vec(),
self.validators_hash.encode_vec(),
self.next_validators_hash.encode_vec(),
self.consensus_hash.encode_vec(),
self.app_hash.clone().encode_vec(),
self.last_results_hash.unwrap_or_default().encode_vec(),
self.evidence_hash.unwrap_or_default().encode_vec(),
self.proposer_address.encode_vec(),
];
Hash::Sha256(merkle::simple_hash_from_byte_vectors::<H>(&fields_bytes))
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Version {
pub block: u64,
pub app: u64,
}
tendermint_pb_modules! {
use super::{Header, Version};
use crate::{block, Error};
use pb::{
types::Header as RawHeader,
version::Consensus as RawConsensusVersion,
};
impl Protobuf<RawHeader> for Header {}
impl TryFrom<RawHeader> for Header {
type Error = Error;
fn try_from(value: RawHeader) -> Result<Self, Self::Error> {
let last_block_id = value
.last_block_id
.map(TryInto::try_into)
.transpose()?
.filter(|l| l != &block::Id::default());
let last_commit_hash = if value.last_commit_hash.is_empty() {
None
} else {
Some(value.last_commit_hash.try_into()?)
};
let last_results_hash = if value.last_results_hash.is_empty() {
None
} else {
Some(value.last_results_hash.try_into()?)
};
let height: block::Height = value.height.try_into()?;
if last_block_id.is_some() && height.value() == 1 {
return Err(Error::invalid_first_header());
}
Ok(Header {
version: value.version.ok_or_else(Error::missing_version)?.into(),
chain_id: value.chain_id.try_into()?,
height,
time: value
.time
.ok_or_else(Error::missing_timestamp)?
.try_into()?,
last_block_id,
last_commit_hash,
data_hash: if value.data_hash.is_empty() {
None
} else {
Some(value.data_hash.try_into()?)
},
validators_hash: value.validators_hash.try_into()?,
next_validators_hash: value.next_validators_hash.try_into()?,
consensus_hash: value.consensus_hash.try_into()?,
app_hash: value.app_hash.try_into()?,
last_results_hash,
evidence_hash: if value.evidence_hash.is_empty() {
None
} else {
Some(value.evidence_hash.try_into()?)
}, proposer_address: value.proposer_address.try_into()?,
})
}
}
impl From<Header> for RawHeader {
fn from(value: Header) -> Self {
RawHeader {
version: Some(value.version.into()),
chain_id: value.chain_id.into(),
height: value.height.into(),
time: Some(value.time.into()),
last_block_id: value.last_block_id.map(Into::into),
last_commit_hash: value.last_commit_hash.unwrap_or_default().into(),
data_hash: value.data_hash.unwrap_or_default().into(),
validators_hash: value.validators_hash.into(),
next_validators_hash: value.next_validators_hash.into(),
consensus_hash: value.consensus_hash.into(),
app_hash: value.app_hash.into(),
last_results_hash: value.last_results_hash.unwrap_or_default().into(),
evidence_hash: value.evidence_hash.unwrap_or_default().into(),
proposer_address: value.proposer_address.into(),
}
}
}
impl Protobuf<RawConsensusVersion> for Version {}
impl From<RawConsensusVersion> for Version {
fn from(value: RawConsensusVersion) -> Self {
Version {
block: value.block,
app: value.app,
}
}
}
impl From<Version> for RawConsensusVersion {
fn from(value: Version) -> Self {
RawConsensusVersion {
block: value.block,
app: value.app,
}
}
}
}
#[cfg(test)]
mod tests {
use super::Header;
use crate::test::test_serialization_roundtrip;
#[test]
fn serialization_roundtrip() {
let json_data = include_str!("../../tests/support/serialization/block/header.json");
test_serialization_roundtrip::<Header>(json_data);
}
#[cfg(feature = "rust-crypto")]
mod crypto {
use super::*;
use crate::{hash::Algorithm, Hash};
#[test]
fn header_hashing() {
let expected_hash = Hash::from_hex_upper(
Algorithm::Sha256,
"F30A71F2409FB15AACAEDB6CC122DFA2525BEE9CAE521721B06BFDCA291B8D56",
)
.unwrap();
let header: Header = serde_json::from_str(include_str!(
"../../tests/support/serialization/block/header_with_known_hash.json"
))
.unwrap();
assert_eq!(expected_hash, header.hash());
}
}
}