layer_climb_core/signing/ibc/
msg.rs

1use crate::{
2    events::{IbcPacket, IbcPacketTimeoutHeight},
3    ibc_types::{
4        IbcChannelId, IbcChannelOrdering, IbcChannelVersion, IbcClientId, IbcConnectionId,
5        IbcPortId,
6    },
7    prelude::*,
8    querier::{
9        abci::AbciProofKind,
10        ibc::{IbcChannelProofs, IbcConnectionProofs},
11    },
12};
13use std::sync::LazyLock;
14
15// hermes connection handshake: https://github.com/informalsystems/hermes/blob/ccd1d907df4853203349057bba200077254bb83d/crates/relayer/src/connection.rs#L566
16// ibc-go connection handshake:
17impl SigningClient {
18    pub async fn ibc_create_client_msg(
19        &self,
20        trusting_period_secs: Option<u64>,
21        remote_querier: &QueryClient,
22    ) -> Result<layer_climb_proto::ibc::client::MsgCreateClient> {
23        let (client_state, consensus_state) = remote_querier
24            .ibc_create_client_consensus_state(trusting_period_secs)
25            .await?;
26
27        Ok(layer_climb_proto::ibc::client::MsgCreateClient {
28            signer: self.addr.to_string(),
29            consensus_state: Some(proto_into_any(&consensus_state)?),
30            client_state: Some(proto_into_any(&client_state)?),
31        })
32    }
33
34    pub async fn ibc_update_client_msg(
35        &self,
36        client_id: &IbcClientId,
37        remote_querier: &QueryClient,
38        trusted_height: Option<layer_climb_proto::RevisionHeight>,
39    ) -> Result<layer_climb_proto::ibc::client::MsgUpdateClient> {
40        // From Go relayer:
41        // > MsgUpdateClient queries for the current client state on dst,
42        // > then queries for the latest and trusted headers on src
43        // > in order to build a MsgUpdateClient message for dst.
44
45        let trusted_height = match trusted_height {
46            None => *self
47                .querier
48                .ibc_client_state(client_id, None)
49                .await?
50                .latest_height
51                .as_ref()
52                .context("missing latest height")?,
53            Some(trusted_height) => trusted_height,
54        };
55
56        remote_querier
57            .wait_until_block_height(trusted_height.revision_height + 1, None)
58            .await?;
59
60        // like "srcHeader" in Go relayer
61        let curr_signed_header = remote_querier.fetch_signed_header(None).await?;
62        let curr_header = curr_signed_header
63            .header
64            .as_ref()
65            .context("missing curr header")?;
66        // like "dstTrustedHeader" in Go relayer
67        let trusted_signed_header = remote_querier
68            .fetch_signed_header(Some(trusted_height.revision_height + 1))
69            .await?;
70        let trusted_header = trusted_signed_header
71            .header
72            .as_ref()
73            .context("missing trusted header")?;
74
75        let validator_set = remote_querier
76            .validator_set(
77                Some(curr_header.height.try_into()?),
78                Some(&curr_header.proposer_address),
79            )
80            .await?;
81        let trusted_validators = remote_querier
82            .validator_set(
83                Some(trusted_header.height.try_into()?),
84                Some(&trusted_header.proposer_address),
85            )
86            .await?;
87
88        let header = layer_climb_proto::ibc::light_client::Header {
89            signed_header: Some(curr_signed_header),
90            trusted_height: Some(trusted_height),
91            validator_set: Some(validator_set),
92            trusted_validators: Some(trusted_validators),
93        };
94
95        Ok(layer_climb_proto::ibc::client::MsgUpdateClient {
96            client_id: client_id.to_string(),
97            signer: self.addr.to_string(),
98            // this is the ibc header
99            // https://github.com/cosmos/relayer/blob/4ed2615217cea7b5e328d3dc2a032bbd8a30df98/relayer/client.go#L372
100            // -> https://github.com/cosmos/relayer/blob/4ed2615217cea7b5e328d3dc2a032bbd8a30df98/relayer/chains/cosmos/tx.go#L762
101            client_message: Some(proto_into_any(&header)?),
102        })
103    }
104
105    pub async fn ibc_open_connection_init_msg(
106        &self,
107        client_id: &IbcClientId,
108        counterparty_client_id: &IbcClientId,
109    ) -> Result<layer_climb_proto::ibc::connection::MsgConnectionOpenInit> {
110        Ok(layer_climb_proto::ibc::connection::MsgConnectionOpenInit {
111            client_id: client_id.to_string(),
112            counterparty: Some(layer_climb_proto::ibc::connection::Counterparty {
113                client_id: counterparty_client_id.to_string(),
114                // Go implementation sets this to empty here: https://github.com/cosmos/ibc-go/blob/bb34919be78550e1a2b2da8ad727889ba6a1fc83/modules/core/03-connection/types/msgs.go#L37
115                connection_id: "".to_string(),
116                prefix: Some(IBC_MERKLE_PREFIX.clone()),
117            }),
118            version: Some(IBC_VERSION.clone()),
119            // just used for "time delayed connections": https://ibc.cosmos.network/v8/ibc/overview/#time-delayed-connections
120            delay_period: 0,
121            signer: self.addr.to_string(),
122        })
123    }
124
125    pub async fn ibc_open_connection_try_msg(
126        &self,
127        client_id: &IbcClientId,
128        counterparty_client_id: &IbcClientId,
129        counterparty_connection_id: &IbcConnectionId,
130        remote_querier: &QueryClient,
131    ) -> Result<layer_climb_proto::ibc::connection::MsgConnectionOpenTry> {
132        let IbcConnectionProofs {
133            proof_height,
134            consensus_height,
135            connection,
136            connection_proof,
137            client_state_proof,
138            consensus_proof,
139            client_state,
140            ..
141        } = remote_querier
142            .ibc_connection_proofs(
143                self.querier
144                    .ibc_client_state(client_id, None)
145                    .await?
146                    .latest_height
147                    .context("missing latest height")?,
148                counterparty_client_id,
149                counterparty_connection_id,
150            )
151            .await?;
152
153        if connection.state() != layer_climb_proto::ibc::connection::State::Init {
154            bail!(
155                "counterparty connection state is not Init, instead it is {:?}",
156                connection.state()
157            );
158        }
159
160        #[allow(deprecated)]
161        Ok(layer_climb_proto::ibc::connection::MsgConnectionOpenTry {
162            client_id: client_id.to_string(),
163            client_state: Some(proto_into_any(&client_state)?),
164            proof_height: Some(proof_height),
165            proof_client: client_state_proof,
166            proof_init: connection_proof,
167            proof_consensus: consensus_proof,
168            consensus_height: Some(consensus_height),
169            counterparty_versions: vec![IBC_VERSION.clone()],
170            counterparty: Some(layer_climb_proto::ibc::connection::Counterparty {
171                client_id: counterparty_client_id.to_string(),
172                connection_id: counterparty_connection_id.to_string(),
173                prefix: Some(IBC_MERKLE_PREFIX.clone()),
174            }),
175            signer: self.addr.to_string(),
176            // hermes doesn't set this field... is it queryable? doesn't seem to be required...
177            host_consensus_state_proof: Vec::new(),
178            // deprecated
179            previous_connection_id: "".to_string(),
180            // just used for "time delayed connections": https://ibc.cosmos.network/v8/ibc/overview/#time-delayed-connections
181            delay_period: 0,
182        })
183    }
184
185    pub async fn ibc_open_connection_ack_msg(
186        &self,
187        client_id: &IbcClientId,
188        counterparty_client_id: &IbcClientId,
189        connection_id: &IbcConnectionId,
190        counterparty_connection_id: &IbcConnectionId,
191        remote_querier: &QueryClient,
192    ) -> Result<layer_climb_proto::ibc::connection::MsgConnectionOpenAck> {
193        let IbcConnectionProofs {
194            query_height,
195            proof_height,
196            consensus_height,
197            connection,
198            connection_proof,
199            client_state_proof,
200            consensus_proof,
201            client_state,
202        } = remote_querier
203            .ibc_connection_proofs(
204                self.querier
205                    .ibc_client_state(client_id, None)
206                    .await?
207                    .latest_height
208                    .context("missing latest height")?,
209                counterparty_client_id,
210                counterparty_connection_id,
211            )
212            .await?;
213
214        if connection.state() != layer_climb_proto::ibc::connection::State::Tryopen {
215            bail!(
216                "counterparty connection state is not TryOpen at height {}, instead it is {:?}",
217                query_height,
218                connection.state(),
219            );
220        }
221
222        #[allow(deprecated)]
223        Ok(layer_climb_proto::ibc::connection::MsgConnectionOpenAck {
224            connection_id: connection_id.to_string(),
225            counterparty_connection_id: counterparty_connection_id.to_string(),
226            client_state: Some(proto_into_any(&client_state)?),
227            proof_height: Some(proof_height),
228            proof_client: client_state_proof,
229            proof_try: connection_proof,
230            proof_consensus: consensus_proof,
231            consensus_height: Some(consensus_height),
232            signer: self.addr.to_string(),
233            version: Some(IBC_VERSION.clone()),
234            // hermes doesn't set this field... is it queryable? doesn't seem to be required...
235            host_consensus_state_proof: Vec::new(),
236        })
237    }
238
239    pub async fn ibc_open_connection_confirm_msg(
240        &self,
241        client_id: &IbcClientId,
242        counterparty_client_id: &IbcClientId,
243        connection_id: &IbcConnectionId,
244        counterparty_connection_id: &IbcConnectionId,
245        remote_querier: &QueryClient,
246    ) -> Result<layer_climb_proto::ibc::connection::MsgConnectionOpenConfirm> {
247        let IbcConnectionProofs {
248            proof_height,
249            connection_proof,
250            ..
251        } = remote_querier
252            .ibc_connection_proofs(
253                self.querier
254                    .ibc_client_state(client_id, None)
255                    .await?
256                    .latest_height
257                    .context("missing latest height")?,
258                counterparty_client_id,
259                counterparty_connection_id,
260            )
261            .await?;
262
263        #[allow(deprecated)]
264        Ok(
265            layer_climb_proto::ibc::connection::MsgConnectionOpenConfirm {
266                connection_id: connection_id.to_string(),
267                proof_ack: connection_proof,
268                proof_height: Some(proof_height),
269                signer: self.addr.to_string(),
270            },
271        )
272    }
273
274    pub fn ibc_open_channel_init_msg(
275        &self,
276        connection_id: &IbcConnectionId,
277        port_id: &IbcPortId,
278        version: &IbcChannelVersion,
279        ordering: IbcChannelOrdering,
280        counterparty_port_id: &IbcPortId,
281    ) -> Result<layer_climb_proto::ibc::channel::MsgChannelOpenInit> {
282        #[allow(deprecated)]
283        Ok(layer_climb_proto::ibc::channel::MsgChannelOpenInit {
284            port_id: port_id.to_string(),
285            channel: Some(layer_climb_proto::ibc::channel::Channel {
286                state: layer_climb_proto::ibc::channel::State::Init as i32,
287                ordering: match ordering {
288                    IbcChannelOrdering::Ordered => {
289                        layer_climb_proto::ibc::channel::Order::Ordered as i32
290                    }
291                    IbcChannelOrdering::Unordered => {
292                        layer_climb_proto::ibc::channel::Order::Unordered as i32
293                    }
294                },
295                counterparty: Some(layer_climb_proto::ibc::channel::Counterparty {
296                    port_id: counterparty_port_id.to_string(),
297                    channel_id: "".to_string(),
298                }),
299                connection_hops: vec![connection_id.to_string()],
300                version: version.to_string(),
301                upgrade_sequence: 0,
302            }),
303            signer: self.addr.to_string(),
304        })
305    }
306
307    #[allow(clippy::too_many_arguments)]
308    pub async fn ibc_open_channel_try_msg(
309        &self,
310        client_id: &IbcClientId,
311        connection_id: &IbcConnectionId,
312        port_id: &IbcPortId,
313        version: &IbcChannelVersion,
314        counterparty_port_id: &IbcPortId,
315        counterparty_channel_id: &IbcChannelId,
316        counterparty_version: &IbcChannelVersion,
317        ordering: IbcChannelOrdering,
318        remote_querier: &QueryClient,
319    ) -> Result<layer_climb_proto::ibc::channel::MsgChannelOpenTry> {
320        let IbcChannelProofs {
321            proof_height,
322            channel_proof,
323            ..
324        } = remote_querier
325            .ibc_channel_proofs(
326                self.querier
327                    .ibc_client_state(client_id, None)
328                    .await?
329                    .latest_height
330                    .context("missing latest height")?,
331                counterparty_channel_id,
332                counterparty_port_id,
333            )
334            .await?;
335
336        #[allow(deprecated)]
337        Ok(layer_climb_proto::ibc::channel::MsgChannelOpenTry {
338            port_id: port_id.to_string(),
339            previous_channel_id: "".to_string(),
340            channel: Some(layer_climb_proto::ibc::channel::Channel {
341                state: layer_climb_proto::ibc::channel::State::Tryopen as i32,
342                ordering: match ordering {
343                    IbcChannelOrdering::Ordered => {
344                        layer_climb_proto::ibc::channel::Order::Ordered as i32
345                    }
346                    IbcChannelOrdering::Unordered => {
347                        layer_climb_proto::ibc::channel::Order::Unordered as i32
348                    }
349                },
350                counterparty: Some(layer_climb_proto::ibc::channel::Counterparty {
351                    port_id: counterparty_port_id.to_string(),
352                    channel_id: counterparty_channel_id.to_string(),
353                }),
354                connection_hops: vec![connection_id.to_string()],
355                version: version.to_string(),
356                upgrade_sequence: 0,
357            }),
358            counterparty_version: counterparty_version.to_string(),
359            proof_init: channel_proof,
360            proof_height: Some(proof_height),
361            signer: self.addr.to_string(),
362        })
363    }
364
365    #[allow(clippy::too_many_arguments)]
366    pub async fn ibc_open_channel_ack_msg(
367        &self,
368        client_id: &IbcClientId,
369        channel_id: &IbcChannelId,
370        port_id: &IbcPortId,
371        counterparty_port_id: &IbcPortId,
372        counterparty_channel_id: &IbcChannelId,
373        counterparty_version: &IbcChannelVersion,
374        remote_querier: &QueryClient,
375    ) -> Result<layer_climb_proto::ibc::channel::MsgChannelOpenAck> {
376        let IbcChannelProofs {
377            proof_height,
378            channel_proof,
379            ..
380        } = remote_querier
381            .ibc_channel_proofs(
382                self.querier
383                    .ibc_client_state(client_id, None)
384                    .await?
385                    .latest_height
386                    .context("missing latest height")?,
387                counterparty_channel_id,
388                counterparty_port_id,
389            )
390            .await?;
391
392        #[allow(deprecated)]
393        Ok(layer_climb_proto::ibc::channel::MsgChannelOpenAck {
394            port_id: port_id.to_string(),
395            channel_id: channel_id.to_string(),
396            counterparty_channel_id: counterparty_channel_id.to_string(),
397            counterparty_version: counterparty_version.to_string(),
398            proof_try: channel_proof,
399            proof_height: Some(proof_height),
400            signer: self.addr.to_string(),
401        })
402    }
403
404    pub async fn ibc_open_channel_confirm_msg(
405        &self,
406        client_id: &IbcClientId,
407        channel_id: &IbcChannelId,
408        port_id: &IbcPortId,
409        counterparty_port_id: &IbcPortId,
410        counterparty_channel_id: &IbcChannelId,
411        remote_querier: &QueryClient,
412    ) -> Result<layer_climb_proto::ibc::channel::MsgChannelOpenConfirm> {
413        let IbcChannelProofs {
414            proof_height,
415            channel_proof,
416            ..
417        } = remote_querier
418            .ibc_channel_proofs(
419                self.querier
420                    .ibc_client_state(client_id, None)
421                    .await?
422                    .latest_height
423                    .context("missing latest height")?,
424                counterparty_channel_id,
425                counterparty_port_id,
426            )
427            .await?;
428
429        #[allow(deprecated)]
430        Ok(layer_climb_proto::ibc::channel::MsgChannelOpenConfirm {
431            port_id: port_id.to_string(),
432            channel_id: channel_id.to_string(),
433            proof_ack: channel_proof,
434            proof_height: Some(proof_height),
435            signer: self.addr.to_string(),
436        })
437    }
438
439    pub async fn ibc_packet_recv_msg(
440        &self,
441        client_id: &IbcClientId,
442        packet: IbcPacket,
443        remote_querier: &QueryClient,
444    ) -> Result<layer_climb_proto::ibc::channel::MsgRecvPacket> {
445        let proof_height = self
446            .querier
447            .ibc_client_state(client_id, None)
448            .await?
449            .latest_height
450            .context("missing latest height")?;
451
452        let query_height = proof_height.revision_height - 1;
453
454        let packet_commitment_store = remote_querier
455            .abci_proof(
456                AbciProofKind::IbcPacketCommitment {
457                    port_id: packet.src_port_id.clone(),
458                    channel_id: packet.src_channel_id.clone(),
459                    sequence: packet.sequence,
460                },
461                Some(query_height),
462            )
463            .await?;
464
465        if packet_commitment_store.value.is_empty() {
466            bail!("packet commitment value is empty");
467        }
468
469        if packet_commitment_store.proof.is_empty() {
470            bail!("packet commitment proof is empty");
471        }
472
473        Ok(layer_climb_proto::ibc::channel::MsgRecvPacket {
474            packet: Some(convert_ibc_packet(&packet)?),
475            proof_commitment: packet_commitment_store.proof,
476            proof_height: Some(proof_height),
477            signer: self.addr.to_string(),
478        })
479    }
480
481    pub async fn ibc_packet_ack_msg(
482        &self,
483        client_id: &IbcClientId,
484        mut packet: IbcPacket,
485        remote_querier: &QueryClient,
486    ) -> Result<layer_climb_proto::ibc::channel::MsgAcknowledgement> {
487        let proof_height = self
488            .querier
489            .ibc_client_state(client_id, None)
490            .await?
491            .latest_height
492            .context("missing latest height")?;
493
494        let query_height = proof_height.revision_height - 1;
495
496        let packet_ack_store = remote_querier
497            .abci_proof(
498                AbciProofKind::IbcPacketAck {
499                    port_id: packet.src_port_id.clone(),
500                    channel_id: packet.src_channel_id.clone(),
501                    sequence: packet.sequence,
502                },
503                Some(query_height),
504            )
505            .await?;
506
507        if packet_ack_store.value.is_empty() {
508            bail!("packet ack value is empty");
509        }
510
511        if packet_ack_store.proof.is_empty() {
512            bail!("packet ack proof is empty");
513        }
514
515        let acknowledgement = packet.ack.take().context("packet ack is missing")?;
516
517        if acknowledgement.is_empty() {
518            bail!("acknowledgement is empty");
519        }
520
521        // the packet has the correct src->dest in terms of trajectory
522        // but it does not reflect the original message, so we need to (re)invert it
523        packet.invert();
524
525        Ok(layer_climb_proto::ibc::channel::MsgAcknowledgement {
526            packet: Some(convert_ibc_packet(&packet)?),
527            acknowledgement,
528            proof_acked: packet_ack_store.proof,
529            proof_height: Some(proof_height),
530            signer: self.addr.to_string(),
531        })
532    }
533}
534
535pub static IBC_VERSION: LazyLock<layer_climb_proto::ibc::connection::Version> =
536    LazyLock::new(|| {
537        layer_climb_proto::ibc::connection::Version {
538            // Go implementation: https://github.com/cosmos/ibc-go/blob/d771177acf66890c9c6f6e5df9a37b8031dbef7d/modules/core/03-connection/types/version.go#L18
539            identifier: "1".to_string(),
540            // Go implementation: https://github.com/cosmos/ibc-go/blob/d771177acf66890c9c6f6e5df9a37b8031dbef7d/modules/core/03-connection/types/version.go#L22
541            features: vec!["ORDER_ORDERED".to_string(), "ORDER_UNORDERED".to_string()],
542        }
543    });
544
545pub static IBC_MERKLE_PREFIX: LazyLock<layer_climb_proto::MerklePrefix> = LazyLock::new(|| {
546    layer_climb_proto::MerklePrefix {
547        // Go implementation: https://github.com/cosmos/ibc-go/blob/d771177acf66890c9c6f6e5df9a37b8031dbef7d/modules/core/03-connection/keeper/keeper.go#L53
548        // -> https://github.com/cosmos/ibc-go/blob/d771177acf66890c9c6f6e5df9a37b8031dbef7d/modules/core/exported/module.go#L5
549        // but also in spec: https://github.com/cosmos/ibc/tree/main/spec/core/ics-003-connection-semantics
550        // > Chains should expose an endpoint to allow relayers to query the connection prefix. If not specified, a default counterpartyPrefix of "ibc" should be used.
551        // and there doesn't seem to be a universal way to query this, so we'll just use the default (hermes does this too)
552        key_prefix: "ibc".as_bytes().to_vec(),
553    }
554});
555
556fn convert_ibc_packet(packet: &IbcPacket) -> Result<layer_climb_proto::ibc::channel::Packet> {
557    Ok(layer_climb_proto::ibc::channel::Packet {
558        sequence: packet.sequence,
559        source_port: packet.src_port_id.to_string(),
560        source_channel: packet.src_channel_id.to_string(),
561        destination_port: packet.dst_port_id.to_string(),
562        destination_channel: packet.dst_channel_id.to_string(),
563        timeout_height: match packet.timeout_height {
564            IbcPacketTimeoutHeight::Revision { revision, height } => {
565                Some(layer_climb_proto::RevisionHeight {
566                    revision_number: revision,
567                    revision_height: height,
568                })
569            }
570            IbcPacketTimeoutHeight::None => None,
571        },
572        timeout_timestamp: packet.timeout_timestamp,
573        data: packet.data.clone().unwrap_or_default(),
574    })
575}