ibc_client_tendermint/client_state/
update_client.rs

1use ibc_client_tendermint_types::error::{IntoResult, TendermintClientError};
2use ibc_client_tendermint_types::{ConsensusState as ConsensusStateType, Header as TmHeader};
3use ibc_core_client::context::{Convertible, ExtClientValidationContext};
4use ibc_core_client::types::error::ClientError;
5use ibc_core_client::types::Height;
6use ibc_core_host::types::error::IdentifierError;
7use ibc_core_host::types::identifiers::{ChainId, ClientId};
8use ibc_core_host::types::path::ClientConsensusStatePath;
9use ibc_primitives::prelude::*;
10use ibc_primitives::IntoHostTime;
11use tendermint::crypto::Sha256;
12use tendermint::merkle::MerkleHash;
13use tendermint_light_client_verifier::options::Options;
14use tendermint_light_client_verifier::types::{TrustedBlockState, UntrustedBlockState};
15use tendermint_light_client_verifier::Verifier;
16
17pub fn verify_header<V, H>(
18    ctx: &V,
19    header: &TmHeader,
20    client_id: &ClientId,
21    chain_id: &ChainId,
22    options: &Options,
23    verifier: &impl Verifier,
24) -> Result<(), ClientError>
25where
26    V: ExtClientValidationContext,
27    ConsensusStateType: Convertible<V::ConsensusStateRef>,
28    <ConsensusStateType as TryFrom<V::ConsensusStateRef>>::Error: Into<ClientError>,
29    H: MerkleHash + Sha256 + Default,
30{
31    // Checks that the header fields are valid.
32    header.validate_basic::<H>()?;
33
34    // The tendermint-light-client crate though works on heights that are assumed
35    // to have the same revision number. We ensure this here.
36    header.verify_chain_id_version_matches_height(chain_id)?;
37
38    // Delegate to tendermint-light-client, which contains the required checks
39    // of the new header against the trusted consensus state.
40    {
41        let trusted_state = {
42            let trusted_client_cons_state_path = ClientConsensusStatePath::new(
43                client_id.clone(),
44                header.trusted_height.revision_number(),
45                header.trusted_height.revision_height(),
46            );
47            let trusted_consensus_state: ConsensusStateType = ctx
48                .consensus_state(&trusted_client_cons_state_path)?
49                .try_into()
50                .map_err(Into::into)?;
51
52            header.check_trusted_next_validator_set::<H>(
53                &trusted_consensus_state.next_validators_hash,
54            )?;
55
56            TrustedBlockState {
57                chain_id: &chain_id.as_str().try_into().map_err(|e| {
58                    IdentifierError::FailedToParse {
59                        description: format!("chain ID `{chain_id}`: {e:?}"),
60                    }
61                })?,
62                header_time: trusted_consensus_state.timestamp(),
63                height: header
64                    .trusted_height
65                    .revision_height()
66                    .try_into()
67                    .map_err(|_| ClientError::FailedToVerifyHeader {
68                        description: TendermintClientError::InvalidHeaderHeight(
69                            header.trusted_height.revision_height(),
70                        )
71                        .to_string(),
72                    })?,
73                next_validators: &header.trusted_next_validator_set,
74                next_validators_hash: trusted_consensus_state.next_validators_hash,
75            }
76        };
77
78        let untrusted_state = UntrustedBlockState {
79            signed_header: &header.signed_header,
80            validators: &header.validator_set,
81            // NB: This will skip the
82            // VerificationPredicates::next_validators_match check for the
83            // untrusted state.
84            next_validators: None,
85        };
86
87        let now = ctx.host_timestamp()?.into_host_time()?;
88
89        // main header verification, delegated to the tendermint-light-client crate.
90        verifier
91            .verify_update_header(untrusted_state, trusted_state, options, now)
92            .into_result()?;
93    }
94
95    Ok(())
96}
97
98/// Checks for misbehaviour upon receiving a new consensus state as part
99/// of a client update.
100pub fn check_for_misbehaviour_on_update<V>(
101    ctx: &V,
102    header: TmHeader,
103    client_id: &ClientId,
104    client_latest_height: &Height,
105) -> Result<bool, ClientError>
106where
107    V: ExtClientValidationContext,
108    ConsensusStateType: Convertible<V::ConsensusStateRef>,
109    <ConsensusStateType as TryFrom<V::ConsensusStateRef>>::Error: Into<ClientError>,
110{
111    let maybe_existing_consensus_state = {
112        let path_at_header_height = ClientConsensusStatePath::new(
113            client_id.clone(),
114            header.height().revision_number(),
115            header.height().revision_height(),
116        );
117
118        ctx.consensus_state(&path_at_header_height).ok()
119    };
120
121    if let Some(existing_consensus_state) = maybe_existing_consensus_state {
122        let existing_consensus_state: ConsensusStateType =
123            existing_consensus_state.try_into().map_err(Into::into)?;
124
125        let header_consensus_state = ConsensusStateType::from(header);
126
127        // There is evidence of misbehaviour if the stored consensus state
128        // is different from the new one we received.
129        Ok(existing_consensus_state != header_consensus_state)
130    } else {
131        // If no header was previously installed, we ensure the monotonicity of timestamps.
132
133        // 1. for all headers, the new header needs to have a larger timestamp than
134        //    the “previous header”
135        {
136            let maybe_prev_cs = ctx.prev_consensus_state(client_id, &header.height())?;
137
138            if let Some(prev_cs) = maybe_prev_cs {
139                // New header timestamp cannot occur *before* the
140                // previous consensus state's height
141                let prev_cs: ConsensusStateType = prev_cs.try_into().map_err(Into::into)?;
142
143                if header.signed_header.header().time <= prev_cs.timestamp() {
144                    return Ok(true);
145                }
146            }
147        }
148
149        // 2. if a header comes in and is not the “last” header, then we also ensure
150        //    that its timestamp is less than the “next header”
151        if &header.height() < client_latest_height {
152            let maybe_next_cs = ctx.next_consensus_state(client_id, &header.height())?;
153
154            if let Some(next_cs) = maybe_next_cs {
155                // New (untrusted) header timestamp cannot occur *after* next
156                // consensus state's height
157                let next_cs: ConsensusStateType = next_cs.try_into().map_err(Into::into)?;
158
159                if header.signed_header.header().time >= next_cs.timestamp() {
160                    return Ok(true);
161                }
162            }
163        }
164
165        Ok(false)
166    }
167}