penumbra_sdk_ibc/component/
ics02_validation.rs1use 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
83pub 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 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 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 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 if IBC_PROOF_SPECS.clone() != tm_client_state.proof_specs {
117 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 validate_trust_threshold(tm_client_state.trust_level)?;
128
129 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 Ok(())
139}
140
141fn 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}