penumbra_sdk_ibc/component/
ics02_validation.rs

1use crate::IBC_PROOF_SPECS;
2use anyhow::{anyhow, Result};
3use ibc_proto::google::protobuf::Any;
4use ibc_types::{
5    core::connection::ChainId,
6    lightclients::tendermint::{
7        client_state::{ClientState as TendermintClientState, TENDERMINT_CLIENT_STATE_TYPE_URL},
8        consensus_state::{
9            ConsensusState as TendermintConsensusState, TENDERMINT_CONSENSUS_STATE_TYPE_URL,
10        },
11        header::{Header as TendermintHeader, TENDERMINT_HEADER_TYPE_URL},
12        misbehaviour::{Misbehaviour as TendermintMisbehavior, TENDERMINT_MISBEHAVIOUR_TYPE_URL},
13        TrustThreshold,
14    },
15};
16
17pub fn is_tendermint_header_state(header: &Any) -> bool {
18    header.type_url.as_str() == TENDERMINT_HEADER_TYPE_URL
19}
20pub fn is_tendermint_consensus_state(consensus_state: &Any) -> bool {
21    consensus_state.type_url.as_str() == TENDERMINT_CONSENSUS_STATE_TYPE_URL
22}
23pub fn is_tendermint_client_state(client_state: &Any) -> bool {
24    client_state.type_url.as_str() == TENDERMINT_CLIENT_STATE_TYPE_URL
25}
26pub fn is_tendermint_misbehavior(misbehavior: &Any) -> bool {
27    misbehavior.type_url.as_str() == TENDERMINT_MISBEHAVIOUR_TYPE_URL
28}
29
30pub fn get_tendermint_misbehavior(misbehavior: Any) -> Result<TendermintMisbehavior> {
31    if is_tendermint_misbehavior(&misbehavior) {
32        TendermintMisbehavior::try_from(misbehavior)
33            .map_err(|e| anyhow!(format!("failed to deserialize tendermint misbehavior: {e}")))
34    } else {
35        anyhow::bail!(format!(
36            "expected a tendermint light client misbehavior, got: {}",
37            misbehavior.type_url.as_str()
38        ))
39    }
40}
41
42pub fn get_tendermint_header(header: Any) -> Result<TendermintHeader> {
43    if is_tendermint_header_state(&header) {
44        TendermintHeader::try_from(header)
45            .map_err(|e| anyhow!(format!("failed to deserialize tendermint header: {e}")))
46    } else {
47        anyhow::bail!(format!(
48            "expected a tendermint light client header, got: {}",
49            header.type_url.as_str()
50        ))
51    }
52}
53
54pub fn get_tendermint_consensus_state(consensus_state: Any) -> Result<TendermintConsensusState> {
55    if is_tendermint_consensus_state(&consensus_state) {
56        TendermintConsensusState::try_from(consensus_state).map_err(|e| {
57            anyhow!(format!(
58                "failed to deserialize tendermint consensus state: {e}"
59            ))
60        })
61    } else {
62        anyhow::bail!(format!(
63            "expected tendermint consensus state, got: {}",
64            consensus_state.type_url.as_str()
65        ))
66    }
67}
68pub fn get_tendermint_client_state(client_state: Any) -> Result<TendermintClientState> {
69    if is_tendermint_client_state(&client_state) {
70        TendermintClientState::try_from(client_state).map_err(|e| {
71            anyhow!(format!(
72                "failed to deserialize tendermint client state: {e}"
73            ))
74        })
75    } else {
76        anyhow::bail!(format!(
77            "expected tendermint client state, got: {}",
78            client_state.type_url.as_str()
79        ))
80    }
81}
82
83// validate the parameters of an AnyClientState, verifying that it is a valid Penumbra client
84// state.
85pub fn validate_penumbra_sdk_client_state(
86    client_state: Any,
87    chain_id: &str,
88    current_height: u64,
89) -> anyhow::Result<()> {
90    let tm_client_state = get_tendermint_client_state(client_state)?;
91
92    if tm_client_state.frozen_height.is_some() {
93        anyhow::bail!("invalid client state: frozen");
94    }
95
96    // NOTE: Chain ID validation is actually not standardized yet. see
97    // https://github.com/informalsystems/ibc-rs/pull/304#discussion_r503917283
98    let chain_id = ChainId::from_string(chain_id);
99    if chain_id != tm_client_state.chain_id {
100        anyhow::bail!("invalid client state: chain id does not match");
101    }
102
103    // check that the revision number is the same as our chain ID's version
104    if tm_client_state.latest_height().revision_number() != chain_id.version() {
105        anyhow::bail!("invalid client state: revision number does not match");
106    }
107
108    // check that the latest height isn't gte the current block height
109    if tm_client_state.latest_height().revision_height() >= current_height {
110        anyhow::bail!(
111            "invalid client state: latest height is greater than or equal to the current block height"
112        );
113    }
114
115    // check client proof specs match penumbra proof specs
116    if IBC_PROOF_SPECS.clone() != tm_client_state.proof_specs {
117        // allow legacy proof specs without prehash_key_before_comparison
118        let mut spec_with_prehash_key = tm_client_state.proof_specs.clone();
119        spec_with_prehash_key[0].prehash_key_before_comparison = true;
120        spec_with_prehash_key[1].prehash_key_before_comparison = true;
121        if IBC_PROOF_SPECS.clone() != spec_with_prehash_key {
122            anyhow::bail!("invalid client state: proof specs do not match");
123        }
124    }
125
126    // check that the trust level is correct
127    validate_trust_threshold(tm_client_state.trust_level)?;
128
129    // TODO: check that the unbonding period is correct
130    //
131    // - check unbonding period is greater than trusting period
132    if tm_client_state.unbonding_period < tm_client_state.trusting_period {
133        anyhow::bail!("invalid client state: unbonding period is less than trusting period");
134    }
135
136    // TODO: check upgrade path
137
138    Ok(())
139}
140
141// Check that the trust threshold is:
142//
143// a) non-zero
144// b) greater or equal to 1/3
145// c) strictly less than 1
146fn validate_trust_threshold(trust_threshold: TrustThreshold) -> anyhow::Result<()> {
147    if trust_threshold.denominator() == 0 {
148        anyhow::bail!("trust threshold denominator cannot be zero");
149    }
150
151    if trust_threshold.numerator() * 3 < trust_threshold.denominator() {
152        anyhow::bail!("trust threshold must be greater than 1/3");
153    }
154
155    if trust_threshold.numerator() > trust_threshold.denominator() {
156        anyhow::bail!("trust threshold must be less than or equal to 1");
157    }
158
159    Ok(())
160}