diff --git a/.changelog/unreleased/improvements/1097-detach-client-state-verifier.md b/.changelog/unreleased/improvements/1097-detach-client-state-verifier.md new file mode 100644 index 000000000..35203b08c --- /dev/null +++ b/.changelog/unreleased/improvements/1097-detach-client-state-verifier.md @@ -0,0 +1,3 @@ +- [ibc-client-tendermint] Detach client state verifier such that users have a + way to utilize custom verifiers + ([\#1097](https://github.com/cosmos/ibc-rs/pull/1097)) diff --git a/.github/workflows/no-std.yaml b/.github/workflows/no-std.yaml index bec26c15b..90a3b8e09 100644 --- a/.github/workflows/no-std.yaml +++ b/.github/workflows/no-std.yaml @@ -48,7 +48,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: - toolchain: nightly + toolchain: nightly-2024-02-24 target: wasm32-unknown-unknown override: true - run: | diff --git a/ci/no-std-check/Makefile b/ci/no-std-check/Makefile index 48fc0dfa0..6702a89a4 100644 --- a/ci/no-std-check/Makefile +++ b/ci/no-std-check/Makefile @@ -1,4 +1,4 @@ -NIGHTLY_VERSION=nightly +NIGHTLY_VERSION=nightly-2024-02-24 .DEFAULT_GOAL := help diff --git a/ibc-clients/ics07-tendermint/Cargo.toml b/ibc-clients/ics07-tendermint/Cargo.toml index b1727305e..f7da34cae 100644 --- a/ibc-clients/ics07-tendermint/Cargo.toml +++ b/ibc-clients/ics07-tendermint/Cargo.toml @@ -18,7 +18,8 @@ all-features = true [dependencies] # external dependencies -serde = { workspace = true, optional = true } +derive_more = { workspace = true } +serde = { workspace = true, optional = true } # ibc dependencies ibc-client-tendermint-types = { workspace = true } diff --git a/ibc-clients/ics07-tendermint/src/client_state.rs b/ibc-clients/ics07-tendermint/src/client_state.rs index 88b00d533..bb9415487 100644 --- a/ibc-clients/ics07-tendermint/src/client_state.rs +++ b/ibc-clients/ics07-tendermint/src/client_state.rs @@ -10,45 +10,31 @@ use ibc_client_tendermint_types::error::Error; use ibc_client_tendermint_types::proto::v1::ClientState as RawTmClientState; -use ibc_client_tendermint_types::{ - client_type as tm_client_type, ClientState as ClientStateType, - ConsensusState as ConsensusStateType, Header as TmHeader, Misbehaviour as TmMisbehaviour, - TENDERMINT_HEADER_TYPE_URL, TENDERMINT_MISBEHAVIOUR_TYPE_URL, -}; -use ibc_core_client::context::client_state::{ - ClientStateCommon, ClientStateExecution, ClientStateValidation, -}; -use ibc_core_client::context::consensus_state::ConsensusState; -use ibc_core_client::context::{ClientExecutionContext, ClientValidationContext}; -use ibc_core_client::types::error::{ClientError, UpgradeClientError}; -use ibc_core_client::types::{Height, Status}; -use ibc_core_commitment_types::commitment::{ - CommitmentPrefix, CommitmentProofBytes, CommitmentRoot, -}; -use ibc_core_commitment_types::merkle::{apply_prefix, MerkleProof}; -use ibc_core_host::types::identifiers::{ClientId, ClientType}; -use ibc_core_host::types::path::{ - ClientConsensusStatePath, ClientStatePath, Path, UpgradeClientPath, -}; -use ibc_core_host::ExecutionContext; +use ibc_client_tendermint_types::ClientState as ClientStateType; +use ibc_core_client::types::error::ClientError; use ibc_primitives::prelude::*; use ibc_primitives::proto::{Any, Protobuf}; -use ibc_primitives::ToVec; -use super::consensus_state::ConsensusState as TmConsensusState; -use crate::context::{ - CommonContext, ExecutionContext as TmExecutionContext, ValidationContext as TmValidationContext, -}; +use crate::context::ValidationContext as TmValidationContext; +mod common; +mod execution; mod misbehaviour; mod update_client; +mod validation; + +pub use common::*; +pub use execution::*; +pub use misbehaviour::*; +pub use update_client::*; +pub use validation::*; /// Newtype wrapper around the `ClientState` type imported from the /// `ibc-client-tendermint-types` crate. This wrapper exists so that we can /// bypass Rust's orphan rules and implement traits from /// `ibc::core::client::context` on the `ClientState` type. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, derive_more::From)] pub struct ClientState(ClientStateType); impl ClientState { @@ -57,12 +43,6 @@ impl ClientState { } } -impl From for ClientState { - fn from(client_state: ClientStateType) -> Self { - Self(client_state) - } -} - impl Protobuf for ClientState {} impl TryFrom for ClientState { @@ -95,438 +75,6 @@ impl From for Any { } } -impl ClientStateCommon for ClientState { - fn verify_consensus_state(&self, consensus_state: Any) -> Result<(), ClientError> { - let tm_consensus_state = TmConsensusState::try_from(consensus_state)?; - if tm_consensus_state.root().is_empty() { - return Err(ClientError::Other { - description: "empty commitment root".into(), - }); - }; - - Ok(()) - } - - fn client_type(&self) -> ClientType { - tm_client_type() - } - - fn latest_height(&self) -> Height { - self.0.latest_height - } - - fn validate_proof_height(&self, proof_height: Height) -> Result<(), ClientError> { - if self.latest_height() < proof_height { - return Err(ClientError::InvalidProofHeight { - latest_height: self.latest_height(), - proof_height, - }); - } - Ok(()) - } - - /// Perform client-specific verifications and check all data in the new - /// client state to be the same across all valid Tendermint clients for the - /// new chain. - /// - /// You can learn more about how to upgrade IBC-connected SDK chains in - /// [this](https://ibc.cosmos.network/main/ibc/upgrades/quick-guide.html) - /// guide - fn verify_upgrade_client( - &self, - upgraded_client_state: Any, - upgraded_consensus_state: Any, - proof_upgrade_client: CommitmentProofBytes, - proof_upgrade_consensus_state: CommitmentProofBytes, - root: &CommitmentRoot, - ) -> Result<(), ClientError> { - // Make sure that the client type is of Tendermint type `ClientState` - let upgraded_tm_client_state = Self::try_from(upgraded_client_state.clone())?; - - // Make sure that the consensus type is of Tendermint type `ConsensusState` - TmConsensusState::try_from(upgraded_consensus_state.clone())?; - - // Make sure the latest height of the current client is not greater then - // the upgrade height This condition checks both the revision number and - // the height - if self.latest_height() >= upgraded_tm_client_state.0.latest_height { - return Err(UpgradeClientError::LowUpgradeHeight { - upgraded_height: self.latest_height(), - client_height: upgraded_tm_client_state.0.latest_height, - })?; - } - - // Check to see if the upgrade path is set - let mut upgrade_path = self.0.upgrade_path.clone(); - if upgrade_path.pop().is_none() { - return Err(ClientError::ClientSpecific { - description: "cannot upgrade client as no upgrade path has been set".to_string(), - }); - }; - - let upgrade_path_prefix = CommitmentPrefix::try_from(upgrade_path[0].clone().into_bytes()) - .map_err(ClientError::InvalidCommitmentProof)?; - - let last_height = self.latest_height().revision_height(); - - // Verify the proof of the upgraded client state - self.verify_membership( - &upgrade_path_prefix, - &proof_upgrade_client, - root, - Path::UpgradeClient(UpgradeClientPath::UpgradedClientState(last_height)), - upgraded_client_state.to_vec(), - )?; - - // Verify the proof of the upgraded consensus state - self.verify_membership( - &upgrade_path_prefix, - &proof_upgrade_consensus_state, - root, - Path::UpgradeClient(UpgradeClientPath::UpgradedClientConsensusState(last_height)), - upgraded_consensus_state.to_vec(), - )?; - - Ok(()) - } - - fn verify_membership( - &self, - prefix: &CommitmentPrefix, - proof: &CommitmentProofBytes, - root: &CommitmentRoot, - path: Path, - value: Vec, - ) -> Result<(), ClientError> { - let merkle_path = apply_prefix(prefix, vec![path.to_string()]); - let merkle_proof = - MerkleProof::try_from(proof).map_err(ClientError::InvalidCommitmentProof)?; - - merkle_proof - .verify_membership( - &self.0.proof_specs, - root.clone().into(), - merkle_path, - value, - 0, - ) - .map_err(ClientError::Ics23Verification) - } - - fn verify_non_membership( - &self, - prefix: &CommitmentPrefix, - proof: &CommitmentProofBytes, - root: &CommitmentRoot, - path: Path, - ) -> Result<(), ClientError> { - let merkle_path = apply_prefix(prefix, vec![path.to_string()]); - let merkle_proof = - MerkleProof::try_from(proof).map_err(ClientError::InvalidCommitmentProof)?; - - merkle_proof - .verify_non_membership(&self.0.proof_specs, root.clone().into(), merkle_path) - .map_err(ClientError::Ics23Verification) - } -} - -impl ClientStateValidation for ClientState -where - V: ClientValidationContext + TmValidationContext, - V::AnyConsensusState: TryInto, - ClientError: From<>::Error>, -{ - fn verify_client_message( - &self, - ctx: &V, - client_id: &ClientId, - client_message: Any, - ) -> Result<(), ClientError> { - match client_message.type_url.as_str() { - TENDERMINT_HEADER_TYPE_URL => { - let header = TmHeader::try_from(client_message)?; - self.verify_header(ctx, client_id, header) - } - TENDERMINT_MISBEHAVIOUR_TYPE_URL => { - let misbehaviour = TmMisbehaviour::try_from(client_message)?; - self.verify_misbehaviour(ctx, client_id, misbehaviour) - } - _ => Err(ClientError::InvalidUpdateClientMessage), - } - } - - /// Namely the following cases are covered: - /// - /// 1 - fork: - /// Assumes at least one consensus state before the fork point exists. Let - /// existing consensus states on chain B be: [Sn,.., Sf, Sf-1, S0] with - /// `Sf-1` being the most recent state before fork. Chain A is queried for a - /// header `Hf'` at `Sf.height` and if it is different than the `Hf` in the - /// event for the client update (the one that has generated `Sf` on chain), - /// then the two headers are included in the evidence and submitted. Note - /// that in this case the headers are different but have the same height. - /// - /// 2 - BFT time violation for unavailable header (a.k.a. Future Lunatic - /// Attack or FLA): - /// Some header with a height that is higher than the latest height on A has - /// been accepted and a consensus state was created on B. Note that this - /// implies that the timestamp of this header must be within the - /// `clock_drift` of the client. Assume the client on B has been updated - /// with `h2`(not present on/ produced by chain A) and it has a timestamp of - /// `t2` that is at most `clock_drift` in the future. Then the latest header - /// from A is fetched, let it be `h1`, with a timestamp of `t1`. If `t1 >= - /// t2` then evidence of misbehavior is submitted to A. - /// - /// 3 - BFT time violation for existing headers: - /// Ensure that consensus state times are monotonically increasing with - /// height. - fn check_for_misbehaviour( - &self, - ctx: &V, - client_id: &ClientId, - client_message: Any, - ) -> Result { - match client_message.type_url.as_str() { - TENDERMINT_HEADER_TYPE_URL => { - let header = TmHeader::try_from(client_message)?; - self.check_for_misbehaviour_update_client(ctx, client_id, header) - } - TENDERMINT_MISBEHAVIOUR_TYPE_URL => { - let misbehaviour = TmMisbehaviour::try_from(client_message)?; - self.check_for_misbehaviour_misbehavior(&misbehaviour) - } - _ => Err(ClientError::InvalidUpdateClientMessage), - } - } - - fn status(&self, ctx: &V, client_id: &ClientId) -> Result { - if self.0.is_frozen() { - return Ok(Status::Frozen); - } - - let latest_consensus_state: TmConsensusState = { - let any_latest_consensus_state = - match ctx.consensus_state(&ClientConsensusStatePath::new( - client_id.clone(), - self.0.latest_height.revision_number(), - self.0.latest_height.revision_height(), - )) { - Ok(cs) => cs, - // if the client state does not have an associated consensus state for its latest height - // then it must be expired - Err(_) => return Ok(Status::Expired), - }; - - any_latest_consensus_state.try_into()? - }; - - // Note: if the `duration_since()` is `None`, indicating that the latest - // consensus state is in the future, then we don't consider the client - // to be expired. - let now = ctx.host_timestamp()?; - if let Some(elapsed_since_latest_consensus_state) = - now.duration_since(&latest_consensus_state.timestamp().into()) - { - if elapsed_since_latest_consensus_state > self.0.trusting_period { - return Ok(Status::Expired); - } - } - - Ok(Status::Active) - } -} - -impl ClientStateExecution for ClientState -where - E: TmExecutionContext + ExecutionContext, - ::AnyClientState: From, - ::AnyConsensusState: From, -{ - fn initialise( - &self, - ctx: &mut E, - client_id: &ClientId, - consensus_state: Any, - ) -> Result<(), ClientError> { - let host_timestamp = CommonContext::host_timestamp(ctx)?; - let host_height = CommonContext::host_height(ctx)?; - - let tm_consensus_state = TmConsensusState::try_from(consensus_state)?; - - ctx.store_client_state(ClientStatePath::new(client_id.clone()), self.clone().into())?; - ctx.store_consensus_state( - ClientConsensusStatePath::new( - client_id.clone(), - self.0.latest_height.revision_number(), - self.0.latest_height.revision_height(), - ), - tm_consensus_state.into(), - )?; - ctx.store_update_meta( - client_id.clone(), - self.latest_height(), - host_timestamp, - host_height, - )?; - - Ok(()) - } - - fn update_state( - &self, - ctx: &mut E, - client_id: &ClientId, - header: Any, - ) -> Result, ClientError> { - let header = TmHeader::try_from(header)?; - let header_height = header.height(); - - self.prune_oldest_consensus_state(ctx, client_id)?; - - let maybe_existing_consensus_state = { - let path_at_header_height = ClientConsensusStatePath::new( - client_id.clone(), - header_height.revision_number(), - header_height.revision_height(), - ); - - CommonContext::consensus_state(ctx, &path_at_header_height).ok() - }; - - if maybe_existing_consensus_state.is_some() { - // if we already had the header installed by a previous relayer - // then this is a no-op. - // - // Do nothing. - } else { - let host_timestamp = CommonContext::host_timestamp(ctx)?; - let host_height = CommonContext::host_height(ctx)?; - - let new_consensus_state = ConsensusStateType::from(header.clone()); - let new_client_state = self.0.clone().with_header(header)?; - - ctx.store_consensus_state( - ClientConsensusStatePath::new( - client_id.clone(), - header_height.revision_number(), - header_height.revision_height(), - ), - TmConsensusState::from(new_consensus_state).into(), - )?; - ctx.store_client_state( - ClientStatePath::new(client_id.clone()), - ClientState::from(new_client_state).into(), - )?; - ctx.store_update_meta( - client_id.clone(), - header_height, - host_timestamp, - host_height, - )?; - } - - Ok(vec![header_height]) - } - - fn update_state_on_misbehaviour( - &self, - ctx: &mut E, - client_id: &ClientId, - _client_message: Any, - ) -> Result<(), ClientError> { - // NOTE: frozen height is set to `Height {revision_height: 0, - // revision_number: 1}` and it is the same for all misbehaviour. This - // aligns with the - // [`ibc-go`](https://github.com/cosmos/ibc-go/blob/0e3f428e66d6fc0fc6b10d2f3c658aaa5000daf7/modules/light-clients/07-tendermint/misbehaviour.go#L18-L19) - // implementation. - let frozen_client_state = self.0.clone().with_frozen_height(Height::min(0)); - - let wrapped_frozen_client_state = ClientState::from(frozen_client_state); - - ctx.store_client_state( - ClientStatePath::new(client_id.clone()), - wrapped_frozen_client_state.into(), - )?; - - Ok(()) - } - - // Commit the new client state and consensus state to the store - fn update_state_on_upgrade( - &self, - ctx: &mut E, - client_id: &ClientId, - upgraded_client_state: Any, - upgraded_consensus_state: Any, - ) -> Result { - let mut upgraded_tm_client_state = Self::try_from(upgraded_client_state)?; - let upgraded_tm_cons_state = TmConsensusState::try_from(upgraded_consensus_state)?; - - upgraded_tm_client_state.0.zero_custom_fields(); - - // Construct new client state and consensus state relayer chosen client - // parameters are ignored. All chain-chosen parameters come from - // committed client, all client-chosen parameters come from current - // client. - let new_client_state = ClientStateType::new( - upgraded_tm_client_state.0.chain_id, - self.0.trust_level, - self.0.trusting_period, - upgraded_tm_client_state.0.unbonding_period, - self.0.max_clock_drift, - upgraded_tm_client_state.0.latest_height, - upgraded_tm_client_state.0.proof_specs, - upgraded_tm_client_state.0.upgrade_path, - self.0.allow_update, - )?; - - // The new consensus state is merely used as a trusted kernel against - // which headers on the new chain can be verified. The root is just a - // stand-in sentinel value as it cannot be known in advance, thus no - // proof verification will pass. The timestamp and the - // NextValidatorsHash of the consensus state is the blocktime and - // NextValidatorsHash of the last block committed by the old chain. This - // will allow the first block of the new chain to be verified against - // the last validators of the old chain so long as it is submitted - // within the TrustingPeriod of this client. - // NOTE: We do not set processed time for this consensus state since - // this consensus state should not be used for packet verification as - // the root is empty. The next consensus state submitted using update - // will be usable for packet-verification. - let sentinel_root = "sentinel_root".as_bytes().to_vec(); - let new_consensus_state = ConsensusStateType::new( - sentinel_root.into(), - upgraded_tm_cons_state.timestamp(), - upgraded_tm_cons_state.next_validators_hash(), - ); - - let latest_height = new_client_state.latest_height; - let host_timestamp = CommonContext::host_timestamp(ctx)?; - let host_height = CommonContext::host_height(ctx)?; - - ctx.store_client_state( - ClientStatePath::new(client_id.clone()), - ClientState::from(new_client_state).into(), - )?; - ctx.store_consensus_state( - ClientConsensusStatePath::new( - client_id.clone(), - latest_height.revision_number(), - latest_height.revision_height(), - ), - TmConsensusState::from(new_consensus_state).into(), - )?; - ctx.store_update_meta( - client_id.clone(), - latest_height, - host_timestamp, - host_height, - )?; - - Ok(latest_height) - } -} - #[cfg(test)] mod tests { use core::time::Duration; @@ -534,8 +82,10 @@ mod tests { use ibc_client_tendermint_types::{ AllowUpdate, ClientState as ClientStateType, TrustThreshold, }; + use ibc_core_client::types::Height; use ibc_core_commitment_types::specs::ProofSpecs; use ibc_core_host::types::identifiers::ChainId; + use tests::common::validate_proof_height; use super::*; @@ -610,7 +160,7 @@ mod tests { Some(setup) => (setup)(ClientState(client_state)), _ => ClientState(client_state), }; - let res = client_state.validate_proof_height(test.height); + let res = validate_proof_height(client_state.inner(), test.height); assert_eq!( test.want_pass, diff --git a/ibc-clients/ics07-tendermint/src/client_state/common.rs b/ibc-clients/ics07-tendermint/src/client_state/common.rs new file mode 100644 index 000000000..583a1f36a --- /dev/null +++ b/ibc-clients/ics07-tendermint/src/client_state/common.rs @@ -0,0 +1,236 @@ +use ibc_client_tendermint_types::{client_type as tm_client_type, ClientState as ClientStateType}; +use ibc_core_client::context::client_state::ClientStateCommon; +use ibc_core_client::context::consensus_state::ConsensusState; +use ibc_core_client::types::error::{ClientError, UpgradeClientError}; +use ibc_core_client::types::Height; +use ibc_core_commitment_types::commitment::{ + CommitmentPrefix, CommitmentProofBytes, CommitmentRoot, +}; +use ibc_core_commitment_types::merkle::{apply_prefix, MerkleProof}; +use ibc_core_host::types::identifiers::ClientType; +use ibc_core_host::types::path::{Path, UpgradeClientPath}; +use ibc_primitives::prelude::*; +use ibc_primitives::proto::Any; +use ibc_primitives::ToVec; + +use super::ClientState; +use crate::consensus_state::ConsensusState as TmConsensusState; + +impl ClientStateCommon for ClientState { + fn verify_consensus_state(&self, consensus_state: Any) -> Result<(), ClientError> { + verify_consensus_state(consensus_state) + } + + fn client_type(&self) -> ClientType { + tm_client_type() + } + + fn latest_height(&self) -> Height { + self.0.latest_height + } + + fn validate_proof_height(&self, proof_height: Height) -> Result<(), ClientError> { + validate_proof_height(self.inner(), proof_height) + } + + fn verify_upgrade_client( + &self, + upgraded_client_state: Any, + upgraded_consensus_state: Any, + proof_upgrade_client: CommitmentProofBytes, + proof_upgrade_consensus_state: CommitmentProofBytes, + root: &CommitmentRoot, + ) -> Result<(), ClientError> { + verify_upgrade_client( + self.inner(), + upgraded_client_state, + upgraded_consensus_state, + proof_upgrade_client, + proof_upgrade_consensus_state, + root, + ) + } + + fn verify_membership( + &self, + prefix: &CommitmentPrefix, + proof: &CommitmentProofBytes, + root: &CommitmentRoot, + path: Path, + value: Vec, + ) -> Result<(), ClientError> { + verify_membership(self.inner(), prefix, proof, root, path, value) + } + + fn verify_non_membership( + &self, + prefix: &CommitmentPrefix, + proof: &CommitmentProofBytes, + root: &CommitmentRoot, + path: Path, + ) -> Result<(), ClientError> { + verify_non_membership(self.inner(), prefix, proof, root, path) + } +} + +/// Verify an `Any` consensus state by attempting to convert it to a `TmConsensusState`. +/// Also checks whether the converted consensus state's root is present. +/// +/// Note that this function is typically implemented as part of the +/// [`ClientStateCommon`] trait, but has been made a standalone function +/// in order to make the ClientState APIs more flexible. +pub fn verify_consensus_state(consensus_state: Any) -> Result<(), ClientError> { + let tm_consensus_state = TmConsensusState::try_from(consensus_state)?; + + if tm_consensus_state.root().is_empty() { + return Err(ClientError::Other { + description: "empty commitment root".into(), + }); + }; + + Ok(()) +} + +/// Validate the given proof height against the client state's latest height, returning +/// an error if the proof height is greater than the latest height of the client state. +/// +/// Note that this function is typically implemented as part of the +/// [`ClientStateCommon`] trait, but has been made a standalone function +/// in order to make the ClientState APIs more flexible. +pub fn validate_proof_height( + client_state: &ClientStateType, + proof_height: Height, +) -> Result<(), ClientError> { + let latest_height = client_state.latest_height; + + if latest_height < proof_height { + return Err(ClientError::InvalidProofHeight { + latest_height, + proof_height, + }); + } + + Ok(()) +} + +/// Perform client-specific verifications and check all data in the new +/// client state to be the same across all valid Tendermint clients for the +/// new chain. +/// +/// You can learn more about how to upgrade IBC-connected SDK chains in +/// [this](https://ibc.cosmos.network/main/ibc/upgrades/quick-guide.html) +/// guide. +/// +/// Note that this function is typically implemented as part of the +/// [`ClientStateCommon`] trait, but has been made a standalone function +/// in order to make the ClientState APIs more flexible. +pub fn verify_upgrade_client( + client_state: &ClientStateType, + upgraded_client_state: Any, + upgraded_consensus_state: Any, + proof_upgrade_client: CommitmentProofBytes, + proof_upgrade_consensus_state: CommitmentProofBytes, + root: &CommitmentRoot, +) -> Result<(), ClientError> { + // Make sure that the client type is of Tendermint type `ClientState` + let upgraded_tm_client_state = ClientState::try_from(upgraded_client_state.clone())?; + + // Make sure that the consensus type is of Tendermint type `ConsensusState` + TmConsensusState::try_from(upgraded_consensus_state.clone())?; + + let latest_height = client_state.latest_height; + let upgraded_tm_client_state_height = upgraded_tm_client_state.latest_height(); + + // Make sure the latest height of the current client is not greater then + // the upgrade height This condition checks both the revision number and + // the height + if latest_height >= upgraded_tm_client_state_height { + Err(UpgradeClientError::LowUpgradeHeight { + upgraded_height: latest_height, + client_height: upgraded_tm_client_state_height, + })? + } + + // Check to see if the upgrade path is set + let mut upgrade_path = client_state.upgrade_path.clone(); + + if upgrade_path.pop().is_none() { + return Err(ClientError::ClientSpecific { + description: "cannot upgrade client as no upgrade path has been set".to_string(), + }); + }; + + let upgrade_path_prefix = CommitmentPrefix::try_from(upgrade_path[0].clone().into_bytes()) + .map_err(ClientError::InvalidCommitmentProof)?; + + let last_height = latest_height.revision_height(); + + // Verify the proof of the upgraded client state + verify_membership( + client_state, + &upgrade_path_prefix, + &proof_upgrade_client, + root, + Path::UpgradeClient(UpgradeClientPath::UpgradedClientState(last_height)), + upgraded_client_state.to_vec(), + )?; + + // Verify the proof of the upgraded consensus state + verify_membership( + client_state, + &upgrade_path_prefix, + &proof_upgrade_consensus_state, + root, + Path::UpgradeClient(UpgradeClientPath::UpgradedClientConsensusState(last_height)), + upgraded_consensus_state.to_vec(), + )?; + + Ok(()) +} + +/// Verify membership of the given value against the client's merkle proof. +/// +/// Note that this function is typically implemented as part of the +/// [`ClientStateCommon`] trait, but has been made a standalone function +/// in order to make the ClientState APIs more flexible. +pub fn verify_membership( + client_state: &ClientStateType, + prefix: &CommitmentPrefix, + proof: &CommitmentProofBytes, + root: &CommitmentRoot, + path: Path, + value: Vec, +) -> Result<(), ClientError> { + let merkle_path = apply_prefix(prefix, vec![path.to_string()]); + let merkle_proof = MerkleProof::try_from(proof).map_err(ClientError::InvalidCommitmentProof)?; + + merkle_proof + .verify_membership( + &client_state.proof_specs, + root.clone().into(), + merkle_path, + value, + 0, + ) + .map_err(ClientError::Ics23Verification) +} + +/// Verify that the given value does not belong in the client's merkle proof. +/// +/// Note that this function is typically implemented as part of the +/// [`ClientStateCommon`] trait, but has been made a standalone function +/// in order to make the ClientState APIs more flexible. +pub fn verify_non_membership( + client_state: &ClientStateType, + prefix: &CommitmentPrefix, + proof: &CommitmentProofBytes, + root: &CommitmentRoot, + path: Path, +) -> Result<(), ClientError> { + let merkle_path = apply_prefix(prefix, vec![path.to_string()]); + let merkle_proof = MerkleProof::try_from(proof).map_err(ClientError::InvalidCommitmentProof)?; + + merkle_proof + .verify_non_membership(&client_state.proof_specs, root.clone().into(), merkle_path) + .map_err(ClientError::Ics23Verification) +} diff --git a/ibc-clients/ics07-tendermint/src/client_state/execution.rs b/ibc-clients/ics07-tendermint/src/client_state/execution.rs new file mode 100644 index 000000000..0cc6c436d --- /dev/null +++ b/ibc-clients/ics07-tendermint/src/client_state/execution.rs @@ -0,0 +1,348 @@ +use ibc_client_tendermint_types::{ + ClientState as ClientStateType, ConsensusState as ConsensusStateType, Header as TmHeader, +}; +use ibc_core_client::context::client_state::ClientStateExecution; +use ibc_core_client::context::ClientExecutionContext; +use ibc_core_client::types::error::ClientError; +use ibc_core_client::types::Height; +use ibc_core_host::types::identifiers::ClientId; +use ibc_core_host::types::path::{ClientConsensusStatePath, ClientStatePath}; +use ibc_core_host::ExecutionContext; +use ibc_primitives::prelude::*; +use ibc_primitives::proto::Any; + +use super::ClientState; +use crate::consensus_state::ConsensusState as TmConsensusState; +use crate::context::{CommonContext, ExecutionContext as TmExecutionContext}; + +impl ClientStateExecution for ClientState +where + E: TmExecutionContext + ExecutionContext, + ::AnyClientState: From, + ::AnyConsensusState: From, +{ + fn initialise( + &self, + ctx: &mut E, + client_id: &ClientId, + consensus_state: Any, + ) -> Result<(), ClientError> { + initialise(self.inner(), ctx, client_id, consensus_state) + } + + fn update_state( + &self, + ctx: &mut E, + client_id: &ClientId, + header: Any, + ) -> Result, ClientError> { + update_state(self.inner(), ctx, client_id, header) + } + + fn update_state_on_misbehaviour( + &self, + ctx: &mut E, + client_id: &ClientId, + client_message: Any, + ) -> Result<(), ClientError> { + update_on_misbehaviour(self.inner(), ctx, client_id, client_message) + } + + fn update_state_on_upgrade( + &self, + ctx: &mut E, + client_id: &ClientId, + upgraded_client_state: Any, + upgraded_consensus_state: Any, + ) -> Result { + update_on_upgrade( + self.inner(), + ctx, + client_id, + upgraded_client_state, + upgraded_consensus_state, + ) + } +} + +/// Seed the host store with initial client and consensus states. +/// +/// Note that this function is typically implemented as part of the +/// [`ClientStateExecution`] trait, but has been made a standalone function +/// in order to make the ClientState APIs more flexible. +pub fn initialise( + client_state: &ClientStateType, + ctx: &mut E, + client_id: &ClientId, + consensus_state: Any, +) -> Result<(), ClientError> +where + E: TmExecutionContext + ExecutionContext, + ::AnyClientState: From, + ::AnyConsensusState: From, +{ + let host_timestamp = CommonContext::host_timestamp(ctx)?; + let host_height = CommonContext::host_height(ctx)?; + + let tm_consensus_state = ConsensusStateType::try_from(consensus_state)?; + + ctx.store_client_state( + ClientStatePath::new(client_id.clone()), + client_state.clone().into(), + )?; + ctx.store_consensus_state( + ClientConsensusStatePath::new( + client_id.clone(), + client_state.latest_height.revision_number(), + client_state.latest_height.revision_height(), + ), + tm_consensus_state.into(), + )?; + + ctx.store_update_meta( + client_id.clone(), + client_state.latest_height, + host_timestamp, + host_height, + )?; + + Ok(()) +} + +/// Update the host store with a new client state, pruning old states from the +/// store if need be. +/// +/// Note that this function is typically implemented as part of the +/// [`ClientStateExecution`] trait, but has been made a standalone function +/// in order to make the ClientState APIs more flexible. +pub fn update_state( + client_state: &ClientStateType, + ctx: &mut E, + client_id: &ClientId, + header: Any, +) -> Result, ClientError> +where + E: TmExecutionContext + ExecutionContext, + ::AnyClientState: From, + ::AnyConsensusState: From, +{ + let header = TmHeader::try_from(header)?; + let header_height = header.height(); + + prune_oldest_consensus_state(client_state, ctx, client_id)?; + + let maybe_existing_consensus_state = { + let path_at_header_height = ClientConsensusStatePath::new( + client_id.clone(), + header_height.revision_number(), + header_height.revision_height(), + ); + + CommonContext::consensus_state(ctx, &path_at_header_height).ok() + }; + + if maybe_existing_consensus_state.is_some() { + // if we already had the header installed by a previous relayer + // then this is a no-op. + // + // Do nothing. + } else { + let host_timestamp = CommonContext::host_timestamp(ctx)?; + let host_height = CommonContext::host_height(ctx)?; + + let new_consensus_state = ConsensusStateType::from(header.clone()); + let new_client_state = client_state.clone().with_header(header)?; + + ctx.store_consensus_state( + ClientConsensusStatePath::new( + client_id.clone(), + header_height.revision_number(), + header_height.revision_height(), + ), + new_consensus_state.into(), + )?; + ctx.store_client_state( + ClientStatePath::new(client_id.clone()), + new_client_state.into(), + )?; + ctx.store_update_meta( + client_id.clone(), + header_height, + host_timestamp, + host_height, + )?; + } + + Ok(vec![header_height]) +} + +/// Commit a frozen client state, which was frozen as a result of having exhibited +/// misbehaviour, to the store. +/// +/// Note that this function is typically implemented as part of the +/// [`ClientStateExecution`] trait, but has been made a standalone function +/// in order to make the ClientState APIs more flexible. +pub fn update_on_misbehaviour( + client_state: &ClientStateType, + ctx: &mut E, + client_id: &ClientId, + _client_message: Any, +) -> Result<(), ClientError> +where + E: TmExecutionContext + ExecutionContext, + ::AnyClientState: From, + ::AnyConsensusState: From, +{ + // NOTE: frozen height is set to `Height {revision_height: 0, + // revision_number: 1}` and it is the same for all misbehaviour. This + // aligns with the + // [`ibc-go`](https://github.com/cosmos/ibc-go/blob/0e3f428e66d6fc0fc6b10d2f3c658aaa5000daf7/modules/light-clients/07-tendermint/misbehaviour.go#L18-L19) + // implementation. + let frozen_client_state = client_state.clone().with_frozen_height(Height::min(0)); + + ctx.store_client_state( + ClientStatePath::new(client_id.clone()), + frozen_client_state.into(), + )?; + + Ok(()) +} + +/// Commit the new client state and consensus state to the store. +/// +/// Note that this function is typically implemented as part of the +/// [`ClientStateExecution`] trait, but has been made a standalone function +/// in order to make the ClientState APIs more flexible. +pub fn update_on_upgrade( + client_state: &ClientStateType, + ctx: &mut E, + client_id: &ClientId, + upgraded_client_state: Any, + upgraded_consensus_state: Any, +) -> Result +where + E: TmExecutionContext + ExecutionContext, + ::AnyClientState: From, + ::AnyConsensusState: From, +{ + let mut upgraded_tm_client_state = ClientState::try_from(upgraded_client_state)?; + let upgraded_tm_cons_state = TmConsensusState::try_from(upgraded_consensus_state)?; + + upgraded_tm_client_state.0.zero_custom_fields(); + + // Construct new client state and consensus state relayer chosen client + // parameters are ignored. All chain-chosen parameters come from + // committed client, all client-chosen parameters come from current + // client. + let new_client_state = ClientStateType::new( + upgraded_tm_client_state.0.chain_id, + client_state.trust_level, + client_state.trusting_period, + upgraded_tm_client_state.0.unbonding_period, + client_state.max_clock_drift, + upgraded_tm_client_state.0.latest_height, + upgraded_tm_client_state.0.proof_specs, + upgraded_tm_client_state.0.upgrade_path, + client_state.allow_update, + )?; + + // The new consensus state is merely used as a trusted kernel against + // which headers on the new chain can be verified. The root is just a + // stand-in sentinel value as it cannot be known in advance, thus no + // proof verification will pass. The timestamp and the + // NextValidatorsHash of the consensus state is the blocktime and + // NextValidatorsHash of the last block committed by the old chain. This + // will allow the first block of the new chain to be verified against + // the last validators of the old chain so long as it is submitted + // within the TrustingPeriod of this client. + // NOTE: We do not set processed time for this consensus state since + // this consensus state should not be used for packet verification as + // the root is empty. The next consensus state submitted using update + // will be usable for packet-verification. + let sentinel_root = "sentinel_root".as_bytes().to_vec(); + let new_consensus_state = ConsensusStateType::new( + sentinel_root.into(), + upgraded_tm_cons_state.timestamp(), + upgraded_tm_cons_state.next_validators_hash(), + ); + + let latest_height = new_client_state.latest_height; + let host_timestamp = CommonContext::host_timestamp(ctx)?; + let host_height = CommonContext::host_height(ctx)?; + + ctx.store_client_state( + ClientStatePath::new(client_id.clone()), + new_client_state.into(), + )?; + ctx.store_consensus_state( + ClientConsensusStatePath::new( + client_id.clone(), + latest_height.revision_number(), + latest_height.revision_height(), + ), + new_consensus_state.into(), + )?; + ctx.store_update_meta( + client_id.clone(), + latest_height, + host_timestamp, + host_height, + )?; + + Ok(latest_height) +} + +/// Removes consensus states from the client store whose timestamps +/// are less than or equal to the host timestamp. This ensures that +/// the client store does not amass a buildup of stale consensus states. +pub fn prune_oldest_consensus_state( + client_state: &ClientStateType, + ctx: &mut E, + client_id: &ClientId, +) -> Result<(), ClientError> +where + E: ClientExecutionContext + CommonContext, +{ + let mut heights = ctx.consensus_state_heights(client_id)?; + + heights.sort(); + + for height in heights { + let client_consensus_state_path = ClientConsensusStatePath::new( + client_id.clone(), + height.revision_number(), + height.revision_height(), + ); + let consensus_state = CommonContext::consensus_state(ctx, &client_consensus_state_path)?; + let tm_consensus_state = consensus_state + .try_into() + .map_err(|err| ClientError::Other { + description: err.to_string(), + })?; + + let host_timestamp = + ctx.host_timestamp()? + .into_tm_time() + .ok_or_else(|| ClientError::Other { + description: String::from("host timestamp is not a valid TM timestamp"), + })?; + + let tm_consensus_state_timestamp = tm_consensus_state.timestamp(); + let tm_consensus_state_expiry = (tm_consensus_state_timestamp + + client_state.trusting_period) + .map_err(|_| ClientError::Other { + description: String::from( + "Timestamp overflow error occurred while attempting to parse TmConsensusState", + ), + })?; + + if tm_consensus_state_expiry > host_timestamp { + break; + } + + ctx.delete_consensus_state(client_consensus_state_path)?; + ctx.delete_update_meta(client_id.clone(), height)?; + } + + Ok(()) +} diff --git a/ibc-clients/ics07-tendermint/src/client_state/misbehaviour.rs b/ibc-clients/ics07-tendermint/src/client_state/misbehaviour.rs index c671ffa63..777bca1da 100644 --- a/ibc-clients/ics07-tendermint/src/client_state/misbehaviour.rs +++ b/ibc-clients/ics07-tendermint/src/client_state/misbehaviour.rs @@ -1,5 +1,8 @@ use ibc_client_tendermint_types::error::{Error, IntoResult}; -use ibc_client_tendermint_types::{Header as TmHeader, Misbehaviour as TmMisbehaviour}; +use ibc_client_tendermint_types::{ + ClientState as ClientStateType, ConsensusState as ConsensusStateType, Header as TmHeader, + Misbehaviour as TmMisbehaviour, +}; use ibc_core_client::types::error::ClientError; use ibc_core_host::types::identifiers::ClientId; use ibc_core_host::types::path::ClientConsensusStatePath; @@ -7,134 +10,146 @@ use ibc_primitives::prelude::*; use ibc_primitives::Timestamp; use tendermint_light_client_verifier::Verifier; -use super::{ClientState as TmClientState, TmValidationContext}; -use crate::consensus_state::ConsensusState as TmConsensusState; - -impl TmClientState { - // verify_misbehaviour determines whether or not two conflicting headers at - // the same height would have convinced the light client. - pub fn verify_misbehaviour( - &self, - ctx: &ClientValidationContext, - client_id: &ClientId, - misbehaviour: TmMisbehaviour, - ) -> Result<(), ClientError> - where - ClientValidationContext: TmValidationContext, +use super::TmValidationContext; +use crate::context::TmVerifier; + +/// Determines whether or not two conflicting headers at the same height would +/// have convinced the light client. +pub fn verify_misbehaviour( + client_state: &ClientStateType, + ctx: &V, + client_id: &ClientId, + misbehaviour: &TmMisbehaviour, + verifier: &impl TmVerifier, +) -> Result<(), ClientError> +where + V: TmValidationContext, +{ + misbehaviour.validate_basic()?; + + let header_1 = misbehaviour.header1(); + let trusted_consensus_state_1 = { + let consensus_state_path = ClientConsensusStatePath::new( + client_id.clone(), + header_1.trusted_height.revision_number(), + header_1.trusted_height.revision_height(), + ); + let consensus_state = ctx.consensus_state(&consensus_state_path)?; + + consensus_state + .try_into() + .map_err(|err| ClientError::Other { + description: err.to_string(), + })? + }; + + let header_2 = misbehaviour.header2(); + let trusted_consensus_state_2 = { + let consensus_state_path = ClientConsensusStatePath::new( + client_id.clone(), + header_2.trusted_height.revision_number(), + header_2.trusted_height.revision_height(), + ); + let consensus_state = ctx.consensus_state(&consensus_state_path)?; + + consensus_state + .try_into() + .map_err(|err| ClientError::Other { + description: err.to_string(), + })? + }; + + let current_timestamp = ctx.host_timestamp()?; + + verify_misbehaviour_header( + client_state, + header_1, + trusted_consensus_state_1.inner(), + current_timestamp, + verifier, + )?; + verify_misbehaviour_header( + client_state, + header_2, + trusted_consensus_state_2.inner(), + current_timestamp, + verifier, + ) +} + +pub fn verify_misbehaviour_header( + client_state: &ClientStateType, + header: &TmHeader, + trusted_consensus_state: &ConsensusStateType, + current_timestamp: Timestamp, + verifier: &impl TmVerifier, +) -> Result<(), ClientError> { + // ensure correctness of the trusted next validator set provided by the relayer + header.check_trusted_next_validator_set(trusted_consensus_state)?; + + // ensure trusted consensus state is within trusting period { - misbehaviour.validate_basic()?; - - let header_1 = misbehaviour.header1(); - let trusted_consensus_state_1 = { - let consensus_state_path = ClientConsensusStatePath::new( - client_id.clone(), - header_1.trusted_height.revision_number(), - header_1.trusted_height.revision_height(), - ); - let consensus_state = ctx.consensus_state(&consensus_state_path)?; - - consensus_state - .try_into() - .map_err(|err| ClientError::Other { - description: err.to_string(), - })? - }; - - let header_2 = misbehaviour.header2(); - let trusted_consensus_state_2 = { - let consensus_state_path = ClientConsensusStatePath::new( - client_id.clone(), - header_2.trusted_height.revision_number(), - header_2.trusted_height.revision_height(), - ); - let consensus_state = ctx.consensus_state(&consensus_state_path)?; - - consensus_state - .try_into() - .map_err(|err| ClientError::Other { - description: err.to_string(), - })? - }; - - let current_timestamp = ctx.host_timestamp()?; - self.verify_misbehaviour_header(header_1, &trusted_consensus_state_1, current_timestamp)?; - self.verify_misbehaviour_header(header_2, &trusted_consensus_state_2, current_timestamp) - } + let duration_since_consensus_state = current_timestamp + .duration_since(&(trusted_consensus_state.timestamp().into())) + .ok_or_else(|| ClientError::InvalidConsensusStateTimestamp { + time1: trusted_consensus_state.timestamp().into(), + time2: current_timestamp, + })?; - pub fn verify_misbehaviour_header( - &self, - header: &TmHeader, - trusted_consensus_state: &TmConsensusState, - current_timestamp: Timestamp, - ) -> Result<(), ClientError> { - // ensure correctness of the trusted next validator set provided by the relayer - header.check_trusted_next_validator_set(trusted_consensus_state.inner())?; - - // ensure trusted consensus state is within trusting period - { - let duration_since_consensus_state = current_timestamp - .duration_since(&(trusted_consensus_state.timestamp().into())) - .ok_or_else(|| ClientError::InvalidConsensusStateTimestamp { - time1: trusted_consensus_state.timestamp().into(), - time2: current_timestamp, - })?; - - if duration_since_consensus_state >= self.0.trusting_period { - return Err(Error::ConsensusStateTimestampGteTrustingPeriod { - duration_since_consensus_state, - trusting_period: self.0.trusting_period, - } - .into()); + if duration_since_consensus_state >= client_state.trusting_period { + return Err(Error::ConsensusStateTimestampGteTrustingPeriod { + duration_since_consensus_state, + trusting_period: client_state.trusting_period, } + .into()); } + } - // main header verification, delegated to the tendermint-light-client crate. - let untrusted_state = header.as_untrusted_block_state(); + // main header verification, delegated to the tendermint-light-client crate. + let untrusted_state = header.as_untrusted_block_state(); - let chain_id = self - .0 + let chain_id = + client_state .chain_id .to_string() .try_into() .map_err(|e| ClientError::Other { description: format!("failed to parse chain id: {}", e), })?; - let trusted_state = - header.as_trusted_block_state(trusted_consensus_state.inner(), &chain_id)?; - let options = self.0.as_light_client_options()?; - let current_timestamp = current_timestamp.into_tm_time().ok_or(ClientError::Other { - description: "host timestamp must not be zero".to_string(), - })?; + let trusted_state = header.as_trusted_block_state(trusted_consensus_state, &chain_id)?; - self.0 - .verifier - .verify_misbehaviour_header(untrusted_state, trusted_state, &options, current_timestamp) - .into_result()?; + let options = client_state.as_light_client_options()?; + let current_timestamp = current_timestamp.into_tm_time().ok_or(ClientError::Other { + description: "host timestamp must not be zero".to_string(), + })?; - Ok(()) - } + verifier + .verifier() + .verify_misbehaviour_header(untrusted_state, trusted_state, &options, current_timestamp) + .into_result()?; - pub fn check_for_misbehaviour_misbehavior( - &self, - misbehaviour: &TmMisbehaviour, - ) -> Result { - let header_1 = misbehaviour.header1(); - let header_2 = misbehaviour.header2(); - - if header_1.height() == header_2.height() { - // when the height of the 2 headers are equal, we only have evidence - // of misbehaviour in the case where the headers are different - // (otherwise, the same header was added twice in the message, - // and this is evidence of nothing) - Ok(header_1.signed_header.commit.block_id.hash - != header_2.signed_header.commit.block_id.hash) - } else { - // header_1 is at greater height than header_2, therefore - // header_1 time must be less than or equal to - // header_2 time in order to be valid misbehaviour (violation of - // monotonic time). - Ok(header_1.signed_header.header.time <= header_2.signed_header.header.time) - } + Ok(()) +} + +pub fn check_for_misbehaviour_misbehavior( + misbehaviour: &TmMisbehaviour, +) -> Result { + let header_1 = misbehaviour.header1(); + let header_2 = misbehaviour.header2(); + + if header_1.height() == header_2.height() { + // when the height of the 2 headers are equal, we only have evidence + // of misbehaviour in the case where the headers are different + // (otherwise, the same header was added twice in the message, + // and this is evidence of nothing) + Ok(header_1.signed_header.commit.block_id.hash + != header_2.signed_header.commit.block_id.hash) + } else { + // header_1 is at greater height than header_2, therefore + // header_1 time must be less than or equal to + // header_2 time in order to be valid misbehaviour (violation of + // monotonic time). + Ok(header_1.signed_header.header.time <= header_2.signed_header.header.time) } } diff --git a/ibc-clients/ics07-tendermint/src/client_state/update_client.rs b/ibc-clients/ics07-tendermint/src/client_state/update_client.rs index f412e6242..b5be61f49 100644 --- a/ibc-clients/ics07-tendermint/src/client_state/update_client.rs +++ b/ibc-clients/ics07-tendermint/src/client_state/update_client.rs @@ -1,6 +1,7 @@ use ibc_client_tendermint_types::error::{Error, IntoResult}; -use ibc_client_tendermint_types::{ConsensusState as ConsensusStateType, Header as TmHeader}; -use ibc_core_client::context::ClientExecutionContext; +use ibc_client_tendermint_types::{ + ClientState as ClientStateType, ConsensusState as ConsensusStateType, Header as TmHeader, +}; use ibc_core_client::types::error::ClientError; use ibc_core_host::types::identifiers::ClientId; use ibc_core_host::types::path::ClientConsensusStatePath; @@ -8,221 +9,171 @@ use ibc_primitives::prelude::*; use tendermint_light_client_verifier::types::{TrustedBlockState, UntrustedBlockState}; use tendermint_light_client_verifier::Verifier; -use super::ClientState; use crate::consensus_state::ConsensusState as TmConsensusState; -use crate::context::{CommonContext, ValidationContext as TmValidationContext}; - -impl ClientState { - pub fn verify_header( - &self, - ctx: &ClientValidationContext, - client_id: &ClientId, - header: TmHeader, - ) -> Result<(), ClientError> - where - ClientValidationContext: TmValidationContext, +use crate::context::{TmVerifier, ValidationContext as TmValidationContext}; + +pub fn verify_header( + client_state: &ClientStateType, + ctx: &V, + client_id: &ClientId, + header: &TmHeader, + verifier: &impl TmVerifier, +) -> Result<(), ClientError> +where + V: TmValidationContext, +{ + // Checks that the header fields are valid. + header.validate_basic()?; + + // The tendermint-light-client crate though works on heights that are assumed + // to have the same revision number. We ensure this here. + header.verify_chain_id_version_matches_height(&client_state.chain_id())?; + + // Delegate to tendermint-light-client, which contains the required checks + // of the new header against the trusted consensus state. { - // Checks that the header fields are valid. - header.validate_basic()?; - - // The tendermint-light-client crate though works on heights that are assumed - // to have the same revision number. We ensure this here. - header.verify_chain_id_version_matches_height(&self.0.chain_id())?; - - // Delegate to tendermint-light-client, which contains the required checks - // of the new header against the trusted consensus state. - { - let trusted_state = - { - let trusted_client_cons_state_path = ClientConsensusStatePath::new( - client_id.clone(), - header.trusted_height.revision_number(), - header.trusted_height.revision_height(), - ); - let trusted_consensus_state: TmConsensusState = ctx - .consensus_state(&trusted_client_cons_state_path)? - .try_into() - .map_err(|err| ClientError::Other { - description: err.to_string(), - })?; + let trusted_state = { + let trusted_client_cons_state_path = ClientConsensusStatePath::new( + client_id.clone(), + header.trusted_height.revision_number(), + header.trusted_height.revision_height(), + ); + let trusted_consensus_state: TmConsensusState = ctx + .consensus_state(&trusted_client_cons_state_path)? + .try_into() + .map_err(|err| ClientError::Other { + description: err.to_string(), + })?; + + header.check_trusted_next_validator_set(trusted_consensus_state.inner())?; - header.check_trusted_next_validator_set(trusted_consensus_state.inner())?; - - TrustedBlockState { - chain_id: &self.0.chain_id.to_string().try_into().map_err(|e| { - ClientError::Other { - description: format!("failed to parse chain id: {}", e), - } - })?, - header_time: trusted_consensus_state.timestamp(), - height: header.trusted_height.revision_height().try_into().map_err( - |_| ClientError::ClientSpecific { - description: Error::InvalidHeaderHeight { - height: header.trusted_height.revision_height(), - } - .to_string(), - }, - )?, - next_validators: &header.trusted_next_validator_set, - next_validators_hash: trusted_consensus_state.next_validators_hash(), + TrustedBlockState { + chain_id: &client_state.chain_id.to_string().try_into().map_err(|e| { + ClientError::Other { + description: format!("failed to parse chain id: {}", e), } - }; - - let untrusted_state = UntrustedBlockState { - signed_header: &header.signed_header, - validators: &header.validator_set, - // NB: This will skip the - // VerificationPredicates::next_validators_match check for the - // untrusted state. - next_validators: None, - }; - - let options = self.0.as_light_client_options()?; - let now = ctx.host_timestamp()?.into_tm_time().ok_or_else(|| { - ClientError::ClientSpecific { - description: "host timestamp is not a valid TM timestamp".to_string(), - } - })?; + })?, + header_time: trusted_consensus_state.timestamp(), + height: header + .trusted_height + .revision_height() + .try_into() + .map_err(|_| ClientError::ClientSpecific { + description: Error::InvalidHeaderHeight { + height: header.trusted_height.revision_height(), + } + .to_string(), + })?, + next_validators: &header.trusted_next_validator_set, + next_validators_hash: trusted_consensus_state.next_validators_hash(), + } + }; - // main header verification, delegated to the tendermint-light-client crate. - self.0 - .verifier - .verify_update_header(untrusted_state, trusted_state, &options, now) - .into_result()?; - } + let untrusted_state = UntrustedBlockState { + signed_header: &header.signed_header, + validators: &header.validator_set, + // NB: This will skip the + // VerificationPredicates::next_validators_match check for the + // untrusted state. + next_validators: None, + }; - Ok(()) - } + let options = client_state.as_light_client_options()?; + let now = + ctx.host_timestamp()? + .into_tm_time() + .ok_or_else(|| ClientError::ClientSpecific { + description: "host timestamp is not a valid TM timestamp".to_string(), + })?; - pub fn check_for_misbehaviour_update_client( - &self, - ctx: &ClientValidationContext, - client_id: &ClientId, - header: TmHeader, - ) -> Result - where - ClientValidationContext: TmValidationContext, - { - let maybe_existing_consensus_state = { - let path_at_header_height = ClientConsensusStatePath::new( - client_id.clone(), - header.height().revision_number(), - header.height().revision_height(), - ); + // main header verification, delegated to the tendermint-light-client crate. + verifier + .verifier() + .verify_update_header(untrusted_state, trusted_state, &options, now) + .into_result()?; + } - ctx.consensus_state(&path_at_header_height).ok() - }; + Ok(()) +} - match maybe_existing_consensus_state { - Some(existing_consensus_state) => { - let existing_consensus_state: TmConsensusState = existing_consensus_state +/// Checks for misbehaviour upon receiving a new consensus state as part +/// of a client update. +pub fn check_for_misbehaviour_update_client( + client_state: &ClientStateType, + ctx: &V, + client_id: &ClientId, + header: TmHeader, +) -> Result +where + V: TmValidationContext, +{ + let maybe_existing_consensus_state = { + let path_at_header_height = ClientConsensusStatePath::new( + client_id.clone(), + header.height().revision_number(), + header.height().revision_height(), + ); + + ctx.consensus_state(&path_at_header_height).ok() + }; + + match maybe_existing_consensus_state { + Some(existing_consensus_state) => { + let existing_consensus_state = + existing_consensus_state .try_into() .map_err(|err| ClientError::Other { description: err.to_string(), })?; - let header_consensus_state = - TmConsensusState::from(ConsensusStateType::from(header.clone())); + let header_consensus_state = + TmConsensusState::from(ConsensusStateType::from(header.clone())); - // There is evidence of misbehaviour if the stored consensus state - // is different from the new one we received. - Ok(existing_consensus_state != header_consensus_state) - } - None => { - // If no header was previously installed, we ensure the monotonicity of timestamps. - - // 1. for all headers, the new header needs to have a larger timestamp than - // the “previous header” - { - let maybe_prev_cs = ctx.prev_consensus_state(client_id, &header.height())?; - - if let Some(prev_cs) = maybe_prev_cs { - // New header timestamp cannot occur *before* the - // previous consensus state's height - let prev_cs: TmConsensusState = - prev_cs.try_into().map_err(|err| ClientError::Other { - description: err.to_string(), - })?; - - if header.signed_header.header().time <= prev_cs.timestamp() { - return Ok(true); - } - } - } + // There is evidence of misbehaviour if the stored consensus state + // is different from the new one we received. + Ok(existing_consensus_state != header_consensus_state) + } + None => { + // If no header was previously installed, we ensure the monotonicity of timestamps. + + // 1. for all headers, the new header needs to have a larger timestamp than + // the “previous header” + { + let maybe_prev_cs = ctx.prev_consensus_state(client_id, &header.height())?; + + if let Some(prev_cs) = maybe_prev_cs { + // New header timestamp cannot occur *before* the + // previous consensus state's height + let prev_cs: TmConsensusState = + prev_cs.try_into().map_err(|err| ClientError::Other { + description: err.to_string(), + })?; - // 2. if a header comes in and is not the “last” header, then we also ensure - // that its timestamp is less than the “next header” - if header.height() < self.0.latest_height { - let maybe_next_cs = ctx.next_consensus_state(client_id, &header.height())?; - - if let Some(next_cs) = maybe_next_cs { - // New (untrusted) header timestamp cannot occur *after* next - // consensus state's height - let next_cs: TmConsensusState = - next_cs.try_into().map_err(|err| ClientError::Other { - description: err.to_string(), - })?; - - if header.signed_header.header().time >= next_cs.timestamp() { - return Ok(true); - } + if header.signed_header.header().time <= prev_cs.timestamp() { + return Ok(true); } } - - Ok(false) } - } - } - - pub fn prune_oldest_consensus_state( - &self, - ctx: &mut E, - client_id: &ClientId, - ) -> Result<(), ClientError> - where - E: ClientExecutionContext + CommonContext, - { - let mut heights = ctx.consensus_state_heights(client_id)?; - heights.sort(); + // 2. if a header comes in and is not the “last” header, then we also ensure + // that its timestamp is less than the “next header” + if header.height() < client_state.latest_height { + let maybe_next_cs = ctx.next_consensus_state(client_id, &header.height())?; - for height in heights { - let client_consensus_state_path = ClientConsensusStatePath::new( - client_id.clone(), - height.revision_number(), - height.revision_height(), - ); - let consensus_state = - CommonContext::consensus_state(ctx, &client_consensus_state_path)?; - let tm_consensus_state: TmConsensusState = - consensus_state - .try_into() - .map_err(|err| ClientError::Other { + if let Some(next_cs) = maybe_next_cs { + // New (untrusted) header timestamp cannot occur *after* next + // consensus state's height + let next_cs = next_cs.try_into().map_err(|err| ClientError::Other { description: err.to_string(), })?; - let host_timestamp = - ctx.host_timestamp()? - .into_tm_time() - .ok_or_else(|| ClientError::Other { - description: String::from("host timestamp is not a valid TM timestamp"), - })?; - let tm_consensus_state_timestamp = tm_consensus_state.timestamp(); - let tm_consensus_state_expiry = (tm_consensus_state_timestamp - + self.0.trusting_period) - .map_err(|_| ClientError::Other { - description: String::from( - "Timestamp overflow error occurred while attempting to parse TmConsensusState", - ), - })?; - - if tm_consensus_state_expiry > host_timestamp { - break; + if header.signed_header.header().time >= next_cs.timestamp() { + return Ok(true); + } + } } - ctx.delete_consensus_state(client_consensus_state_path)?; - ctx.delete_update_meta(client_id.clone(), height)?; - } - Ok(()) + Ok(false) + } } } diff --git a/ibc-clients/ics07-tendermint/src/client_state/validation.rs b/ibc-clients/ics07-tendermint/src/client_state/validation.rs new file mode 100644 index 000000000..0b79e9d45 --- /dev/null +++ b/ibc-clients/ics07-tendermint/src/client_state/validation.rs @@ -0,0 +1,196 @@ +use ibc_client_tendermint_types::{ + ClientState as ClientStateType, Header as TmHeader, Misbehaviour as TmMisbehaviour, + TENDERMINT_HEADER_TYPE_URL, TENDERMINT_MISBEHAVIOUR_TYPE_URL, +}; +use ibc_core_client::context::client_state::ClientStateValidation; +use ibc_core_client::context::ClientValidationContext; +use ibc_core_client::types::error::ClientError; +use ibc_core_client::types::Status; +use ibc_core_host::types::identifiers::ClientId; +use ibc_core_host::types::path::ClientConsensusStatePath; +use ibc_primitives::prelude::*; +use ibc_primitives::proto::Any; + +use super::{ + check_for_misbehaviour_misbehavior, check_for_misbehaviour_update_client, ClientState, +}; +use crate::client_state::{verify_header, verify_misbehaviour}; +use crate::consensus_state::ConsensusState as TmConsensusState; +use crate::context::{DefaultVerifier, TmVerifier, ValidationContext as TmValidationContext}; + +impl ClientStateValidation for ClientState +where + V: ClientValidationContext + TmValidationContext, + V::AnyConsensusState: TryInto, + ClientError: From<>::Error>, +{ + /// The default verification logic exposed by ibc-rs simply delegates to a + /// standalone `verify_client_message` function. This is to make it as simple + /// as possible for those who merely need the `DefaultVerifier` behaviour, as + /// well as those who require custom verification logic. + fn verify_client_message( + &self, + ctx: &V, + client_id: &ClientId, + client_message: Any, + ) -> Result<(), ClientError> { + verify_client_message( + self.inner(), + ctx, + client_id, + client_message, + &DefaultVerifier, + ) + } + + fn check_for_misbehaviour( + &self, + ctx: &V, + client_id: &ClientId, + client_message: Any, + ) -> Result { + check_for_misbehaviour(self.inner(), ctx, client_id, client_message) + } + + fn status(&self, ctx: &V, client_id: &ClientId) -> Result { + status(self.inner(), ctx, client_id) + } +} + +/// Verify the client message as part of the client state validation process. +/// +/// Note that this function is typically implemented as part of the +/// [`ClientStateValidation`] trait, but has been made a standalone function +/// in order to make the ClientState APIs more flexible. It mostly adheres to +/// the same signature as the `ClientStateValidation::verify_client_message` +/// function, except for an additional `verifier` parameter that allows users +/// who require custom verification logic to easily pass in their own verifier +/// implementation. +pub fn verify_client_message( + client_state: &ClientStateType, + ctx: &V, + client_id: &ClientId, + client_message: Any, + verifier: &impl TmVerifier, +) -> Result<(), ClientError> +where + V: ClientValidationContext + TmValidationContext, +{ + match client_message.type_url.as_str() { + TENDERMINT_HEADER_TYPE_URL => { + let header = TmHeader::try_from(client_message)?; + verify_header(client_state, ctx, client_id, &header, verifier) + } + TENDERMINT_MISBEHAVIOUR_TYPE_URL => { + let misbehaviour = TmMisbehaviour::try_from(client_message)?; + verify_misbehaviour(client_state, ctx, client_id, &misbehaviour, verifier) + } + _ => Err(ClientError::InvalidUpdateClientMessage), + } +} + +/// Check for misbehaviour on the client state as part of the client state +/// validation process. +/// +/// Note that this function is typically implemented as part of the +/// [`ClientStateValidation`] trait, but has been made a standalone function +/// in order to make the ClientState APIs more flexible. +/// +/// This method covers the following cases: +/// +/// 1 - fork: +/// Assumes at least one consensus state before the fork point exists. Let +/// existing consensus states on chain B be: [Sn,.., Sf, Sf-1, S0] with +/// `Sf-1` being the most recent state before fork. Chain A is queried for a +/// header `Hf'` at `Sf.height` and if it is different than the `Hf` in the +/// event for the client update (the one that has generated `Sf` on chain), +/// then the two headers are included in the evidence and submitted. Note +/// that in this case the headers are different but have the same height. +/// +/// 2 - BFT time violation for unavailable header (a.k.a. Future Lunatic +/// Attack or FLA): +/// Some header with a height that is higher than the latest height on A has +/// been accepted and a consensus state was created on B. Note that this +/// implies that the timestamp of this header must be within the +/// `clock_drift` of the client. Assume the client on B has been updated +/// with `h2`(not present on/ produced by chain A) and it has a timestamp of +/// `t2` that is at most `clock_drift` in the future. Then the latest header +/// from A is fetched, let it be `h1`, with a timestamp of `t1`. If `t1 >= +/// t2` then evidence of misbehavior is submitted to A. +/// +/// 3 - BFT time violation for existing headers: +/// Ensure that consensus state times are monotonically increasing with +/// height. +pub fn check_for_misbehaviour( + client_state: &ClientStateType, + ctx: &V, + client_id: &ClientId, + client_message: Any, +) -> Result +where + V: ClientValidationContext + TmValidationContext, +{ + match client_message.type_url.as_str() { + TENDERMINT_HEADER_TYPE_URL => { + let header = TmHeader::try_from(client_message)?; + check_for_misbehaviour_update_client(client_state, ctx, client_id, header) + } + TENDERMINT_MISBEHAVIOUR_TYPE_URL => { + let misbehaviour = TmMisbehaviour::try_from(client_message)?; + check_for_misbehaviour_misbehavior(&misbehaviour) + } + _ => Err(ClientError::InvalidUpdateClientMessage), + } +} + +/// Query the status of the client state. +/// +/// Note that this function is typically implemented as part of the +/// [`ClientStateValidation`] trait, but has been made a standalone function +/// in order to make the ClientState APIs more flexible. +pub fn status( + client_state: &ClientStateType, + ctx: &V, + client_id: &ClientId, +) -> Result +where + V: ClientValidationContext + TmValidationContext, +{ + if client_state.is_frozen() { + return Ok(Status::Frozen); + } + + let latest_consensus_state: TmConsensusState = { + let any_latest_consensus_state = match ctx.consensus_state(&ClientConsensusStatePath::new( + client_id.clone(), + client_state.latest_height.revision_number(), + client_state.latest_height.revision_height(), + )) { + Ok(cs) => cs, + // if the client state does not have an associated consensus state for its latest height + // then it must be expired + Err(_) => return Ok(Status::Expired), + }; + + any_latest_consensus_state + .try_into() + .map_err(|err| ClientError::Other { + description: err.to_string(), + })? + }; + + // Note: if the `duration_since()` is `None`, indicating that the latest + // consensus state is in the future, then we don't consider the client + // to be expired. + let now = ctx.host_timestamp()?; + + if let Some(elapsed_since_latest_consensus_state) = + now.duration_since(&latest_consensus_state.timestamp().into()) + { + if elapsed_since_latest_consensus_state > client_state.trusting_period { + return Ok(Status::Expired); + } + } + + Ok(Status::Active) +} diff --git a/ibc-clients/ics07-tendermint/src/consensus_state.rs b/ibc-clients/ics07-tendermint/src/consensus_state.rs index 020849e57..a09fc705a 100644 --- a/ibc-clients/ics07-tendermint/src/consensus_state.rs +++ b/ibc-clients/ics07-tendermint/src/consensus_state.rs @@ -22,7 +22,7 @@ use tendermint::{Hash, Time}; /// bypass Rust's orphan rules and implement traits from /// `ibc::core::client::context` on the `ConsensusState` type. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, derive_more::From)] pub struct ConsensusState(ConsensusStateType); impl ConsensusState { @@ -39,12 +39,6 @@ impl ConsensusState { } } -impl From for ConsensusState { - fn from(consensus_state: ConsensusStateType) -> Self { - Self(consensus_state) - } -} - impl Protobuf for ConsensusState {} impl TryFrom for ConsensusState { diff --git a/ibc-clients/ics07-tendermint/src/context.rs b/ibc-clients/ics07-tendermint/src/context.rs index 1e190f5b6..cd998f6df 100644 --- a/ibc-clients/ics07-tendermint/src/context.rs +++ b/ibc-clients/ics07-tendermint/src/context.rs @@ -5,6 +5,7 @@ use ibc_core_host::types::identifiers::ClientId; use ibc_core_host::types::path::ClientConsensusStatePath; use ibc_primitives::prelude::*; use ibc_primitives::Timestamp; +use tendermint_light_client_verifier::ProdVerifier; use crate::consensus_state::ConsensusState as TmConsensusState; @@ -56,3 +57,39 @@ pub trait ValidationContext: CommonContext { pub trait ExecutionContext: CommonContext + ClientExecutionContext {} impl ExecutionContext for T where T: CommonContext + ClientExecutionContext {} + +/// Specifies the Verifier interface that hosts must adhere to when customizing +/// Tendermint client verification behaviour. +/// +/// For users who require custom verification logic, i.e., in situations when +/// the Tendermint `ProdVerifier` doesn't provide the desired outcome, users +/// should define a custom verifier struct as a unit struct and then implement +/// `TmVerifier` for it. Note that the custom verifier does need to also +/// implement the `tendermint_light_client_verifier::Verifier` trait. +/// +/// In order to wire up the custom verifier, the `verify_client_message` method +/// on the `ClientStateValidation` trait must be implemented. The simplest way +/// to do this is to import and call the standalone `verify_client_message` +/// function located in the `ibc::clients::tendermint::client_state` module, +/// passing in your custom verifier type as its `verifier` parameter. The rest +/// of the methods in the `ClientStateValidation` trait can be implemented by +/// importing and calling their analogous standalone version from the +/// `tendermint::client_state` module, unless bespoke logic is desired for any +/// of those functions. +pub trait TmVerifier { + type Verifier: tendermint_light_client_verifier::Verifier; + + fn verifier(&self) -> Self::Verifier; +} + +/// The default verifier for IBC clients, the Tendermint light client +/// ProdVerifier, for those users who don't require custom verification logic. +pub struct DefaultVerifier; + +impl TmVerifier for DefaultVerifier { + type Verifier = ProdVerifier; + + fn verifier(&self) -> Self::Verifier { + ProdVerifier::default() + } +} diff --git a/ibc-clients/ics07-tendermint/types/src/client_state.rs b/ibc-clients/ics07-tendermint/types/src/client_state.rs index e88495872..5fbb53189 100644 --- a/ibc-clients/ics07-tendermint/types/src/client_state.rs +++ b/ibc-clients/ics07-tendermint/types/src/client_state.rs @@ -17,7 +17,6 @@ use ibc_proto::Protobuf; use tendermint::chain::id::MAX_LENGTH as MaxChainIdLen; use tendermint::trust_threshold::TrustThresholdFraction as TendermintTrustThresholdFraction; use tendermint_light_client_verifier::options::Options; -use tendermint_light_client_verifier::ProdVerifier; use crate::error::Error; use crate::header::Header as TmHeader; @@ -46,8 +45,6 @@ pub struct ClientState { pub upgrade_path: Vec, pub allow_update: AllowUpdate, pub frozen_height: Option, - #[cfg_attr(feature = "serde", serde(skip))] - pub verifier: ProdVerifier, } impl ClientState { @@ -75,7 +72,6 @@ impl ClientState { upgrade_path, allow_update, frozen_height, - verifier: ProdVerifier::default(), } } diff --git a/ibc-core/ics24-host/cosmos/src/validate_self_client.rs b/ibc-core/ics24-host/cosmos/src/validate_self_client.rs index e0cdb9b81..9f32348eb 100644 --- a/ibc-core/ics24-host/cosmos/src/validate_self_client.rs +++ b/ibc-core/ics24-host/cosmos/src/validate_self_client.rs @@ -50,25 +50,26 @@ pub trait ValidateSelfClientContext { )); } + let latest_height = tm_client_state.latest_height(); let self_revision_number = self_chain_id.revision_number(); - if self_revision_number != tm_client_state.latest_height().revision_number() { + if self_revision_number != latest_height.revision_number() { return Err(ContextError::ConnectionError( ConnectionError::InvalidClientState { reason: format!( "client is not in the same revision as the chain. expected: {}, got: {}", self_revision_number, - tm_client_state.latest_height().revision_number() + latest_height.revision_number() ), }, )); } - if tm_client_state.latest_height() >= self.host_current_height() { + if latest_height >= self.host_current_height() { return Err(ContextError::ConnectionError( ConnectionError::InvalidClientState { reason: format!( "client has latest height {} greater than or equal to chain height {}", - tm_client_state.latest_height(), + latest_height, self.host_current_height() ), }, diff --git a/ibc-testkit/src/testapp/ibc/clients/mod.rs b/ibc-testkit/src/testapp/ibc/clients/mod.rs index e5f7ad7be..27ce9e34c 100644 --- a/ibc-testkit/src/testapp/ibc/clients/mod.rs +++ b/ibc-testkit/src/testapp/ibc/clients/mod.rs @@ -4,6 +4,7 @@ use derive_more::{From, TryInto}; use ibc::clients::tendermint::client_state::ClientState as TmClientState; use ibc::clients::tendermint::consensus_state::ConsensusState as TmConsensusState; use ibc::clients::tendermint::types::{ + ClientState as ClientStateType, ConsensusState as ConsensusStateType, TENDERMINT_CLIENT_STATE_TYPE_URL, TENDERMINT_CONSENSUS_STATE_TYPE_URL, }; use ibc::core::client::types::error::ClientError; @@ -54,6 +55,18 @@ impl From for Any { } } +impl From for AnyClientState { + fn from(client_state: ClientStateType) -> Self { + AnyClientState::Tendermint(client_state.into()) + } +} + +impl From for AnyConsensusState { + fn from(consensus_state: ConsensusStateType) -> Self { + AnyConsensusState::Tendermint(consensus_state.into()) + } +} + #[derive(Debug, Clone, From, TryInto, PartialEq, ConsensusState)] pub enum AnyConsensusState { Tendermint(TmConsensusState),