penumbra_ibc/component/msg_handler/
connection_open_ack.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
use anyhow::{Context, Result};
use async_trait::async_trait;
use cnidarium::{StateRead, StateWrite};
use ibc_types::core::{
    client::Height,
    connection::{events, msgs::MsgConnectionOpenAck, ConnectionEnd, Counterparty, State},
};
use ibc_types::lightclients::tendermint::client_state::ClientState as TendermintClientState;
use ibc_types::path::{ClientConsensusStatePath, ClientStatePath, ConnectionPath};

use crate::{
    component::{
        client::StateReadExt as _,
        connection::{StateReadExt as _, StateWriteExt as _},
        ics02_validation::validate_penumbra_client_state,
        proof_verification, HostInterface, MsgHandler,
    },
    IBC_COMMITMENT_PREFIX,
};

#[async_trait]
impl MsgHandler for MsgConnectionOpenAck {
    async fn check_stateless<H>(&self) -> Result<()> {
        Ok(())
    }

    async fn try_execute<S: StateWrite, AH, HI: HostInterface>(&self, mut state: S) -> Result<()> {
        tracing::debug!(msg = ?self);
        // Validate a ConnectionOpenAck message, which is sent to us by a counterparty chain that
        // has committed a Connection to us expected to be in the TRYOPEN state. Before executing a
        // ConnectionOpenAck, we must have a prior connection to this chain in the INIT state.
        //
        // In order to verify a ConnectionOpenAck, we need to check that the counterparty chain has
        // committed a _valid_ Penumbra consensus state, that the counterparty chain has committed
        // the expected Connection to its state (in the TRYOPEN state) with the expected version,
        // and that the counterparty has committed a correct Penumbra client state to its state.
        //
        // Here we are Chain A.
        // CHAINS:          (A, B)
        // PRIOR STATE:     (INIT, TRYOPEN)
        // POSTERIOR STATE: (OPEN, TRYOPEN)
        //
        // verify that the consensus height is correct
        consensus_height_is_correct::<&S, HI>(&state, self).await?;

        // verify that the client state is well formed
        penumbra_client_state_is_well_formed::<&S, HI>(&state, self).await?;

        // verify the previous connection that we're ACKing is in the correct state
        let connection = verify_previous_connection(&state, self).await?;

        // verify that the counterparty committed a TRYOPEN connection with us as the
        // counterparty
        let expected_counterparty = Counterparty {
            client_id: connection.client_id.clone(), // client ID (local)
            connection_id: Some(self.conn_id_on_a.clone()), // connection ID (local)
            prefix: IBC_COMMITMENT_PREFIX.clone(),   // commitment prefix (local)
        };

        // the connection we expect the counterparty to have committed
        let expected_conn = ConnectionEnd {
            state: State::TryOpen,
            client_id: connection.counterparty.client_id.clone(),
            counterparty: expected_counterparty.clone(),
            versions: vec![self.version.clone()],
            delay_period: connection.delay_period,
        };

        // get the stored client state for the counterparty
        let trusted_client_state = state.get_client_state(&connection.client_id).await?;

        // check if the client is frozen
        // TODO: should we also check if the client is expired here?
        if trusted_client_state.is_frozen() {
            anyhow::bail!("client is frozen");
        }

        // get the stored consensus state for the counterparty
        let trusted_consensus_state = state
            .get_verified_consensus_state(&self.proofs_height_on_b, &connection.client_id)
            .await?;

        // PROOF VERIFICATION
        // 1. verify that the counterparty chain committed the expected_conn to its state
        tracing::debug!(?trusted_client_state,);
        tracing::debug!(
            msg.proofs_height_on_b = ?self.proofs_height_on_b,
        );
        tracing::debug!(
            counterparty_prefix = ?connection.counterparty.prefix,
        );
        tracing::debug!(
            msg.proof_conn_end_on_b = ?self.proof_conn_end_on_b,
        );
        tracing::debug!(
            trusted_consensus_state_root = ?trusted_consensus_state.root,
        );
        tracing::debug!(
            connection_path = %ConnectionPath::new(&self.conn_id_on_b),
        );
        tracing::debug!(
            expected_conn = ?expected_conn,
        );
        let conn_end_on_b_proof = self.proof_conn_end_on_b.clone();
        proof_verification::verify_connection_state(
            &trusted_client_state,
            self.proofs_height_on_b,
            &connection.counterparty.prefix,
            &conn_end_on_b_proof,
            &trusted_consensus_state.root,
            &ConnectionPath::new(&self.conn_id_on_b),
            &expected_conn,
        )
        .context("couldn't verify connection state")?;

        // 2. verify that the counterparty chain committed the correct ClientState (that was
        //    provided in the msg)
        let proof_client_state_of_a_on_b = self.proof_client_state_of_a_on_b.clone();
        let client_state_of_a_on_b: TendermintClientState =
            self.client_state_of_a_on_b.clone().try_into()?;

        proof_verification::verify_client_full_state(
            &trusted_client_state,
            self.proofs_height_on_b,
            &connection.counterparty.prefix,
            &proof_client_state_of_a_on_b,
            &trusted_consensus_state.root,
            &ClientStatePath::new(&connection.counterparty.client_id),
            client_state_of_a_on_b,
        )
        .context("couldn't verify client state")?;

        let expected_consensus = state
            .get_penumbra_consensus_state(self.consensus_height_of_a_on_b)
            .await?;

        // 3. verify that the counterparty chain stored the correct consensus state of Penumbra at
        //    the given consensus height
        let proof_consensus_state_of_a_on_b = self.proof_consensus_state_of_a_on_b.clone();
        proof_verification::verify_client_consensus_state(
            &trusted_client_state,
            self.proofs_height_on_b,
            &connection.counterparty.prefix,
            &proof_consensus_state_of_a_on_b,
            &trusted_consensus_state.root,
            &ClientConsensusStatePath::new(
                &connection.counterparty.client_id,
                &self.consensus_height_of_a_on_b,
            ),
            expected_consensus,
        )
        .context("couldn't verify client consensus state")?;

        // VERIFICATION SUCCESSFUL. now execute

        let mut connection = state
            .get_connection(&self.conn_id_on_a)
            .await
            .context("should be able to get connection")?
            .context("missing connection")?;

        // TODO(erwan): reviewer should check that CP is correct pls
        let prev_counterparty = connection.counterparty;
        let counterparty = Counterparty {
            client_id: prev_counterparty.client_id.clone(),
            connection_id: Some(self.conn_id_on_b.clone()),
            prefix: prev_counterparty.prefix,
        };
        connection.state = State::Open;
        connection.versions = vec![self.version.clone()];
        connection.counterparty = counterparty;

        state.update_connection(&self.conn_id_on_a, connection.clone());

        state.record(
            events::ConnectionOpenAck {
                conn_id_on_a: self.conn_id_on_a.clone(),
                client_id_on_a: connection.client_id.clone(),
                conn_id_on_b: connection
                    .counterparty
                    .connection_id
                    .clone()
                    .unwrap_or_default(),
                client_id_on_b: connection.counterparty.client_id,
            }
            .into(),
        );

        Ok(())
    }
}
async fn consensus_height_is_correct<S: StateRead, HI: HostInterface>(
    state: S,
    msg: &MsgConnectionOpenAck,
) -> anyhow::Result<()> {
    let current_height = Height::new(
        HI::get_revision_number(&state).await?,
        HI::get_block_height(&state).await?,
    )?;
    if msg.consensus_height_of_a_on_b >= current_height {
        anyhow::bail!("consensus height is greater than the current block height",);
    }

    Ok(())
}

async fn penumbra_client_state_is_well_formed<S: StateRead, HI: HostInterface>(
    state: S,
    msg: &MsgConnectionOpenAck,
) -> anyhow::Result<()> {
    let height = HI::get_block_height(&state).await?;
    let chain_id = HI::get_chain_id(&state).await?;
    validate_penumbra_client_state(msg.client_state_of_a_on_b.clone(), &chain_id, height)?;

    Ok(())
}
async fn verify_previous_connection<S: StateRead>(
    state: S,
    msg: &MsgConnectionOpenAck,
) -> anyhow::Result<ConnectionEnd> {
    let connection = state
        .get_connection(&msg.conn_id_on_a)
        .await?
        .ok_or_else(|| {
            anyhow::anyhow!(
                "no connection with the specified ID {} exists",
                msg.conn_id_on_a
            )
        })?;

    // see
    // https://github.com/cosmos/ibc/blob/master/spec/core/ics-003-connection-semantics/README.md
    //
    // for this validation logic
    let state_is_consistent =
        connection.state_matches(&State::Init) && connection.versions.contains(&msg.version);

    if !state_is_consistent {
        anyhow::bail!("connection is not in the correct state");
    } else {
        Ok(connection)
    }
}