ibc_client_tendermint_types/
header.rs

1//! Defines the domain type for tendermint headers
2
3use core::fmt::{Display, Error as FmtError, Formatter};
4use core::str::FromStr;
5
6use ibc_core_client_types::error::ClientError;
7use ibc_core_client_types::Height;
8use ibc_core_host_types::error::DecodingError;
9use ibc_core_host_types::identifiers::ChainId;
10use ibc_primitives::prelude::*;
11use ibc_primitives::{IntoTimestamp, Timestamp};
12use ibc_proto::google::protobuf::Any;
13use ibc_proto::ibc::lightclients::tendermint::v1::Header as RawHeader;
14use ibc_proto::Protobuf;
15use pretty::{PrettySignedHeader, PrettyValidatorSet};
16use tendermint::block::signed_header::SignedHeader;
17use tendermint::chain::Id as TmChainId;
18use tendermint::crypto::Sha256;
19use tendermint::merkle::MerkleHash;
20use tendermint::validator::Set as ValidatorSet;
21use tendermint::{Hash, Time};
22use tendermint_light_client_verifier::types::{TrustedBlockState, UntrustedBlockState};
23
24use crate::error::TendermintClientError;
25
26pub const TENDERMINT_HEADER_TYPE_URL: &str = "/ibc.lightclients.tendermint.v1.Header";
27
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29/// Tendermint consensus header
30#[derive(Clone, PartialEq, Eq)]
31pub struct Header {
32    pub signed_header: SignedHeader, // contains the commitment root
33    pub validator_set: ValidatorSet, // the validator set that signed Header
34    pub trusted_height: Height, // the height of a trusted header seen by client less than or equal to Header
35    pub trusted_next_validator_set: ValidatorSet, // the last trusted validator set at trusted height
36}
37
38impl core::fmt::Debug for Header {
39    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
40        write!(f, " Header {{...}}")
41    }
42}
43
44impl Display for Header {
45    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
46        write!(f, "Header {{ signed_header: {}, validator_set: {}, trusted_height: {}, trusted_validator_set: {} }}", PrettySignedHeader(&self.signed_header), PrettyValidatorSet(&self.validator_set), self.trusted_height, PrettyValidatorSet(&self.trusted_next_validator_set))
47    }
48}
49
50impl Header {
51    pub fn timestamp(&self) -> Result<Timestamp, TendermintClientError> {
52        self.signed_header
53            .header
54            .time
55            .into_timestamp()
56            .map_err(Into::into)
57    }
58
59    pub fn height(&self) -> Height {
60        Height::new(
61            ChainId::from_str(self.signed_header.header.chain_id.as_str())
62                .expect("chain id")
63                .revision_number(),
64            u64::from(self.signed_header.header.height),
65        )
66        .expect("malformed tendermint header domain type has an illegal height of 0")
67    }
68
69    pub fn as_untrusted_block_state(&self) -> UntrustedBlockState<'_> {
70        UntrustedBlockState {
71            signed_header: &self.signed_header,
72            validators: &self.validator_set,
73            next_validators: None,
74        }
75    }
76
77    pub fn as_trusted_block_state<'a>(
78        &'a self,
79        chain_id: &'a TmChainId,
80        header_time: Time,
81        next_validators_hash: Hash,
82    ) -> Result<TrustedBlockState<'a>, TendermintClientError> {
83        Ok(TrustedBlockState {
84            chain_id,
85            header_time,
86            height: self
87                .trusted_height
88                .revision_height()
89                .try_into()
90                .map_err(|_| {
91                    TendermintClientError::InvalidHeaderHeight(
92                        self.trusted_height.revision_height(),
93                    )
94                })?,
95            next_validators: &self.trusted_next_validator_set,
96            next_validators_hash,
97        })
98    }
99
100    pub fn verify_chain_id_version_matches_height(
101        &self,
102        chain_id: &ChainId,
103    ) -> Result<(), TendermintClientError> {
104        if self.height().revision_number() != chain_id.revision_number() {
105            return Err(TendermintClientError::MismatchedHeaderChainIds {
106                actual: self.signed_header.header.chain_id.to_string(),
107                expected: chain_id.to_string(),
108            });
109        }
110        Ok(())
111    }
112
113    /// `header.trusted_next_validator_set` was given to us by the relayer.
114    /// Thus, we need to ensure that the relayer gave us the right set, i.e. by
115    /// ensuring that it matches the hash we have stored on chain.
116    pub fn check_trusted_next_validator_set<H: MerkleHash + Sha256 + Default>(
117        &self,
118        trusted_next_validator_hash: &Hash,
119    ) -> Result<(), ClientError> {
120        if &self.trusted_next_validator_set.hash_with::<H>() == trusted_next_validator_hash {
121            Ok(())
122        } else {
123            Err(ClientError::FailedToVerifyHeader {
124                description: "trusted next validator set hash does not match hash stored on chain"
125                    .to_string(),
126            })
127        }
128    }
129
130    /// Checks if the fields of a given header are consistent with the trusted fields of this header.
131    pub fn validate_basic<H: MerkleHash + Sha256 + Default>(
132        &self,
133    ) -> Result<(), TendermintClientError> {
134        if self.height().revision_number() != self.trusted_height.revision_number() {
135            return Err(TendermintClientError::MismatchedRevisionHeights {
136                expected: self.trusted_height.revision_number(),
137                actual: self.height().revision_number(),
138            });
139        }
140
141        // We need to ensure that the trusted height (representing the
142        // height of the header already on chain for which this client update is
143        // based on) must be smaller than height of the new header that we're
144        // installing.
145        if self.trusted_height >= self.height() {
146            return Err(TendermintClientError::InvalidHeaderHeight(
147                self.height().revision_height(),
148            ));
149        }
150
151        let validators_hash = self.validator_set.hash_with::<H>();
152
153        if validators_hash != self.signed_header.header.validators_hash {
154            return Err(TendermintClientError::MismatchedValidatorHashes {
155                expected: self.signed_header.header.validators_hash,
156                actual: validators_hash,
157            });
158        }
159
160        Ok(())
161    }
162}
163
164impl Protobuf<RawHeader> for Header {}
165
166impl TryFrom<RawHeader> for Header {
167    type Error = DecodingError;
168
169    fn try_from(raw: RawHeader) -> Result<Self, Self::Error> {
170        let header = Self {
171            signed_header: raw
172                .signed_header
173                .ok_or(DecodingError::missing_raw_data("signed header"))?
174                .try_into()
175                .map_err(|e| DecodingError::invalid_raw_data(format!("signed header: {e:?}")))?,
176            validator_set: raw
177                .validator_set
178                .ok_or(DecodingError::missing_raw_data("validator set"))?
179                .try_into()
180                .map_err(|e| DecodingError::invalid_raw_data(format!("validator set: {e:?}")))?,
181            trusted_height: raw
182                .trusted_height
183                .and_then(|raw_height| raw_height.try_into().ok())
184                .ok_or(DecodingError::missing_raw_data("trusted height"))?,
185            trusted_next_validator_set: raw
186                .trusted_validators
187                .ok_or(DecodingError::missing_raw_data(
188                    "trusted next validator set",
189                ))?
190                .try_into()
191                .map_err(|e| {
192                    DecodingError::invalid_raw_data(format!("trusted next validator set: {e:?}"))
193                })?,
194        };
195
196        Ok(header)
197    }
198}
199
200impl Protobuf<Any> for Header {}
201
202impl TryFrom<Any> for Header {
203    type Error = DecodingError;
204
205    fn try_from(raw: Any) -> Result<Self, Self::Error> {
206        if let TENDERMINT_HEADER_TYPE_URL = raw.type_url.as_str() {
207            Protobuf::<RawHeader>::decode(raw.value.as_ref()).map_err(Into::into)
208        } else {
209            Err(DecodingError::MismatchedResourceName {
210                expected: TENDERMINT_HEADER_TYPE_URL.to_string(),
211                actual: raw.type_url,
212            })
213        }
214    }
215}
216
217impl From<Header> for Any {
218    fn from(header: Header) -> Self {
219        Any {
220            type_url: TENDERMINT_HEADER_TYPE_URL.to_string(),
221            value: Protobuf::<RawHeader>::encode_vec(header),
222        }
223    }
224}
225
226impl From<Header> for RawHeader {
227    fn from(value: Header) -> Self {
228        RawHeader {
229            signed_header: Some(value.signed_header.into()),
230            validator_set: Some(value.validator_set.into()),
231            trusted_height: Some(value.trusted_height.into()),
232            trusted_validators: Some(value.trusted_next_validator_set.into()),
233        }
234    }
235}
236
237mod pretty {
238    use ibc_primitives::utils::PrettySlice;
239
240    pub use super::*;
241
242    pub struct PrettySignedHeader<'a>(pub &'a SignedHeader);
243
244    impl Display for PrettySignedHeader<'_> {
245        fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
246            write!(
247            f,
248            "SignedHeader {{ header: {{ chain_id: {}, height: {} }}, commit: {{ height: {} }} }}",
249            self.0.header.chain_id, self.0.header.height, self.0.commit.height
250        )
251        }
252    }
253
254    pub struct PrettyValidatorSet<'a>(pub &'a ValidatorSet);
255
256    impl Display for PrettyValidatorSet<'_> {
257        fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
258            let validator_addresses: Vec<_> = self
259                .0
260                .validators()
261                .iter()
262                .map(|validator| validator.address)
263                .collect();
264            if let Some(proposer) = self.0.proposer() {
265                match &proposer.name {
266                Some(name) => write!(f, "PrettyValidatorSet {{ validators: {}, proposer: {}, total_voting_power: {} }}", PrettySlice(&validator_addresses), name, self.0.total_voting_power()),
267                None =>  write!(f, "PrettyValidatorSet {{ validators: {}, proposer: None, total_voting_power: {} }}", PrettySlice(&validator_addresses), self.0.total_voting_power()),
268            }
269            } else {
270                write!(
271                f,
272                "PrettyValidatorSet {{ validators: {}, proposer: None, total_voting_power: {} }}",
273                PrettySlice(&validator_addresses),
274                self.0.total_voting_power()
275            )
276            }
277        }
278    }
279}