penumbra_sdk_ibc/component/
proof_verification.rs

1use crate::component::client::StateReadExt;
2
3use core::time::Duration;
4use ibc_proto::Protobuf;
5use ibc_types::path::{ClientConsensusStatePath, ClientUpgradePath};
6use ibc_types::DomainType;
7use ibc_types::{
8    core::{
9        channel::{
10            msgs::MsgAcknowledgement, msgs::MsgRecvPacket, msgs::MsgTimeout, ChannelEnd, ChannelId,
11            Packet, PortId,
12        },
13        client::ClientId,
14        client::Height,
15        commitment::{MerklePrefix, MerkleProof, MerkleRoot},
16        connection::ConnectionEnd,
17    },
18    lightclients::tendermint::{
19        client_state::ClientState as TendermintClientState,
20        consensus_state::ConsensusState as TendermintConsensusState,
21    },
22    path::{
23        AckPath, ChannelEndPath, ClientStatePath, CommitmentPath, ConnectionPath, Path,
24        ReceiptPath, SeqRecvPath,
25    },
26};
27
28use async_trait::async_trait;
29use cnidarium::StateRead;
30use num_traits::float::FloatCore;
31use sha2::{Digest, Sha256};
32
33use super::HostInterface;
34
35// NOTE: this is underspecified.
36// using the same implementation here as ibc-go:
37// https://github.com/cosmos/ibc-go/blob/main/modules/core/04-channel/types/packet.go#L19
38// timeout_timestamp + timeout_height.revision_number + timeout_height.revision_height
39// + sha256(data)
40pub fn commit_packet(packet: &Packet) -> Vec<u8> {
41    let mut commit = vec![];
42    commit.extend_from_slice(&packet.timeout_timestamp_on_b.nanoseconds().to_be_bytes());
43    commit.extend_from_slice(
44        &packet
45            .timeout_height_on_b
46            .commitment_revision_number()
47            .to_be_bytes(),
48    );
49    commit.extend_from_slice(
50        &packet
51            .timeout_height_on_b
52            .commitment_revision_height()
53            .to_be_bytes(),
54    );
55    commit.extend_from_slice(&Sha256::digest(&packet.data)[..]);
56
57    Sha256::digest(&commit).to_vec()
58}
59
60// NOTE: this is underspecified.
61// using the same implementation here as ibc-go:
62// https://github.com/cosmos/ibc-go/blob/main/modules/core/04-channel/types/packet.go#L38
63pub fn commit_acknowledgement(ack_data: &[u8]) -> Vec<u8> {
64    Sha256::digest(ack_data).to_vec()
65}
66
67pub fn calculate_block_delay(
68    delay_period_time: &Duration,
69    max_expected_time_per_block: &Duration,
70) -> u64 {
71    if max_expected_time_per_block.is_zero() {
72        return 0;
73    }
74
75    FloatCore::ceil(delay_period_time.as_secs_f64() / max_expected_time_per_block.as_secs_f64())
76        as u64
77}
78
79fn verify_merkle_absence_proof(
80    proof_specs: &[ics23::ProofSpec],
81    prefix: &MerklePrefix,
82    proof: &MerkleProof,
83    root: &MerkleRoot,
84    path: impl Into<Path>,
85) -> anyhow::Result<()> {
86    let merkle_path = prefix.apply(vec![path.into().to_string()]);
87    proof.verify_non_membership(proof_specs, root.clone(), merkle_path)?;
88
89    Ok(())
90}
91
92fn verify_merkle_proof(
93    proof_specs: &[ics23::ProofSpec],
94    prefix: &MerklePrefix,
95    proof: &MerkleProof,
96    root: &MerkleRoot,
97    path: impl Into<Path>,
98    value: Vec<u8>,
99) -> anyhow::Result<()> {
100    let merkle_path = prefix.apply(vec![path.into().to_string()]);
101    tracing::debug!(
102        ?root,
103        ?merkle_path,
104        value = ?hex::encode(&value),
105    );
106    proof.verify_membership(proof_specs, root.clone(), merkle_path, value, 0)?;
107
108    Ok(())
109}
110
111#[async_trait]
112pub trait ClientUpgradeProofVerifier: StateReadExt + Sized {
113    async fn verify_client_upgrade_proof<HI: HostInterface>(
114        &self,
115        client_id: &ClientId,
116        client_state_proof: &MerkleProof,
117        consensus_state_proof: &MerkleProof,
118        upgraded_tm_consensus_state: TendermintConsensusState,
119        upgraded_tm_client_state: TendermintClientState,
120    ) -> anyhow::Result<()> {
121        // get the stored client state for the counterparty
122        let trusted_client_state = self.get_client_state(client_id).await?;
123
124        // Check to see if the upgrade path is set
125        let mut upgrade_path = trusted_client_state.upgrade_path.clone();
126        if upgrade_path.pop().is_none() {
127            anyhow::bail!("upgrade path is not set");
128        };
129
130        let upgrade_path_prefix =
131            MerklePrefix::try_from(upgrade_path.clone().concat().into_bytes()).map_err(|_| {
132                anyhow::anyhow!("couldn't create commitment prefix from client upgrade path")
133            })?;
134
135        // check if the client is frozen
136        if trusted_client_state.is_frozen() {
137            anyhow::bail!("client is frozen");
138        }
139
140        // get the stored consensus state for the counterparty
141        let trusted_consensus_state = self
142            .get_verified_consensus_state(&trusted_client_state.latest_height(), client_id)
143            .await?;
144
145        // check that the client is not expired
146        let now = HI::get_block_timestamp(&self).await?;
147        let time_elapsed = now.duration_since(trusted_consensus_state.timestamp)?;
148
149        if trusted_client_state.expired(time_elapsed) {
150            anyhow::bail!("client is expired");
151        }
152
153        verify_merkle_proof(
154            &trusted_client_state.proof_specs,
155            &upgrade_path_prefix,
156            client_state_proof,
157            &trusted_consensus_state.root,
158            ClientUpgradePath::UpgradedClientState(
159                trusted_client_state.latest_height().revision_height(),
160            ),
161            upgraded_tm_client_state.encode_to_vec(),
162        )?;
163
164        verify_merkle_proof(
165            &trusted_client_state.proof_specs,
166            &upgrade_path_prefix,
167            consensus_state_proof,
168            &trusted_consensus_state.root,
169            ClientUpgradePath::UpgradedClientConsensusState(
170                trusted_client_state.latest_height().revision_height(),
171            ),
172            upgraded_tm_consensus_state.encode_to_vec(),
173        )?;
174
175        Ok(())
176    }
177}
178
179impl<T: StateRead> ClientUpgradeProofVerifier for T {}
180
181#[async_trait]
182pub trait ChannelProofVerifier: StateReadExt {
183    async fn verify_channel_proof(
184        &self,
185        connection: &ConnectionEnd,
186        proof: &MerkleProof,
187        proof_height: &Height,
188        channel_id: &ChannelId,
189        port_id: &PortId,
190        expected_channel: &ChannelEnd,
191    ) -> anyhow::Result<()> {
192        // get the stored client state for the counterparty
193        let trusted_client_state = self.get_client_state(&connection.client_id).await?;
194
195        // check if the client is frozen
196        // TODO: should we also check if the client is expired here?
197        if trusted_client_state.is_frozen() {
198            anyhow::bail!("client is frozen");
199        }
200
201        // get the stored consensus state for the counterparty
202        let trusted_consensus_state = self
203            .get_verified_consensus_state(proof_height, &connection.client_id)
204            .await?;
205
206        trusted_client_state.verify_height(*proof_height)?;
207
208        // TODO: ok to clone this?
209        let value = expected_channel.clone().encode_vec();
210
211        verify_merkle_proof(
212            &trusted_client_state.proof_specs,
213            &connection.counterparty.prefix.clone(),
214            proof,
215            &trusted_consensus_state.root,
216            ChannelEndPath::new(port_id, channel_id),
217            value,
218        )?;
219
220        Ok(())
221    }
222}
223
224impl<T: StateRead> ChannelProofVerifier for T {}
225
226pub fn verify_connection_state(
227    client_state: &TendermintClientState,
228    height: Height,
229    prefix: &MerklePrefix,
230    proof: &MerkleProof,
231    root: &MerkleRoot,
232    conn_path: &ConnectionPath,
233    expected_connection_end: &ConnectionEnd,
234) -> anyhow::Result<()> {
235    client_state.verify_height(height)?;
236
237    // TODO: ok to clone this?
238    let value = expected_connection_end.clone().encode_vec();
239
240    verify_merkle_proof(
241        &client_state.proof_specs,
242        prefix,
243        proof,
244        root,
245        conn_path.clone(),
246        value,
247    )?;
248
249    Ok(())
250}
251
252pub fn verify_client_full_state(
253    client_state: &TendermintClientState,
254    height: Height,
255    prefix: &MerklePrefix,
256    proof: &MerkleProof,
257    root: &MerkleRoot,
258    client_state_path: &ClientStatePath,
259    expected_client_state: TendermintClientState,
260) -> anyhow::Result<()> {
261    client_state.verify_height(height)?;
262
263    let value: Vec<u8> = expected_client_state.encode_to_vec();
264
265    verify_merkle_proof(
266        &client_state.proof_specs,
267        prefix,
268        proof,
269        root,
270        client_state_path.clone(),
271        value,
272    )?;
273
274    Ok(())
275}
276
277pub fn verify_client_consensus_state(
278    client_state: &TendermintClientState,
279    height: Height,
280    prefix: &MerklePrefix,
281    proof: &MerkleProof,
282    root: &MerkleRoot,
283    client_cons_state_path: &ClientConsensusStatePath,
284    expected_consenus_state: TendermintConsensusState,
285) -> anyhow::Result<()> {
286    client_state.verify_height(height)?;
287
288    let value: Vec<u8> = expected_consenus_state.encode_to_vec();
289
290    verify_merkle_proof(
291        &client_state.proof_specs,
292        prefix,
293        proof,
294        root,
295        client_cons_state_path.clone(),
296        value,
297    )?;
298
299    Ok(())
300}
301
302#[async_trait]
303pub trait PacketProofVerifier: StateReadExt + inner::Inner {
304    async fn verify_packet_recv_proof<HI: HostInterface>(
305        &self,
306        connection: &ConnectionEnd,
307        msg: &MsgRecvPacket,
308    ) -> anyhow::Result<()> {
309        let (trusted_client_state, trusted_consensus_state) = self
310            .get_trusted_client_and_consensus_state::<HI>(
311                &connection.client_id,
312                &msg.proof_height_on_a,
313                connection,
314            )
315            .await?;
316
317        let commitment_path = CommitmentPath {
318            port_id: msg.packet.port_on_a.clone(),
319            channel_id: msg.packet.chan_on_a.clone(),
320            sequence: msg.packet.sequence,
321        };
322
323        let commitment_bytes = commit_packet(&msg.packet);
324
325        verify_merkle_proof(
326            &trusted_client_state.proof_specs,
327            &connection.counterparty.prefix.clone(),
328            &msg.proof_commitment_on_a,
329            &trusted_consensus_state.root,
330            commitment_path,
331            commitment_bytes,
332        )?;
333
334        Ok(())
335    }
336
337    async fn verify_packet_ack_proof<HI: HostInterface>(
338        &self,
339        connection: &ConnectionEnd,
340        msg: &MsgAcknowledgement,
341    ) -> anyhow::Result<()> {
342        let (trusted_client_state, trusted_consensus_state) = self
343            .get_trusted_client_and_consensus_state::<HI>(
344                &connection.client_id,
345                &msg.proof_height_on_b,
346                connection,
347            )
348            .await?;
349
350        let ack_path = AckPath {
351            port_id: msg.packet.port_on_b.clone(),
352            channel_id: msg.packet.chan_on_b.clone(),
353            sequence: msg.packet.sequence,
354        };
355
356        let ack_bytes = commit_acknowledgement(&msg.acknowledgement);
357
358        verify_merkle_proof(
359            &trusted_client_state.proof_specs,
360            &connection.counterparty.prefix.clone(),
361            &msg.proof_acked_on_b,
362            &trusted_consensus_state.root,
363            ack_path,
364            ack_bytes,
365        )?;
366
367        Ok(())
368    }
369
370    async fn verify_packet_timeout_proof<HI: HostInterface>(
371        &self,
372        connection: &ConnectionEnd,
373        msg: &MsgTimeout,
374    ) -> anyhow::Result<()> {
375        let (trusted_client_state, trusted_consensus_state) = self
376            .get_trusted_client_and_consensus_state::<HI>(
377                &connection.client_id,
378                &msg.proof_height_on_b,
379                connection,
380            )
381            .await?;
382
383        let seq_bytes = msg.next_seq_recv_on_b.0.to_be_bytes().to_vec();
384        let seq_path = SeqRecvPath(msg.packet.port_on_b.clone(), msg.packet.chan_on_b.clone());
385
386        verify_merkle_proof(
387            &trusted_client_state.proof_specs,
388            &connection.counterparty.prefix.clone(),
389            &msg.proof_unreceived_on_b,
390            &trusted_consensus_state.root,
391            seq_path,
392            seq_bytes,
393        )?;
394
395        Ok(())
396    }
397
398    async fn verify_packet_timeout_absence_proof<HI: HostInterface>(
399        &self,
400        connection: &ConnectionEnd,
401        msg: &MsgTimeout,
402    ) -> anyhow::Result<()> {
403        let (trusted_client_state, trusted_consensus_state) = self
404            .get_trusted_client_and_consensus_state::<HI>(
405                &connection.client_id,
406                &msg.proof_height_on_b,
407                connection,
408            )
409            .await?;
410
411        let receipt_path = ReceiptPath {
412            port_id: msg.packet.port_on_b.clone(),
413            channel_id: msg.packet.chan_on_b.clone(),
414            sequence: msg.packet.sequence,
415        };
416
417        verify_merkle_absence_proof(
418            &trusted_client_state.proof_specs,
419            &connection.counterparty.prefix.clone(),
420            &msg.proof_unreceived_on_b,
421            &trusted_consensus_state.root,
422            receipt_path,
423        )?;
424
425        Ok(())
426    }
427}
428
429impl<T: StateRead> PacketProofVerifier for T {}
430
431mod inner {
432    use crate::component::HostInterface;
433
434    use super::*;
435
436    #[async_trait]
437    pub trait Inner: StateReadExt + Sized {
438        async fn get_trusted_client_and_consensus_state<HI: HostInterface>(
439            &self,
440            client_id: &ClientId,
441            height: &Height,
442            connection: &ConnectionEnd,
443        ) -> anyhow::Result<(TendermintClientState, TendermintConsensusState)> {
444            let trusted_client_state = self.get_client_state(client_id).await?;
445
446            // TODO: should we also check if the client is expired here?
447            if trusted_client_state.is_frozen() {
448                anyhow::bail!("client is frozen");
449            }
450
451            let trusted_consensus_state =
452                self.get_verified_consensus_state(height, client_id).await?;
453
454            let tm_client_state = trusted_client_state;
455
456            tm_client_state.verify_height(*height)?;
457
458            // verify that the delay time has passed (see ICS07 tendermint IBC client spec for
459            // more details)
460            let current_timestamp = HI::get_block_timestamp(&self).await?;
461            let current_height = HI::get_block_height(&self).await?;
462            let processed_height = self.get_client_update_height(client_id, height).await?;
463            let processed_time = self.get_client_update_time(client_id, height).await?;
464
465            // NOTE: hardcoded for now, should probably be a chain parameter.
466            let max_time_per_block = std::time::Duration::from_secs(20);
467
468            let delay_period_time = connection.delay_period;
469            let delay_period_blocks =
470                calculate_block_delay(&delay_period_time, &max_time_per_block);
471
472            TendermintClientState::verify_delay_passed(
473                current_timestamp.into(),
474                Height::new(HI::get_revision_number(self).await?, current_height)?,
475                processed_time,
476                processed_height,
477                delay_period_time,
478                delay_period_blocks,
479            )?;
480
481            Ok((tm_client_state, trusted_consensus_state))
482        }
483    }
484
485    impl<T: StateReadExt> Inner for T {}
486}