penumbra_sdk_ibc/component/msg_handler/
upgrade_client.rs

1use anyhow::{Context, Result};
2use async_trait::async_trait;
3use cnidarium::StateWrite;
4use ibc_types::{
5    core::{
6        client::{events, msgs::MsgUpgradeClient},
7        commitment::{MerkleProof, MerkleRoot},
8    },
9    lightclients::tendermint::{
10        client_state::ClientState as TendermintClientState,
11        consensus_state::ConsensusState as TendermintConsensusState, TrustThreshold,
12        TENDERMINT_CLIENT_TYPE,
13    },
14    timestamp::ZERO_DURATION,
15};
16
17use crate::component::{
18    client::{ConsensusStateWriteExt as _, StateReadExt as _, StateWriteExt as _},
19    proof_verification::ClientUpgradeProofVerifier,
20    HostInterface, MsgHandler,
21};
22
23static SENTINEL_UPGRADE_ROOT: &str = "sentinel_root";
24
25#[async_trait]
26impl MsgHandler for MsgUpgradeClient {
27    async fn check_stateless<AH>(&self) -> Result<()> {
28        Ok(())
29    }
30
31    // execute an ibc client upgrade for a counterparty client.
32    //
33    // the message being parsed here is initiating an upgrade that allows the counterparty to
34    // change certain parameters of its client state (such as the chain id), as well as the
35    // consensus state (the next set of validators).
36    //
37    // in order for a client upgrade to be valid, the counterparty must have committed (using the
38    // trusted, un-upgraded client state) the new client and consensus states to its state tree.
39    //
40    // the first consensus state of the upgraded client uses a sentinel root, against which no
41    // proofs will verify. subsequent client updates, post-upgrade, will provide usable roots.
42    async fn try_execute<S: StateWrite, AH, HI: HostInterface>(&self, mut state: S) -> Result<()> {
43        tracing::debug!(msg = ?self);
44
45        let upgraded_client_state_tm = TendermintClientState::try_from(self.client_state.clone())
46            .context("client state is not a Tendermint client state")?;
47
48        let upgraded_consensus_state_tm =
49            TendermintConsensusState::try_from(self.consensus_state.clone())
50                .context("consensus state is not a Tendermint consensus state")?;
51
52        // this part of the IBC spec is sketchy and undocumented.
53        // we're going to reset some of the upgraded client state fields to their default values before verifying
54        // the remote's inclusion of the client state; this is what ibc-go does.
55        //
56        // note that later on in this function, we only allow the fields that we didn't reset here to change across upgrades.
57        // this means that there's no danger of a relayer sending an updated state
58        // with a new trust level and lying to us here, causing us to reconfigure the client with a bogus trust level.
59        //
60        // relevant ibc-go code:
61        // https://github.com/cosmos/ibc-go/blob/main/modules/light-clients/07-tendermint/upgrade.go#L74
62        // https://github.com/cosmos/ibc-go/blob/2555a7c504a904064d659e4c1a3a74000887f73d/modules/core/02-client/keeper/keeper.go#L552-L564
63        let mut upgraded_client_state_tm_zeroed_fields = upgraded_client_state_tm.clone();
64        upgraded_client_state_tm_zeroed_fields.trusting_period = ZERO_DURATION;
65        upgraded_client_state_tm_zeroed_fields.trust_level = TrustThreshold::ZERO;
66        upgraded_client_state_tm_zeroed_fields
67            .allow_update
68            .after_expiry = false;
69        upgraded_client_state_tm_zeroed_fields
70            .allow_update
71            .after_misbehaviour = false;
72        upgraded_client_state_tm_zeroed_fields.frozen_height = None;
73        upgraded_client_state_tm_zeroed_fields.max_clock_drift = ZERO_DURATION;
74
75        let proof_consensus_state: MerkleProof = self
76            .proof_upgrade_consensus_state
77            .clone()
78            .try_into()
79            .context("couldn't decode proof for upgraded consensus state")?;
80        let proof_client_state: MerkleProof = self
81            .proof_upgrade_client
82            .clone()
83            .try_into()
84            .context("couldn't decode proof for upgraded client state")?;
85
86        state
87            .verify_client_upgrade_proof::<HI>(
88                &self.client_id,
89                &proof_client_state,
90                &proof_consensus_state,
91                upgraded_consensus_state_tm.clone(),
92                upgraded_client_state_tm_zeroed_fields.clone(),
93            )
94            .await?;
95
96        let old_client_state = state.get_client_state(&self.client_id).await?;
97
98        // construct the new client state to be committed to our state. we don't allow the
99        // trust_level, trusting_period, clock_drift, allow_update, or frozen_height to change
100        // across upgrades.
101        //
102        // NOTE: this client state can differ from the one that was committed on the other chain!
103        // that is, the other chain *could* commit different trust level, trusting period, etc, but
104        // we would just ignore it here. should we error instead?
105        let new_client_state = TendermintClientState::new(
106            upgraded_client_state_tm.chain_id,
107            old_client_state.trust_level,
108            old_client_state.trusting_period,
109            upgraded_client_state_tm.unbonding_period,
110            old_client_state.max_clock_drift,
111            upgraded_client_state_tm.latest_height,
112            upgraded_client_state_tm.proof_specs,
113            upgraded_client_state_tm.upgrade_path,
114            old_client_state.allow_update,
115            old_client_state.frozen_height,
116        )?;
117
118        let new_consensus_state = TendermintConsensusState::new(
119            MerkleRoot {
120                hash: SENTINEL_UPGRADE_ROOT.into(),
121            },
122            upgraded_consensus_state_tm.timestamp,
123            upgraded_consensus_state_tm.next_validators_hash,
124        );
125
126        let latest_height = new_client_state.latest_height();
127
128        state.put_client(&self.client_id, new_client_state);
129        state
130            .put_verified_consensus_state::<HI>(
131                latest_height,
132                self.client_id.clone(),
133                new_consensus_state,
134            )
135            .await?;
136
137        state.record(
138            events::UpgradeClient {
139                client_id: self.client_id.clone(),
140                client_type: ibc_types::core::client::ClientType(
141                    TENDERMINT_CLIENT_TYPE.to_string(),
142                ),
143                consensus_height: latest_height,
144            }
145            .into(),
146        );
147
148        Ok(())
149    }
150}