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