penumbra_sdk_ibc/component/
client.rs

1use core::fmt;
2use std::fmt::Display;
3use std::fmt::Formatter;
4
5use anyhow::{Context, Result};
6use async_trait::async_trait;
7
8use ibc_types::core::client::ClientId;
9use ibc_types::core::client::ClientType;
10use ibc_types::core::client::Height;
11
12use ibc_types::path::{ClientConsensusStatePath, ClientStatePath, ClientTypePath};
13
14use cnidarium::{StateRead, StateWrite};
15use ibc_types::lightclients::tendermint::{
16    client_state::ClientState as TendermintClientState,
17    consensus_state::ConsensusState as TendermintConsensusState,
18    header::Header as TendermintHeader,
19};
20use penumbra_sdk_proto::{StateReadProto, StateWriteProto};
21
22use crate::component::client_counter::{ClientCounter, VerifiedHeights};
23use crate::prefix::MerklePrefixExt;
24use crate::IBC_COMMITMENT_PREFIX;
25
26use super::state_key;
27use super::HostInterface;
28
29/// ClientStatus represents the current status of an IBC client.
30///
31/// https://github.com/cosmos/ibc-go/blob/main/modules/core/exported/client.go#L30
32pub enum ClientStatus {
33    /// Active is a status type of a client. An active client is allowed to be used.
34    Active,
35    /// Frozen is a status type of a client. A frozen client is not allowed to be used.
36    Frozen,
37    /// Expired is a status type of a client. An expired client is not allowed to be used.
38    Expired,
39    /// Unknown indicates there was an error in determining the status of a client.
40    Unknown,
41    /// Unauthorized indicates that the client type is not registered as an allowed client type.
42    Unauthorized,
43}
44
45impl Display for ClientStatus {
46    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
47        match self {
48            ClientStatus::Active => write!(f, "Active"),
49            ClientStatus::Frozen => write!(f, "Frozen"),
50            ClientStatus::Expired => write!(f, "Expired"),
51            ClientStatus::Unknown => write!(f, "Unknown"),
52            ClientStatus::Unauthorized => write!(f, "Unauthorized"),
53        }
54    }
55}
56
57#[async_trait]
58pub(crate) trait Ics2ClientExt: StateWrite {
59    // given an already verified tendermint header, and a trusted tendermint client state, compute
60    // the next client and consensus states.
61    async fn next_tendermint_state(
62        &self,
63        client_id: ClientId,
64        trusted_client_state: TendermintClientState,
65        verified_header: TendermintHeader,
66    ) -> (TendermintClientState, TendermintConsensusState) {
67        let verified_consensus_state = TendermintConsensusState::from(verified_header.clone());
68
69        // if we have a stored consensus state for this height that conflicts, we need to freeze
70        // the client. if it doesn't conflict, we can return early
71        if let Ok(stored_cs_state) = self
72            .get_verified_consensus_state(&verified_header.height(), &client_id)
73            .await
74        {
75            if stored_cs_state == verified_consensus_state {
76                return (trusted_client_state, verified_consensus_state);
77            } else {
78                return (
79                    trusted_client_state
80                        .with_header(verified_header.clone())
81                        .expect("able to add header to client state")
82                        .with_frozen_height(ibc_types::core::client::Height {
83                            revision_number: 0,
84                            revision_height: 1,
85                        }),
86                    verified_consensus_state,
87                );
88            }
89        }
90
91        // check that updates have monotonic timestamps. we may receive client updates that are
92        // disjoint: the header we received and validated may be older than the newest header we
93        // have. In that case, we need to verify that the timestamp is correct. if it isn't, freeze
94        // the client.
95        let next_consensus_state = self
96            .next_verified_consensus_state(&client_id, &verified_header.height())
97            .await
98            .expect("able to get next verified consensus state");
99        let prev_consensus_state = self
100            .prev_verified_consensus_state(&client_id, &verified_header.height())
101            .await
102            .expect("able to get previous verified consensus state");
103
104        // case 1: if we have a verified consensus state previous to this header, verify that this
105        // header's timestamp is greater than or equal to the stored consensus state's timestamp
106        if let Some(prev_state) = prev_consensus_state {
107            if verified_header.signed_header.header().time < prev_state.timestamp {
108                return (
109                    trusted_client_state
110                        .with_header(verified_header.clone())
111                        .expect("able to add header to client state")
112                        .with_frozen_height(ibc_types::core::client::Height {
113                            revision_number: 0,
114                            revision_height: 1,
115                        }),
116                    verified_consensus_state,
117                );
118            }
119        }
120        // case 2: if we have a verified consensus state with higher block height than this header,
121        // verify that this header's timestamp is less than or equal to this header's timestamp.
122        if let Some(next_state) = next_consensus_state {
123            if verified_header.signed_header.header().time > next_state.timestamp {
124                return (
125                    trusted_client_state
126                        .with_header(verified_header.clone())
127                        .expect("able to add header to client state")
128                        .with_frozen_height(ibc_types::core::client::Height {
129                            revision_number: 0,
130                            revision_height: 1,
131                        }),
132                    verified_consensus_state,
133                );
134            }
135        }
136
137        (
138            trusted_client_state
139                .with_header(verified_header.clone())
140                .expect("able to add header to client state"),
141            verified_consensus_state,
142        )
143    }
144}
145
146impl<T: StateWrite + ?Sized> Ics2ClientExt for T {}
147
148#[async_trait]
149pub trait ConsensusStateWriteExt: StateWrite + Sized {
150    async fn put_verified_consensus_state<HI: HostInterface>(
151        &mut self,
152        height: Height,
153        client_id: ClientId,
154        consensus_state: TendermintConsensusState,
155    ) -> Result<()> {
156        self.put(
157            IBC_COMMITMENT_PREFIX
158                .apply_string(ClientConsensusStatePath::new(&client_id, &height).to_string()),
159            consensus_state,
160        );
161
162        let current_height = HI::get_block_height(&self).await?;
163        let revision_number = HI::get_revision_number(&self).await?;
164        let current_time: ibc_types::timestamp::Timestamp =
165            HI::get_block_timestamp(&self).await?.into();
166
167        self.put_proto::<u64>(
168            state_key::client_processed_times(&client_id, &height),
169            current_time.nanoseconds(),
170        );
171
172        self.put(
173            state_key::client_processed_heights(&client_id, &height),
174            ibc_types::core::client::Height::new(revision_number, current_height)?,
175        );
176
177        // update verified heights
178        let mut verified_heights =
179            self.get_verified_heights(&client_id)
180                .await?
181                .unwrap_or(VerifiedHeights {
182                    heights: Vec::new(),
183                });
184
185        verified_heights.heights.push(height);
186
187        self.put_verified_heights(&client_id, verified_heights);
188
189        Ok(())
190    }
191}
192
193impl<T: StateWrite> ConsensusStateWriteExt for T {}
194
195#[async_trait]
196pub trait StateWriteExt: StateWrite + StateReadExt {
197    fn put_client_counter(&mut self, counter: ClientCounter) {
198        self.put("ibc_client_counter".into(), counter);
199    }
200
201    fn put_client(&mut self, client_id: &ClientId, client_state: TendermintClientState) {
202        self.put_proto(
203            IBC_COMMITMENT_PREFIX
204                .apply_string(ibc_types::path::ClientTypePath(client_id.clone()).to_string()),
205            ibc_types::lightclients::tendermint::client_type().to_string(),
206        );
207
208        self.put(
209            IBC_COMMITMENT_PREFIX.apply_string(ClientStatePath(client_id.clone()).to_string()),
210            client_state,
211        );
212    }
213
214    fn put_verified_heights(&mut self, client_id: &ClientId, verified_heights: VerifiedHeights) {
215        self.put(
216            format!(
217                // NOTE: this is an implementation detail of the Penumbra ICS2 implementation, so
218                // it's not in the same path namespace.
219                "penumbra_verified_heights/{client_id}/verified_heights"
220            ),
221            verified_heights,
222        );
223    }
224
225    // returns the ConsensusState for the penumbra chain (this chain) at the given height
226    fn put_penumbra_sdk_consensus_state(
227        &mut self,
228        height: Height,
229        consensus_state: TendermintConsensusState,
230    ) {
231        // NOTE: this is an implementation detail of the Penumbra ICS2 implementation, so
232        // it's not in the same path namespace.
233        self.put(
234            format!("penumbra_consensus_states/{height}"),
235            consensus_state,
236        );
237    }
238}
239
240impl<T: StateWrite + ?Sized> StateWriteExt for T {}
241
242#[async_trait]
243pub trait StateReadExt: StateRead {
244    async fn client_counter(&self) -> Result<ClientCounter> {
245        self.get("ibc_client_counter")
246            .await
247            .map(|counter| counter.unwrap_or(ClientCounter(0)))
248    }
249
250    async fn get_client_type(&self, client_id: &ClientId) -> Result<ClientType> {
251        self.get_proto(
252            &IBC_COMMITMENT_PREFIX.apply_string(ClientTypePath(client_id.clone()).to_string()),
253        )
254        .await?
255        .context(format!("could not find client type for {client_id}"))
256        .map(ClientType::new)
257    }
258
259    async fn get_client_state(&self, client_id: &ClientId) -> Result<TendermintClientState> {
260        let client_state = self
261            .get(
262                &IBC_COMMITMENT_PREFIX.apply_string(ClientStatePath(client_id.clone()).to_string()),
263            )
264            .await?;
265
266        client_state.context(format!("could not find client state for {client_id}"))
267    }
268
269    async fn get_client_status(
270        &self,
271        client_id: &ClientId,
272        current_block_time: tendermint::Time,
273    ) -> ClientStatus {
274        let client_type = self.get_client_type(client_id).await;
275
276        if client_type.is_err() {
277            return ClientStatus::Unknown;
278        }
279
280        // let _client_type = client_type.expect("client type is Ok");
281        // IBC-Go has a check here to see if the client type is allowed.
282        // We don't have a similar allowlist in Penumbra, so we skip that check.
283        // https://github.com/cosmos/ibc-go/blob/main/modules/core/02-client/types/params.go#L34
284
285        let client_state = self.get_client_state(client_id).await;
286
287        if client_state.is_err() {
288            return ClientStatus::Unknown;
289        }
290
291        let client_state = client_state.expect("client state is Ok");
292
293        if client_state.is_frozen() {
294            return ClientStatus::Frozen;
295        }
296
297        // get latest consensus state to check for expiry
298        let latest_consensus_state = self
299            .get_verified_consensus_state(&client_state.latest_height(), client_id)
300            .await;
301
302        if latest_consensus_state.is_err() {
303            // if the client state does not have an associated consensus state for its latest height
304            // then it must be expired
305            return ClientStatus::Expired;
306        }
307
308        let latest_consensus_state = latest_consensus_state.expect("latest consensus state is Ok");
309
310        let time_elapsed = current_block_time.duration_since(latest_consensus_state.timestamp);
311        if time_elapsed.is_err() {
312            return ClientStatus::Unknown;
313        }
314        let time_elapsed = time_elapsed.expect("time elapsed is Ok");
315
316        if client_state.expired(time_elapsed) {
317            return ClientStatus::Expired;
318        }
319
320        ClientStatus::Active
321    }
322
323    async fn get_verified_heights(&self, client_id: &ClientId) -> Result<Option<VerifiedHeights>> {
324        self.get(&format!(
325            // NOTE: this is an implementation detail of the Penumbra ICS2 implementation, so
326            // it's not in the same path namespace.
327            "penumbra_verified_heights/{client_id}/verified_heights"
328        ))
329        .await
330    }
331
332    // returns the ConsensusState for the penumbra chain (this chain) at the given height
333    async fn get_penumbra_sdk_consensus_state(
334        &self,
335        height: Height,
336    ) -> Result<TendermintConsensusState> {
337        // NOTE: this is an implementation detail of the Penumbra ICS2 implementation, so
338        // it's not in the same path namespace.
339        self.get(&format!("penumbra_consensus_states/{height}"))
340            .await?
341            .ok_or_else(|| {
342                anyhow::anyhow!("penumbra consensus state not found for height {height}")
343            })
344    }
345
346    async fn get_verified_consensus_state(
347        &self,
348        height: &Height,
349        client_id: &ClientId,
350    ) -> Result<TendermintConsensusState> {
351        self.get(
352            &IBC_COMMITMENT_PREFIX
353                .apply_string(ClientConsensusStatePath::new(client_id, height).to_string()),
354        )
355        .await?
356        .ok_or_else(|| {
357            anyhow::anyhow!(
358                "counterparty consensus state not found for client {client_id} at height {height}"
359            )
360        })
361    }
362
363    async fn get_client_update_height(
364        &self,
365        client_id: &ClientId,
366        height: &Height,
367    ) -> Result<ibc_types::core::client::Height> {
368        self.get(&state_key::client_processed_heights(client_id, height))
369            .await?
370            .ok_or_else(|| {
371                anyhow::anyhow!(
372                    "client update time not found for client {client_id} at height {height}"
373                )
374            })
375    }
376
377    async fn get_client_update_time(
378        &self,
379        client_id: &ClientId,
380        height: &Height,
381    ) -> Result<ibc_types::timestamp::Timestamp> {
382        let timestamp_nanos = self
383            .get_proto::<u64>(&state_key::client_processed_times(client_id, height))
384            .await?
385            .ok_or_else(|| {
386                anyhow::anyhow!(
387                    "client update time not found for client {client_id} at height {height}"
388                )
389            })?;
390
391        ibc_types::timestamp::Timestamp::from_nanoseconds(timestamp_nanos)
392            .context("invalid client update time")
393    }
394
395    // returns the lowest verified consensus state that is higher than the given height, if it
396    // exists.
397    async fn next_verified_consensus_state(
398        &self,
399        client_id: &ClientId,
400        height: &Height,
401    ) -> Result<Option<TendermintConsensusState>> {
402        let mut verified_heights =
403            self.get_verified_heights(client_id)
404                .await?
405                .unwrap_or(VerifiedHeights {
406                    heights: Vec::new(),
407                });
408
409        // WARNING: load-bearing sort
410        verified_heights.heights.sort();
411
412        if let Some(next_height) = verified_heights
413            .heights
414            .iter()
415            .find(|&verified_height| verified_height > &height)
416        {
417            let next_cons_state = self
418                .get_verified_consensus_state(next_height, client_id)
419                .await?;
420            return Ok(Some(next_cons_state));
421        } else {
422            return Ok(None);
423        }
424    }
425
426    // returns the highest verified consensus state that is lower than the given height, if it
427    // exists.
428    async fn prev_verified_consensus_state(
429        &self,
430        client_id: &ClientId,
431        height: &Height,
432    ) -> Result<Option<TendermintConsensusState>> {
433        let mut verified_heights =
434            self.get_verified_heights(client_id)
435                .await?
436                .unwrap_or(VerifiedHeights {
437                    heights: Vec::new(),
438                });
439
440        // WARNING: load-bearing sort
441        verified_heights.heights.sort();
442
443        if let Some(prev_height) = verified_heights
444            .heights
445            .iter()
446            .find(|&verified_height| verified_height < &height)
447        {
448            let prev_cons_state = self
449                .get_verified_consensus_state(prev_height, client_id)
450                .await?;
451            return Ok(Some(prev_cons_state));
452        } else {
453            return Ok(None);
454        }
455    }
456}
457
458impl<T: StateRead + ?Sized> StateReadExt for T {}
459
460#[cfg(test)]
461mod tests {
462    use base64::prelude::*;
463    use std::sync::Arc;
464
465    use super::*;
466    use cnidarium::{ArcStateDeltaExt, StateDelta};
467    use ibc_types::core::client::msgs::MsgUpdateClient;
468    use ibc_types::{core::client::msgs::MsgCreateClient, DomainType};
469    use penumbra_sdk_sct::component::clock::{EpochManager as _, EpochRead};
470    use std::str::FromStr;
471    use tendermint::Time;
472
473    use crate::component::ibc_action_with_handler::IbcRelayWithHandlers;
474    use crate::component::ClientStateReadExt;
475    use crate::{IbcRelay, StateWriteExt};
476
477    use crate::component::app_handler::{AppHandler, AppHandlerCheck, AppHandlerExecute};
478    use ibc_types::core::channel::msgs::{
479        MsgAcknowledgement, MsgChannelCloseConfirm, MsgChannelCloseInit, MsgChannelOpenAck,
480        MsgChannelOpenConfirm, MsgChannelOpenInit, MsgChannelOpenTry, MsgRecvPacket, MsgTimeout,
481    };
482
483    struct MockHost {}
484
485    #[async_trait]
486    impl HostInterface for MockHost {
487        async fn get_chain_id<S: StateRead>(_state: S) -> Result<String> {
488            Ok("mock_chain_id".to_string())
489        }
490
491        async fn get_revision_number<S: StateRead>(_state: S) -> Result<u64> {
492            Ok(0u64)
493        }
494
495        async fn get_block_height<S: StateRead>(state: S) -> Result<u64> {
496            Ok(state.get_block_height().await?)
497        }
498
499        async fn get_block_timestamp<S: StateRead>(state: S) -> Result<tendermint::Time> {
500            state.get_current_block_timestamp().await
501        }
502    }
503
504    struct MockAppHandler {}
505
506    #[async_trait]
507    impl AppHandlerCheck for MockAppHandler {
508        async fn chan_open_init_check<S: StateRead>(
509            _state: S,
510            _msg: &MsgChannelOpenInit,
511        ) -> Result<()> {
512            Ok(())
513        }
514        async fn chan_open_try_check<S: StateRead>(
515            _state: S,
516            _msg: &MsgChannelOpenTry,
517        ) -> Result<()> {
518            Ok(())
519        }
520        async fn chan_open_ack_check<S: StateRead>(
521            _state: S,
522            _msg: &MsgChannelOpenAck,
523        ) -> Result<()> {
524            Ok(())
525        }
526        async fn chan_open_confirm_check<S: StateRead>(
527            _state: S,
528            _msg: &MsgChannelOpenConfirm,
529        ) -> Result<()> {
530            Ok(())
531        }
532        async fn chan_close_confirm_check<S: StateRead>(
533            _state: S,
534            _msg: &MsgChannelCloseConfirm,
535        ) -> Result<()> {
536            Ok(())
537        }
538        async fn chan_close_init_check<S: StateRead>(
539            _state: S,
540            _msg: &MsgChannelCloseInit,
541        ) -> Result<()> {
542            Ok(())
543        }
544        async fn recv_packet_check<S: StateRead>(_state: S, _msg: &MsgRecvPacket) -> Result<()> {
545            Ok(())
546        }
547        async fn timeout_packet_check<S: StateRead>(_state: S, _msg: &MsgTimeout) -> Result<()> {
548            Ok(())
549        }
550        async fn acknowledge_packet_check<S: StateRead>(
551            _state: S,
552            _msg: &MsgAcknowledgement,
553        ) -> Result<()> {
554            Ok(())
555        }
556    }
557
558    #[async_trait]
559    impl AppHandlerExecute for MockAppHandler {
560        async fn chan_open_init_execute<S: StateWrite>(_state: S, _msg: &MsgChannelOpenInit) {}
561        async fn chan_open_try_execute<S: StateWrite>(_state: S, _msg: &MsgChannelOpenTry) {}
562        async fn chan_open_ack_execute<S: StateWrite>(_state: S, _msg: &MsgChannelOpenAck) {}
563        async fn chan_open_confirm_execute<S: StateWrite>(_state: S, _msg: &MsgChannelOpenConfirm) {
564        }
565        async fn chan_close_confirm_execute<S: StateWrite>(
566            _state: S,
567            _msg: &MsgChannelCloseConfirm,
568        ) {
569        }
570        async fn chan_close_init_execute<S: StateWrite>(_state: S, _msg: &MsgChannelCloseInit) {}
571        async fn recv_packet_execute<S: StateWrite>(_state: S, _msg: &MsgRecvPacket) -> Result<()> {
572            Ok(())
573        }
574        async fn timeout_packet_execute<S: StateWrite>(_state: S, _msg: &MsgTimeout) -> Result<()> {
575            Ok(())
576        }
577        async fn acknowledge_packet_execute<S: StateWrite>(
578            _state: S,
579            _msg: &MsgAcknowledgement,
580        ) -> Result<()> {
581            Ok(())
582        }
583    }
584
585    #[async_trait]
586    impl AppHandler for MockAppHandler {}
587
588    // test that we can create and update a light client.
589    #[tokio::test]
590    async fn test_create_and_update_light_client() -> anyhow::Result<()> {
591        use penumbra_sdk_sct::epoch::Epoch;
592        // create a storage backend for testing
593
594        // TODO(erwan): `apply_default_genesis` is not available here. We need a component
595        // equivalent.
596        let mut state = Arc::new(StateDelta::new(()));
597        {
598            // TODO: this is copied out of App::init_chain, can we put it somewhere else?
599            let mut state_tx = state.try_begin_transaction().unwrap();
600            state_tx.put_block_height(0);
601            state_tx.put_epoch_by_height(
602                0,
603                Epoch {
604                    index: 0,
605                    start_height: 0,
606                },
607            );
608            state_tx.put_epoch_by_height(
609                1,
610                Epoch {
611                    index: 0,
612                    start_height: 0,
613                },
614            );
615            state_tx.apply();
616        }
617
618        // Light client verification is time-dependent.  In practice, the latest
619        // (consensus) time will be delivered in each BeginBlock and written
620        // into the state.  Here, set the block timestamp manually so it's
621        // available to the unit test.
622        let timestamp = Time::parse_from_rfc3339("2022-02-11T17:30:50.425417198Z")?;
623        let mut state_tx = state.try_begin_transaction().unwrap();
624        state_tx.put_block_timestamp(1u64, timestamp);
625        state_tx.put_block_height(1);
626        state_tx.put_ibc_params(crate::params::IBCParameters {
627            ibc_enabled: true,
628            inbound_ics20_transfers_enabled: true,
629            outbound_ics20_transfers_enabled: true,
630        });
631        state_tx.put_epoch_by_height(
632            1,
633            Epoch {
634                index: 0,
635                start_height: 0,
636            },
637        );
638        state_tx.apply();
639
640        // base64 encoded MsgCreateClient that was used to create the currently in-use Stargaze
641        // light client on the cosmos hub:
642        // https://cosmos.bigdipper.live/transactions/13C1ECC54F088473E2925AD497DDCC092101ADE420BC64BADE67D34A75769CE9
643        let msg_create_client_stargaze_raw = BASE64_STANDARD
644            .decode(include_str!("./test/create_client.msg").replace('\n', ""))
645            .unwrap();
646        let msg_create_stargaze_client =
647            MsgCreateClient::decode(msg_create_client_stargaze_raw.as_slice()).unwrap();
648
649        // base64 encoded MsgUpdateClient that was used to issue the first update to the in-use stargaze light client on the cosmos hub:
650        // https://cosmos.bigdipper.live/transactions/24F1E19F218CAF5CA41D6E0B653E85EB965843B1F3615A6CD7BCF336E6B0E707
651        let msg_update_client_stargaze_raw = BASE64_STANDARD
652            .decode(include_str!("./test/update_client_1.msg").replace('\n', ""))
653            .unwrap();
654        let mut msg_update_stargaze_client =
655            MsgUpdateClient::decode(msg_update_client_stargaze_raw.as_slice()).unwrap();
656
657        msg_update_stargaze_client.client_id = ClientId::from_str("07-tendermint-0").unwrap();
658
659        let create_client_action = IbcRelayWithHandlers::<MockAppHandler, MockHost>::new(
660            IbcRelay::CreateClient(msg_create_stargaze_client),
661        );
662        let update_client_action = IbcRelayWithHandlers::<MockAppHandler, MockHost>::new(
663            IbcRelay::UpdateClient(msg_update_stargaze_client),
664        );
665
666        create_client_action.check_stateless(()).await?;
667        create_client_action.check_historical(state.clone()).await?;
668        let mut state_tx = state.try_begin_transaction().unwrap();
669        create_client_action
670            .check_and_execute(&mut state_tx)
671            .await?;
672        state_tx.apply();
673
674        // Check that state reflects +1 client apps registered.
675        assert_eq!(state.client_counter().await.unwrap().0, 1);
676
677        // Now we update the client and confirm that the update landed in state.
678        update_client_action.check_stateless(()).await?;
679        update_client_action.check_historical(state.clone()).await?;
680        let mut state_tx = state.try_begin_transaction().unwrap();
681        update_client_action
682            .check_and_execute(&mut state_tx)
683            .await?;
684        state_tx.apply();
685
686        // We've had one client update, yes. What about second client update?
687        // https://cosmos.bigdipper.live/transactions/ED217D360F51E622859F7B783FEF98BDE3544AA32BBD13C6C77D8D0D57A19FFD
688        let msg_update_second = BASE64_STANDARD
689            .decode(include_str!("./test/update_client_2.msg").replace('\n', ""))
690            .unwrap();
691
692        let mut second_update = MsgUpdateClient::decode(msg_update_second.as_slice()).unwrap();
693        second_update.client_id = ClientId::from_str("07-tendermint-0").unwrap();
694        let second_update_client_action = IbcRelayWithHandlers::<MockAppHandler, MockHost>::new(
695            IbcRelay::UpdateClient(second_update),
696        );
697
698        second_update_client_action.check_stateless(()).await?;
699        second_update_client_action
700            .check_historical(state.clone())
701            .await?;
702        let mut state_tx = state.try_begin_transaction().unwrap();
703        second_update_client_action
704            .check_and_execute(&mut state_tx)
705            .await?;
706        state_tx.apply();
707
708        Ok(())
709    }
710
711    #[tokio::test]
712    /// Check that we're not able to create a client if the IBC component is disabled.
713    async fn test_disabled_ibc_component() -> anyhow::Result<()> {
714        let mut state = Arc::new(StateDelta::new(()));
715        let mut state_tx = state.try_begin_transaction().unwrap();
716        state_tx.put_ibc_params(crate::params::IBCParameters {
717            ibc_enabled: false,
718            inbound_ics20_transfers_enabled: true,
719            outbound_ics20_transfers_enabled: true,
720        });
721
722        let msg_create_client_stargaze_raw = BASE64_STANDARD
723            .decode(include_str!("./test/create_client.msg").replace('\n', ""))
724            .unwrap();
725        let msg_create_stargaze_client =
726            MsgCreateClient::decode(msg_create_client_stargaze_raw.as_slice()).unwrap();
727
728        let create_client_action = IbcRelayWithHandlers::<MockAppHandler, MockHost>::new(
729            IbcRelay::CreateClient(msg_create_stargaze_client),
730        );
731        state_tx.apply();
732
733        create_client_action.check_stateless(()).await?;
734        create_client_action
735            .check_historical(state.clone())
736            .await
737            .expect_err("should not be able to create a client");
738
739        Ok(())
740    }
741}