penumbra_sdk_ibc/component/msg_handler/
connection_open_try.rs

1use crate::component::{proof_verification, HostInterface};
2use crate::version::pick_connection_version;
3use crate::IBC_COMMITMENT_PREFIX;
4use anyhow::{Context, Result};
5use async_trait::async_trait;
6use cnidarium::{StateRead, StateWrite};
7use ibc_types::lightclients::tendermint::client_state::ClientState as TendermintClientState;
8use ibc_types::path::{ClientConsensusStatePath, ClientStatePath, ConnectionPath};
9use ibc_types::{
10    core::client::Height as IBCHeight,
11    core::connection::{
12        events, msgs::MsgConnectionOpenTry, ConnectionEnd, ConnectionId, Counterparty,
13        State as ConnectionState,
14    },
15};
16
17use crate::component::{
18    client::StateReadExt as _,
19    connection::{StateReadExt as _, StateWriteExt as _},
20    connection_counter::SUPPORTED_VERSIONS,
21    ics02_validation::validate_penumbra_sdk_client_state,
22    MsgHandler,
23};
24
25#[async_trait]
26impl MsgHandler for MsgConnectionOpenTry {
27    async fn check_stateless<H>(&self) -> Result<()> {
28        // basic checks are performed by the ibc-rs crate when deserializing domain types.
29        Ok(())
30    }
31
32    async fn try_execute<S: StateWrite, AH, HI: HostInterface>(&self, mut state: S) -> Result<()> {
33        tracing::debug!(msg = ?self);
34
35        // Validate a ConnectionOpenTry message, which is sent to us by a counterparty chain that
36        // has committed a Connection to us in an INIT state on its chain. Before executing a
37        // ConnectionOpenTry message, we have no knowledge about the connection: our counterparty
38        // is in INIT state, and we are in none state. After executing ConnectionOpenTry, our
39        // counterparty is in INIT state, and we are in TRYOPEN state.
40        //
41        // In order to verify a ConnectionOpenTry, we need to check that the counterparty chain has
42        // committed a _valid_ Penumbra consensus state, that the counterparty chain has committed
43        // the expected Connection to its state (in the INIT state), and that the counterparty has
44        // committed a correct Penumbra client state to its state.
45        //
46        // Here we are Chain B.
47        // CHAINS:          (A, B)
48        // PRIOR STATE:     (INIT, none)
49        // POSTERIOR STATE: (INIT, TRYOPEN)
50        // verify that the consensus height is correct
51
52        consensus_height_is_correct::<&S, HI>(&mut state, self).await?;
53
54        // verify that the client state (which is a Penumbra client) is well-formed for a
55        // penumbra client.
56        penumbra_sdk_client_state_is_well_formed::<&S, HI>(&mut state, self).await?;
57
58        // TODO(erwan): how to handle this with ibc-rs@0.23.0?
59        // if this msg provides a previous_connection_id to resume from, then check that the
60        // provided previous connection ID is valid
61        // let previous_connection = self.check_previous_connection(msg).await?;
62
63        // perform version intersection
64        // let supported_versions = previous_connection
65        //     .map(|c| c.versions().to_vec())
66        //     .unwrap_or_else(|| SUPPORTED_VERSIONS.clone());
67        let supported_versions = SUPPORTED_VERSIONS.clone();
68
69        pick_connection_version(&supported_versions, &self.versions_on_a.clone())?;
70
71        // expected_conn is the conn that we expect to have been committed to on the counterparty
72        // chain
73        let expected_conn = ConnectionEnd {
74            state: ConnectionState::Init,
75            client_id: self.counterparty.client_id.clone(),
76            counterparty: Counterparty {
77                client_id: self.client_id_on_b.clone(),
78                connection_id: None,
79                prefix: IBC_COMMITMENT_PREFIX.clone(),
80            },
81            versions: self.versions_on_a.clone(),
82            delay_period: self.delay_period,
83        };
84
85        // get the stored client state for the counterparty
86        let trusted_client_state = state.get_client_state(&self.client_id_on_b).await?;
87
88        // check if the client is frozen
89        // TODO: should we also check if the client is expired here?
90        if trusted_client_state.is_frozen() {
91            anyhow::bail!("client is frozen");
92        }
93
94        // get the stored consensus state for the counterparty
95        let trusted_consensus_state = state
96            .get_verified_consensus_state(&self.proofs_height_on_a, &self.client_id_on_b)
97            .await?;
98
99        // PROOF VERIFICATION
100        // 1. verify that the counterparty chain committed the expected_conn to its state
101        let proof_conn_end_on_a = self.proof_conn_end_on_a.clone();
102        proof_verification::verify_connection_state(
103            &trusted_client_state,
104            self.proofs_height_on_a,
105            &self.counterparty.prefix,
106            &proof_conn_end_on_a,
107            &trusted_consensus_state.root,
108            &ConnectionPath::new(
109                self.counterparty
110                    .connection_id
111                    .as_ref()
112                    .ok_or_else(|| anyhow::anyhow!("counterparty connection id is not set"))?,
113            ),
114            &expected_conn,
115        )
116        .context("failed to verify connection state")?;
117
118        // 2. verify that the counterparty chain committed the correct ClientState (that was
119        //    provided in the msg)
120        let proof_client_state_of_b_on_a = self.proof_client_state_of_b_on_a.clone();
121
122        let client_state_of_b_on_a: TendermintClientState =
123            self.client_state_of_b_on_a.clone().try_into()?;
124
125        proof_verification::verify_client_full_state(
126            &trusted_client_state,
127            self.proofs_height_on_a,
128            &self.counterparty.prefix,
129            &proof_client_state_of_b_on_a,
130            &trusted_consensus_state.root,
131            &ClientStatePath::new(&self.counterparty.client_id),
132            client_state_of_b_on_a,
133        )
134        .context("couldn't verify client state")?;
135
136        let expected_consensus = state
137            .get_penumbra_sdk_consensus_state(self.consensus_height_of_b_on_a)
138            .await?;
139
140        // 3. verify that the counterparty chain stored the correct consensus state of Penumbra at
141        //    the given consensus height
142        let proof_consensus_state_of_b_on_a = self.proof_consensus_state_of_b_on_a.clone();
143        proof_verification::verify_client_consensus_state(
144            &trusted_client_state,
145            self.proofs_height_on_a,
146            &self.counterparty.prefix,
147            &proof_consensus_state_of_b_on_a,
148            &trusted_consensus_state.root,
149            &ClientConsensusStatePath::new(
150                &self.counterparty.client_id,
151                &self.consensus_height_of_b_on_a,
152            ),
153            expected_consensus,
154        )
155        .context("couldn't verify client consensus state")?;
156
157        // VALIDATION SUCCESSFUL, now execute
158        //
159        // new_conn is the new connection that we will open on this chain
160        let mut new_conn = ConnectionEnd {
161            state: ConnectionState::TryOpen,
162            client_id: self.client_id_on_b.clone(),
163            counterparty: self.counterparty.clone(),
164            versions: self.versions_on_a.clone(),
165            delay_period: self.delay_period,
166        };
167
168        new_conn.versions = vec![pick_connection_version(
169            &SUPPORTED_VERSIONS.to_vec(),
170            &self.versions_on_a.clone(),
171        )?];
172
173        let new_connection_id = ConnectionId::new(
174            state
175                .get_connection_counter()
176                .await
177                .context("unable to get connection counter")?
178                .0,
179        );
180
181        state
182            .put_new_connection(&new_connection_id, new_conn)
183            .await
184            .context("unable to put new connection")?;
185
186        state.record(
187            events::ConnectionOpenTry {
188                conn_id_on_b: new_connection_id.clone(),
189                client_id_on_b: self.client_id_on_b.clone(),
190                conn_id_on_a: self.counterparty.connection_id.clone().unwrap_or_default(),
191                client_id_on_a: self.counterparty.client_id.clone(),
192            }
193            .into(),
194        );
195
196        Ok(())
197    }
198}
199async fn consensus_height_is_correct<S: StateRead, HI: HostInterface>(
200    state: S,
201    msg: &MsgConnectionOpenTry,
202) -> anyhow::Result<()> {
203    let current_height = IBCHeight::new(
204        HI::get_revision_number(&state).await?,
205        HI::get_block_height(&state).await?,
206    )?;
207    if msg.consensus_height_of_b_on_a >= current_height {
208        anyhow::bail!("consensus height is greater than the current block height",);
209    }
210
211    Ok(())
212}
213async fn penumbra_sdk_client_state_is_well_formed<S: StateRead, HI: HostInterface>(
214    state: S,
215    msg: &MsgConnectionOpenTry,
216) -> anyhow::Result<()> {
217    let height = HI::get_block_height(&state).await?;
218    let chain_id = HI::get_chain_id(&state).await?;
219    validate_penumbra_sdk_client_state(msg.client_state_of_b_on_a.clone(), &chain_id, height)?;
220
221    Ok(())
222}