penumbra_sdk_ibc/component/msg_handler/
update_client.rs

1use 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        // Optimization: no-op if the update is already committed.  We no-op
35        // to Ok(()) rather than erroring to avoid having two "racing" relay
36        // transactions fail just because they both contain the same client
37        // update.
38        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        // The (still untrusted) header uses the `trusted_height` field to
58        // specify the trusted anchor data it is extending.
59        let trusted_height = untrusted_header.trusted_height;
60
61        // We use the specified trusted height to query the trusted
62        // consensus state the update extends.
63        let last_trusted_consensus_state = state
64            .get_verified_consensus_state(&trusted_height, &self.client_id)
65            .await?;
66
67        // We also have to convert from an IBC height, which has two
68        // components, to a Tendermint height, which has only one.
69        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        // Now we build the trusted and untrusted states to feed to the Tendermint light client.
78
79        let trusted_state = TrustedBlockState {
80            // TODO(erwan): do we need an additional check on `chain_id`
81            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, // TODO: do we need this?
92        };
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        // get the latest client state
119        let client_state = state
120            .get_client_state(&self.client_id)
121            .await
122            .context("unable to get client state")?;
123
124        // NOTE: next_tendermint_state will freeze the client on equivocation.
125        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        // store the updated client and consensus states
134        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                ), // TODO: hardcoded
149                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    // check if we already have a consensus state for this height, if we do, check that it is
177    // the same as this update, if it is, return early.
178    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        // If we don't have a consensus state for this height for
189        // whatever reason (either missing or a DB error), we don't
190        // consider it an error, it's just not already committed.
191        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    // TODO(erwan): for now there is no casting that needs to happen because `get_verified_consensus_state` does not return an
205    // abstracted consensus state.
206    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}