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
29pub enum ClientStatus {
33 Active,
35 Frozen,
37 Expired,
39 Unknown,
41 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 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 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 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 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 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 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 "penumbra_verified_heights/{client_id}/verified_heights"
220 ),
221 verified_heights,
222 );
223 }
224
225 fn put_penumbra_sdk_consensus_state(
227 &mut self,
228 height: Height,
229 consensus_state: TendermintConsensusState,
230 ) {
231 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_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 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 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 "penumbra_verified_heights/{client_id}/verified_heights"
328 ))
329 .await
330 }
331
332 async fn get_penumbra_sdk_consensus_state(
334 &self,
335 height: Height,
336 ) -> Result<TendermintConsensusState> {
337 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 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 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 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 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 #[tokio::test]
590 async fn test_create_and_update_light_client() -> anyhow::Result<()> {
591 use penumbra_sdk_sct::epoch::Epoch;
592 let mut state = Arc::new(StateDelta::new(()));
597 {
598 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 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 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 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 assert_eq!(state.client_counter().await.unwrap().0, 1);
676
677 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 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 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}