penumbra_sdk_ibc/component/msg_handler/
update_client.rs1use anyhow::{Context, Result};
2use async_trait::async_trait;
3use cnidarium::{StateRead, StateWrite};
4use ibc_types::{
5 core::{client::events::UpdateClient, client::msgs::MsgUpdateClient, client::ClientId},
6 lightclients::tendermint::client_state::ClientState as TendermintClientState,
7 lightclients::tendermint::header::Header as TendermintHeader,
8 lightclients::tendermint::{
9 consensus_state::ConsensusState as TendermintConsensusState, TENDERMINT_CLIENT_TYPE,
10 },
11};
12use tendermint::validator;
13use tendermint_light_client_verifier::{
14 types::{TrustedBlockState, UntrustedBlockState},
15 ProdVerifier, Verdict, Verifier,
16};
17
18use crate::component::{
19 client::{
20 ConsensusStateWriteExt as _, Ics2ClientExt as _, StateReadExt as _, StateWriteExt as _,
21 },
22 ics02_validation, HostInterface, MsgHandler,
23};
24
25#[async_trait]
26impl MsgHandler for MsgUpdateClient {
27 async fn check_stateless<AH>(&self) -> Result<()> {
28 header_is_tendermint(self)?;
29
30 Ok(())
31 }
32
33 async fn try_execute<S: StateWrite, AH, HI: HostInterface>(&self, mut state: S) -> Result<()> {
34 if update_is_already_committed(&state, self).await? {
39 tracing::debug!("skipping duplicate update");
40 return Ok(());
41 }
42 tracing::debug!(msg = ?self);
43
44 let client_state = client_is_present(&state, self).await?;
45
46 client_is_not_frozen(&client_state)?;
47 client_is_not_expired::<&S, HI>(&state, &self.client_id, &client_state).await?;
48
49 let trusted_client_state = client_state;
50
51 let untrusted_header =
52 ics02_validation::get_tendermint_header(self.client_message.clone())?;
53
54 header_revision_matches_client_state(&trusted_client_state, &untrusted_header)?;
55 header_height_is_consistent(&untrusted_header)?;
56
57 let trusted_height = untrusted_header.trusted_height;
60
61 let last_trusted_consensus_state = state
64 .get_verified_consensus_state(&trusted_height, &self.client_id)
65 .await?;
66
67 let trusted_height = trusted_height
70 .revision_height()
71 .try_into()
72 .context("invalid header height")?;
73
74 let trusted_validator_set =
75 verify_header_validator_set(&untrusted_header, &last_trusted_consensus_state)?;
76
77 let trusted_state = TrustedBlockState {
80 chain_id: &trusted_client_state.chain_id.clone().into(),
82 header_time: last_trusted_consensus_state.timestamp,
83 height: trusted_height,
84 next_validators: trusted_validator_set,
85 next_validators_hash: last_trusted_consensus_state.next_validators_hash,
86 };
87
88 let untrusted_state = UntrustedBlockState {
89 signed_header: &untrusted_header.signed_header,
90 validators: &untrusted_header.validator_set,
91 next_validators: None, };
93
94 let options = trusted_client_state.as_light_client_options()?;
95 let verifier = ProdVerifier::default();
96
97 let verdict = verifier.verify_update_header(
98 untrusted_state,
99 trusted_state,
100 &options,
101 HI::get_block_timestamp(&state).await?,
102 );
103
104 match verdict {
105 Verdict::Success => Ok(()),
106 Verdict::NotEnoughTrust(voting_power_tally) => Err(anyhow::anyhow!(
107 "not enough trust, voting power tally: {:?}",
108 voting_power_tally
109 )),
110 Verdict::Invalid(detail) => Err(anyhow::anyhow!(
111 "could not verify tendermint header: invalid: {:?}",
112 detail
113 )),
114 }?;
115
116 let trusted_header = untrusted_header;
117
118 let client_state = state
120 .get_client_state(&self.client_id)
121 .await
122 .context("unable to get client state")?;
123
124 let (next_tm_client_state, next_tm_consensus_state) = state
126 .next_tendermint_state(
127 self.client_id.clone(),
128 client_state.clone(),
129 trusted_header.clone(),
130 )
131 .await;
132
133 state.put_client(&self.client_id, next_tm_client_state);
135 state
136 .put_verified_consensus_state::<HI>(
137 trusted_header.height(),
138 self.client_id.clone(),
139 next_tm_consensus_state,
140 )
141 .await?;
142
143 state.record(
144 UpdateClient {
145 client_id: self.client_id.clone(),
146 client_type: ibc_types::core::client::ClientType(
147 TENDERMINT_CLIENT_TYPE.to_string(),
148 ), consensus_height: trusted_header.height(),
150 header:
151 <ibc_types::lightclients::tendermint::header::Header as ibc_proto::Protobuf<
152 ibc_proto::ibc::lightclients::tendermint::v1::Header,
153 >>::encode_vec(trusted_header),
154 }
155 .into(),
156 );
157 Ok(())
158 }
159}
160
161fn header_is_tendermint(msg: &MsgUpdateClient) -> anyhow::Result<()> {
162 if ics02_validation::is_tendermint_header_state(&msg.client_message) {
163 Ok(())
164 } else {
165 Err(anyhow::anyhow!("MsgUpdateClient: not a tendermint header"))
166 }
167}
168
169async fn update_is_already_committed<S: StateRead>(
170 state: S,
171 msg: &MsgUpdateClient,
172) -> anyhow::Result<bool> {
173 let untrusted_header = ics02_validation::get_tendermint_header(msg.client_message.clone())?;
174 let client_id = msg.client_id.clone();
175
176 let height = untrusted_header.height();
179 let untrusted_consensus_state = TendermintConsensusState::from(untrusted_header);
180 if let Ok(stored_consensus_state) = state
181 .get_verified_consensus_state(&height, &client_id)
182 .await
183 {
184 let stored_tm_consensus_state = stored_consensus_state;
185
186 Ok(stored_tm_consensus_state == untrusted_consensus_state)
187 } else {
188 Ok(false)
192 }
193}
194
195async fn client_is_not_expired<S: StateRead, HI: HostInterface>(
196 state: S,
197 client_id: &ClientId,
198 client_state: &TendermintClientState,
199) -> anyhow::Result<()> {
200 let latest_consensus_state = state
201 .get_verified_consensus_state(&client_state.latest_height(), client_id)
202 .await?;
203
204 let latest_consensus_state_tm = latest_consensus_state;
207
208 let now = HI::get_block_timestamp(&state).await?;
209 let time_elapsed = now.duration_since(latest_consensus_state_tm.timestamp)?;
210
211 if client_state.expired(time_elapsed) {
212 Err(anyhow::anyhow!("client is expired"))
213 } else {
214 Ok(())
215 }
216}
217
218async fn client_is_present<S: StateRead>(
219 state: S,
220 msg: &MsgUpdateClient,
221) -> anyhow::Result<TendermintClientState> {
222 state.get_client_type(&msg.client_id).await?;
223
224 state.get_client_state(&msg.client_id).await
225}
226
227fn client_is_not_frozen(client: &TendermintClientState) -> anyhow::Result<()> {
228 if client.is_frozen() {
229 Err(anyhow::anyhow!("client is frozen"))
230 } else {
231 Ok(())
232 }
233}
234
235fn header_revision_matches_client_state(
236 trusted_client_state: &TendermintClientState,
237 untrusted_header: &TendermintHeader,
238) -> anyhow::Result<()> {
239 if untrusted_header.height().revision_number() != trusted_client_state.chain_id.version() {
240 Err(anyhow::anyhow!(
241 "client update revision number does not match client state"
242 ))
243 } else {
244 Ok(())
245 }
246}
247
248fn header_height_is_consistent(untrusted_header: &TendermintHeader) -> anyhow::Result<()> {
249 if untrusted_header.height() <= untrusted_header.trusted_height {
250 Err(anyhow::anyhow!(
251 "client update height is not greater than trusted height"
252 ))
253 } else {
254 Ok(())
255 }
256}
257
258pub fn verify_header_validator_set<'h>(
259 untrusted_header: &'h TendermintHeader,
260 last_trusted_consensus_state: &TendermintConsensusState,
261) -> anyhow::Result<&'h validator::Set> {
262 if untrusted_header.trusted_validator_set.hash()
263 != last_trusted_consensus_state.next_validators_hash
264 {
265 Err(anyhow::anyhow!(
266 "client update validator set hash does not match trusted consensus state"
267 ))
268 } else {
269 Ok(&untrusted_header.trusted_validator_set)
270 }
271}