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#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum ClientStatus {
34 Active,
36 Frozen,
38 Expired,
40 Unknown,
42 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 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 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 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 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 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 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 "penumbra_verified_heights/{client_id}/verified_heights"
221 ),
222 verified_heights,
223 );
224 }
225
226 fn put_penumbra_sdk_consensus_state(
228 &mut self,
229 height: Height,
230 consensus_state: TendermintConsensusState,
231 ) {
232 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_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 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 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 "penumbra_verified_heights/{client_id}/verified_heights"
329 ))
330 .await
331 }
332
333 async fn get_penumbra_sdk_consensus_state(
335 &self,
336 height: Height,
337 ) -> Result<TendermintConsensusState> {
338 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 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 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 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 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 #[tokio::test]
591 async fn test_create_and_update_light_client() -> anyhow::Result<()> {
592 use penumbra_sdk_sct::epoch::Epoch;
593 let mut state = Arc::new(StateDelta::new(()));
598 {
599 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 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 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 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 assert_eq!(state.client_counter().await.unwrap().0, 1);
677
678 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 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 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}