penumbra_ibc/component/msg_handler/upgrade_client.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
use anyhow::{Context, Result};
use async_trait::async_trait;
use cnidarium::StateWrite;
use ibc_types::{
core::{
client::{events, msgs::MsgUpgradeClient},
commitment::{MerkleProof, MerkleRoot},
},
lightclients::tendermint::{
client_state::ClientState as TendermintClientState,
consensus_state::ConsensusState as TendermintConsensusState, TrustThreshold,
TENDERMINT_CLIENT_TYPE,
},
timestamp::ZERO_DURATION,
};
use crate::component::{
client::{ConsensusStateWriteExt as _, StateReadExt as _, StateWriteExt as _},
proof_verification::ClientUpgradeProofVerifier,
HostInterface, MsgHandler,
};
static SENTINEL_UPGRADE_ROOT: &str = "sentinel_root";
#[async_trait]
impl MsgHandler for MsgUpgradeClient {
async fn check_stateless<AH>(&self) -> Result<()> {
Ok(())
}
// execute an ibc client upgrade for a counterparty client.
//
// the message being parsed here is initiating an upgrade that allows the counterparty to
// change certain parameters of its client state (such as the chain id), as well as the
// consensus state (the next set of validators).
//
// in order for a client upgrade to be valid, the counterparty must have committed (using the
// trusted, un-upgraded client state) the new client and consensus states to its state tree.
//
// the first consensus state of the upgraded client uses a sentinel root, against which no
// proofs will verify. subsequent client updates, post-upgrade, will provide usable roots.
async fn try_execute<S: StateWrite, AH, HI: HostInterface>(&self, mut state: S) -> Result<()> {
tracing::debug!(msg = ?self);
let upgraded_client_state_tm = TendermintClientState::try_from(self.client_state.clone())
.context("client state is not a Tendermint client state")?;
let upgraded_consensus_state_tm =
TendermintConsensusState::try_from(self.consensus_state.clone())
.context("consensus state is not a Tendermint consensus state")?;
// this part of the IBC spec is sketchy and undocumented.
// we're going to reset some of the upgraded client state fields to their default values before verifying
// the remote's inclusion of the client state; this is what ibc-go does.
//
// note that later on in this function, we only allow the fields that we didn't reset here to change across upgrades.
// this means that there's no danger of a relayer sending an updated state
// with a new trust level and lying to us here, causing us to reconfigure the client with a bogus trust level.
//
// relevant ibc-go code:
// https://github.com/cosmos/ibc-go/blob/main/modules/light-clients/07-tendermint/upgrade.go#L74
// https://github.com/cosmos/ibc-go/blob/2555a7c504a904064d659e4c1a3a74000887f73d/modules/core/02-client/keeper/keeper.go#L552-L564
let mut upgraded_client_state_tm_zeroed_fields = upgraded_client_state_tm.clone();
upgraded_client_state_tm_zeroed_fields.trusting_period = ZERO_DURATION;
upgraded_client_state_tm_zeroed_fields.trust_level = TrustThreshold::ZERO;
upgraded_client_state_tm_zeroed_fields
.allow_update
.after_expiry = false;
upgraded_client_state_tm_zeroed_fields
.allow_update
.after_misbehaviour = false;
upgraded_client_state_tm_zeroed_fields.frozen_height = None;
upgraded_client_state_tm_zeroed_fields.max_clock_drift = ZERO_DURATION;
let proof_consensus_state: MerkleProof = self
.proof_upgrade_consensus_state
.clone()
.try_into()
.context("couldn't decode proof for upgraded consensus state")?;
let proof_client_state: MerkleProof = self
.proof_upgrade_client
.clone()
.try_into()
.context("couldn't decode proof for upgraded client state")?;
state
.verify_client_upgrade_proof::<HI>(
&self.client_id,
&proof_client_state,
&proof_consensus_state,
upgraded_consensus_state_tm.clone(),
upgraded_client_state_tm_zeroed_fields.clone(),
)
.await?;
let old_client_state = state.get_client_state(&self.client_id).await?;
// construct the new client state to be committed to our state. we don't allow the
// trust_level, trusting_period, clock_drift, allow_update, or frozen_height to change
// across upgrades.
//
// NOTE: this client state can differ from the one that was committed on the other chain!
// that is, the other chain *could* commit different trust level, trusting period, etc, but
// we would just ignore it here. should we error instead?
let new_client_state = TendermintClientState::new(
upgraded_client_state_tm.chain_id,
old_client_state.trust_level,
old_client_state.trusting_period,
upgraded_client_state_tm.unbonding_period,
old_client_state.max_clock_drift,
upgraded_client_state_tm.latest_height,
upgraded_client_state_tm.proof_specs,
upgraded_client_state_tm.upgrade_path,
old_client_state.allow_update,
old_client_state.frozen_height,
)?;
let new_consensus_state = TendermintConsensusState::new(
MerkleRoot {
hash: SENTINEL_UPGRADE_ROOT.into(),
},
upgraded_consensus_state_tm.timestamp,
upgraded_consensus_state_tm.next_validators_hash,
);
let latest_height = new_client_state.latest_height();
state.put_client(&self.client_id, new_client_state);
state
.put_verified_consensus_state::<HI>(
latest_height,
self.client_id.clone(),
new_consensus_state,
)
.await?;
state.record(
events::UpgradeClient {
client_id: self.client_id.clone(),
client_type: ibc_types::core::client::ClientType(
TENDERMINT_CLIENT_TYPE.to_string(),
),
consensus_height: latest_height,
}
.into(),
);
Ok(())
}
}