penumbra_sdk_ibc/component/msg_handler/
connection_open_ack.rs

1use anyhow::{Context, Result};
2use async_trait::async_trait;
3use cnidarium::{StateRead, StateWrite};
4use ibc_types::core::{
5    client::Height,
6    connection::{events, msgs::MsgConnectionOpenAck, ConnectionEnd, Counterparty, State},
7};
8use ibc_types::lightclients::tendermint::client_state::ClientState as TendermintClientState;
9use ibc_types::path::{ClientConsensusStatePath, ClientStatePath, ConnectionPath};
10
11use crate::{
12    component::{
13        client::StateReadExt as _,
14        connection::{StateReadExt as _, StateWriteExt as _},
15        ics02_validation::validate_penumbra_sdk_client_state,
16        proof_verification, HostInterface, MsgHandler,
17    },
18    IBC_COMMITMENT_PREFIX,
19};
20
21#[async_trait]
22impl MsgHandler for MsgConnectionOpenAck {
23    async fn check_stateless<H>(&self) -> Result<()> {
24        Ok(())
25    }
26
27    async fn try_execute<S: StateWrite, AH, HI: HostInterface>(&self, mut state: S) -> Result<()> {
28        tracing::debug!(msg = ?self);
29        // Validate a ConnectionOpenAck message, which is sent to us by a counterparty chain that
30        // has committed a Connection to us expected to be in the TRYOPEN state. Before executing a
31        // ConnectionOpenAck, we must have a prior connection to this chain in the INIT state.
32        //
33        // In order to verify a ConnectionOpenAck, we need to check that the counterparty chain has
34        // committed a _valid_ Penumbra consensus state, that the counterparty chain has committed
35        // the expected Connection to its state (in the TRYOPEN state) with the expected version,
36        // and that the counterparty has committed a correct Penumbra client state to its state.
37        //
38        // Here we are Chain A.
39        // CHAINS:          (A, B)
40        // PRIOR STATE:     (INIT, TRYOPEN)
41        // POSTERIOR STATE: (OPEN, TRYOPEN)
42        //
43        // verify that the consensus height is correct
44        consensus_height_is_correct::<&S, HI>(&state, self).await?;
45
46        // verify that the client state is well formed
47        penumbra_sdk_client_state_is_well_formed::<&S, HI>(&state, self).await?;
48
49        // verify the previous connection that we're ACKing is in the correct state
50        let connection = verify_previous_connection(&state, self).await?;
51
52        // verify that the counterparty committed a TRYOPEN connection with us as the
53        // counterparty
54        let expected_counterparty = Counterparty {
55            client_id: connection.client_id.clone(), // client ID (local)
56            connection_id: Some(self.conn_id_on_a.clone()), // connection ID (local)
57            prefix: IBC_COMMITMENT_PREFIX.clone(),   // commitment prefix (local)
58        };
59
60        // the connection we expect the counterparty to have committed
61        let expected_conn = ConnectionEnd {
62            state: State::TryOpen,
63            client_id: connection.counterparty.client_id.clone(),
64            counterparty: expected_counterparty.clone(),
65            versions: vec![self.version.clone()],
66            delay_period: connection.delay_period,
67        };
68
69        // get the stored client state for the counterparty
70        let trusted_client_state = state.get_client_state(&connection.client_id).await?;
71
72        // check if the client is frozen
73        // TODO: should we also check if the client is expired here?
74        if trusted_client_state.is_frozen() {
75            anyhow::bail!("client is frozen");
76        }
77
78        // get the stored consensus state for the counterparty
79        let trusted_consensus_state = state
80            .get_verified_consensus_state(&self.proofs_height_on_b, &connection.client_id)
81            .await?;
82
83        // PROOF VERIFICATION
84        // 1. verify that the counterparty chain committed the expected_conn to its state
85        tracing::debug!(?trusted_client_state,);
86        tracing::debug!(
87            msg.proofs_height_on_b = ?self.proofs_height_on_b,
88        );
89        tracing::debug!(
90            counterparty_prefix = ?connection.counterparty.prefix,
91        );
92        tracing::debug!(
93            msg.proof_conn_end_on_b = ?self.proof_conn_end_on_b,
94        );
95        tracing::debug!(
96            trusted_consensus_state_root = ?trusted_consensus_state.root,
97        );
98        tracing::debug!(
99            connection_path = %ConnectionPath::new(&self.conn_id_on_b),
100        );
101        tracing::debug!(
102            expected_conn = ?expected_conn,
103        );
104        let conn_end_on_b_proof = self.proof_conn_end_on_b.clone();
105        proof_verification::verify_connection_state(
106            &trusted_client_state,
107            self.proofs_height_on_b,
108            &connection.counterparty.prefix,
109            &conn_end_on_b_proof,
110            &trusted_consensus_state.root,
111            &ConnectionPath::new(&self.conn_id_on_b),
112            &expected_conn,
113        )
114        .context("couldn't verify connection state")?;
115
116        // 2. verify that the counterparty chain committed the correct ClientState (that was
117        //    provided in the msg)
118        let proof_client_state_of_a_on_b = self.proof_client_state_of_a_on_b.clone();
119        let client_state_of_a_on_b: TendermintClientState =
120            self.client_state_of_a_on_b.clone().try_into()?;
121
122        proof_verification::verify_client_full_state(
123            &trusted_client_state,
124            self.proofs_height_on_b,
125            &connection.counterparty.prefix,
126            &proof_client_state_of_a_on_b,
127            &trusted_consensus_state.root,
128            &ClientStatePath::new(&connection.counterparty.client_id),
129            client_state_of_a_on_b,
130        )
131        .context("couldn't verify client state")?;
132
133        let expected_consensus = state
134            .get_penumbra_sdk_consensus_state(self.consensus_height_of_a_on_b)
135            .await?;
136
137        // 3. verify that the counterparty chain stored the correct consensus state of Penumbra at
138        //    the given consensus height
139        let proof_consensus_state_of_a_on_b = self.proof_consensus_state_of_a_on_b.clone();
140        proof_verification::verify_client_consensus_state(
141            &trusted_client_state,
142            self.proofs_height_on_b,
143            &connection.counterparty.prefix,
144            &proof_consensus_state_of_a_on_b,
145            &trusted_consensus_state.root,
146            &ClientConsensusStatePath::new(
147                &connection.counterparty.client_id,
148                &self.consensus_height_of_a_on_b,
149            ),
150            expected_consensus,
151        )
152        .context("couldn't verify client consensus state")?;
153
154        // VERIFICATION SUCCESSFUL. now execute
155
156        let mut connection = state
157            .get_connection(&self.conn_id_on_a)
158            .await
159            .context("should be able to get connection")?
160            .context("missing connection")?;
161
162        // TODO(erwan): reviewer should check that CP is correct pls
163        let prev_counterparty = connection.counterparty;
164        let counterparty = Counterparty {
165            client_id: prev_counterparty.client_id.clone(),
166            connection_id: Some(self.conn_id_on_b.clone()),
167            prefix: prev_counterparty.prefix,
168        };
169        connection.state = State::Open;
170        connection.versions = vec![self.version.clone()];
171        connection.counterparty = counterparty;
172
173        state.update_connection(&self.conn_id_on_a, connection.clone());
174
175        state.record(
176            events::ConnectionOpenAck {
177                conn_id_on_a: self.conn_id_on_a.clone(),
178                client_id_on_a: connection.client_id.clone(),
179                conn_id_on_b: connection
180                    .counterparty
181                    .connection_id
182                    .clone()
183                    .unwrap_or_default(),
184                client_id_on_b: connection.counterparty.client_id,
185            }
186            .into(),
187        );
188
189        Ok(())
190    }
191}
192async fn consensus_height_is_correct<S: StateRead, HI: HostInterface>(
193    state: S,
194    msg: &MsgConnectionOpenAck,
195) -> anyhow::Result<()> {
196    let current_height = Height::new(
197        HI::get_revision_number(&state).await?,
198        HI::get_block_height(&state).await?,
199    )?;
200    if msg.consensus_height_of_a_on_b >= current_height {
201        anyhow::bail!("consensus height is greater than the current block height",);
202    }
203
204    Ok(())
205}
206
207async fn penumbra_sdk_client_state_is_well_formed<S: StateRead, HI: HostInterface>(
208    state: S,
209    msg: &MsgConnectionOpenAck,
210) -> anyhow::Result<()> {
211    let height = HI::get_block_height(&state).await?;
212    let chain_id = HI::get_chain_id(&state).await?;
213    validate_penumbra_sdk_client_state(msg.client_state_of_a_on_b.clone(), &chain_id, height)?;
214
215    Ok(())
216}
217async fn verify_previous_connection<S: StateRead>(
218    state: S,
219    msg: &MsgConnectionOpenAck,
220) -> anyhow::Result<ConnectionEnd> {
221    let connection = state
222        .get_connection(&msg.conn_id_on_a)
223        .await?
224        .ok_or_else(|| {
225            anyhow::anyhow!(
226                "no connection with the specified ID {} exists",
227                msg.conn_id_on_a
228            )
229        })?;
230
231    // see
232    // https://github.com/cosmos/ibc/blob/master/spec/core/ics-003-connection-semantics/README.md
233    //
234    // for this validation logic
235    let state_is_consistent =
236        connection.state_matches(&State::Init) && connection.versions.contains(&msg.version);
237
238    if !state_is_consistent {
239        anyhow::bail!("connection is not in the correct state");
240    } else {
241        Ok(connection)
242    }
243}