From efbbf2657d55b93bbf7b5caedbe941f1afd29710 Mon Sep 17 00:00:00 2001 From: Jun Kimura Date: Tue, 6 Feb 2024 00:43:38 +0900 Subject: [PATCH 1/2] tendermint-lc: fork `check_header_and_update_state` function from ibc-rs Signed-off-by: Jun Kimura --- Cargo.lock | 1 + enclave/Cargo.lock | 1 + modules/tendermint-lc/Cargo.toml | 1 + modules/tendermint-lc/src/client.rs | 23 +-- modules/tendermint-lc/src/lib.rs | 1 + modules/tendermint-lc/src/verifier.rs | 232 ++++++++++++++++++++++++++ 6 files changed, 248 insertions(+), 11 deletions(-) create mode 100644 modules/tendermint-lc/src/verifier.rs diff --git a/Cargo.lock b/Cargo.lock index ce8b85ec..7a28dd5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5177,6 +5177,7 @@ dependencies = [ "light-client", "log 0.4.17", "serde", + "tendermint-light-client-verifier 0.29.0", ] [[package]] diff --git a/enclave/Cargo.lock b/enclave/Cargo.lock index 70c1f5c8..5d1985fb 100644 --- a/enclave/Cargo.lock +++ b/enclave/Cargo.lock @@ -1859,6 +1859,7 @@ dependencies = [ "light-client", "log 0.4.17", "serde", + "tendermint-light-client-verifier", ] [[package]] diff --git a/modules/tendermint-lc/Cargo.toml b/modules/tendermint-lc/Cargo.toml index 2aa93e2b..52c63125 100644 --- a/modules/tendermint-lc/Cargo.toml +++ b/modules/tendermint-lc/Cargo.toml @@ -8,6 +8,7 @@ ibc = { version = "0.29.0", default-features = false, features = ["serde"] } serde = { version = "1.0.184", default-features = false, features = ["alloc"] } log = { version = "0.4.8", default-features = false } flex-error = { version = "0.4.4", default-features = false } +tendermint-light-client-verifier = { version = "0.29", features = ["rust-crypto"], default-features = false } light-client = { path = "../light-client", default-features = false, features = ["ibc"] } lcp-proto = { path = "../../proto", default-features = false } diff --git a/modules/tendermint-lc/src/client.rs b/modules/tendermint-lc/src/client.rs index e9ea85ff..e515699f 100644 --- a/modules/tendermint-lc/src/client.rs +++ b/modules/tendermint-lc/src/client.rs @@ -2,6 +2,7 @@ use crate::errors::Error; use crate::message::{ClientMessage, Header, Misbehaviour}; use crate::prelude::*; use crate::state::{canonicalize_state, gen_state_id, ClientState, ConsensusState}; +use crate::verifier::check_header_and_update_state; use core::str::FromStr; use crypto::Keccak256; use ibc::clients::ics07_tendermint::client_state::{ @@ -288,17 +289,17 @@ impl TendermintLightClient { let UpdatedState { client_state: new_client_state, consensus_state: new_consensus_state, - } = client_state - .check_header_and_update_state( - &IBCContext::::new(ctx), - client_id.into(), - Any::from(header.clone()).into(), - ) - .map_err(|e| { - Error::ics02(ICS02Error::HeaderVerificationFailure { - reason: e.to_string(), - }) - })?; + } = check_header_and_update_state( + &client_state, + &IBCContext::::new(ctx), + client_id.into(), + Any::from(header.clone()).into(), + ) + .map_err(|e| { + Error::ics02(ICS02Error::HeaderVerificationFailure { + reason: e.to_string(), + }) + })?; let new_client_state = ClientState( downcast_client_state::(new_client_state.as_ref()) diff --git a/modules/tendermint-lc/src/lib.rs b/modules/tendermint-lc/src/lib.rs index 3eb66cb7..9391e809 100644 --- a/modules/tendermint-lc/src/lib.rs +++ b/modules/tendermint-lc/src/lib.rs @@ -26,3 +26,4 @@ pub mod client; pub mod errors; pub mod message; pub mod state; +mod verifier; diff --git a/modules/tendermint-lc/src/verifier.rs b/modules/tendermint-lc/src/verifier.rs new file mode 100644 index 00000000..8a75cb61 --- /dev/null +++ b/modules/tendermint-lc/src/verifier.rs @@ -0,0 +1,232 @@ +use crate::prelude::*; +use ibc::{ + clients::ics07_tendermint::{ + client_state::ClientState, client_type, + consensus_state::ConsensusState as TmConsensusState, error::Error, + header::Header as TmHeader, + }, + core::{ + ics02_client::{ + client_state::{ClientState as Ics2ClientState, UpdatedState}, + consensus_state::ConsensusState, + error::ClientError, + }, + ics24_host::{identifier::ClientId, path::ClientConsensusStatePath}, + ContextError, ValidationContext, + }, +}; +use lcp_proto::google::protobuf::Any; +use tendermint_light_client_verifier::{ + types::{TrustedBlockState, UntrustedBlockState}, + ProdVerifier, Verdict, Verifier, +}; + +/// Fork of the `check_header_and_update_state` function from ibc-rs v0.29.0 +/// https://github.com/cosmos/ibc-rs/blob/10b47c077065a07ded9ac7f03fdb6c0980592d81/crates/ibc/src/clients/ics07_tendermint/client_state.rs#L457 +pub(crate) fn check_header_and_update_state( + client_state: &ClientState, + ctx: &dyn ValidationContext, + client_id: ClientId, + header: Any, +) -> Result { + fn maybe_consensus_state( + ctx: &dyn ValidationContext, + client_cons_state_path: &ClientConsensusStatePath, + ) -> Result>, ClientError> { + match ctx.consensus_state(client_cons_state_path) { + Ok(cs) => Ok(Some(cs)), + Err(e) => match e { + ContextError::ClientError(ClientError::ConsensusStateNotFound { + client_id: _, + height: _, + }) => Ok(None), + ContextError::ClientError(e) => Err(e), + _ => Err(ClientError::Other { + description: e.to_string(), + }), + }, + } + } + + let client_state = downcast_tm_client_state(client_state)?.clone(); + let header = TmHeader::try_from(header)?; + + if header.height().revision_number() != client_state.chain_id().version() { + return Err(ClientError::ClientSpecific { + description: Error::MismatchedRevisions { + current_revision: client_state.chain_id().version(), + update_revision: header.height().revision_number(), + } + .to_string(), + }); + } + + // Check if a consensus state is already installed; if so it should + // match the untrusted header. + let header_consensus_state = TmConsensusState::from(header.clone()); + let client_cons_state_path = ClientConsensusStatePath::new(&client_id, &header.height()); + let existing_consensus_state = match maybe_consensus_state(ctx, &client_cons_state_path)? { + Some(cs) => { + let cs = downcast_tm_consensus_state(cs.as_ref())?; + // If this consensus state matches, skip verification + // (optimization) + if cs == header_consensus_state { + // Header is already installed and matches the incoming + // header (already verified) + return Ok(UpdatedState { + client_state: client_state.into_box(), + consensus_state: cs.into_box(), + }); + } + Some(cs) + } + None => None, + }; + + let trusted_client_cons_state_path = + ClientConsensusStatePath::new(&client_id, &header.trusted_height); + let trusted_consensus_state = downcast_tm_consensus_state( + ctx.consensus_state(&trusted_client_cons_state_path) + .map_err(|e| match e { + ContextError::ClientError(e) => e, + _ => ClientError::Other { + description: e.to_string(), + }, + })? + .as_ref(), + )?; + + let trusted_state = TrustedBlockState { + chain_id: &client_state.chain_id.clone().into(), + 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_validator_set, + next_validators_hash: trusted_consensus_state.next_validators_hash, + }; + + 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 = client_state.as_light_client_options()?; + let now = ctx + .host_timestamp() + .map_err(|e| ClientError::Other { + description: e.to_string(), + })? + .into_tm_time() + .unwrap(); + + match ProdVerifier::default().verify(untrusted_state, trusted_state, &options, now) { + Verdict::Success => Ok(()), + Verdict::NotEnoughTrust(reason) => Err(Error::NotEnoughTrustedValsSigned { reason }), + Verdict::Invalid(detail) => Err(Error::VerificationError { detail }), + }?; + + // If the header has verified, but its corresponding consensus state + // differs from the existing consensus state for that height, freeze the + // client and return the installed consensus state. + if let Some(cs) = existing_consensus_state { + if cs != header_consensus_state { + return Ok(UpdatedState { + client_state: client_state.with_frozen_height(header.height()).into_box(), + consensus_state: cs.into_box(), + }); + } + } + + // Monotonicity checks for timestamps for in-the-middle updates + // (cs-new, cs-next, cs-latest) + if header.height() < client_state.latest_height() { + let maybe_next_cs = ctx + .next_consensus_state(&client_id, &header.height()) + .map_err(|e| match e { + ContextError::ClientError(e) => e, + _ => ClientError::Other { + description: e.to_string(), + }, + })? + .as_ref() + .map(|cs| downcast_tm_consensus_state(cs.as_ref())) + .transpose()?; + + if let Some(next_cs) = maybe_next_cs { + // New (untrusted) header timestamp cannot occur after next + // consensus state's height + if header.signed_header.header().time > next_cs.timestamp { + return Err(ClientError::ClientSpecific { + description: Error::HeaderTimestampTooHigh { + actual: header.signed_header.header().time.to_string(), + max: next_cs.timestamp.to_string(), + } + .to_string(), + }); + } + } + } + + // (cs-trusted, cs-prev, cs-new) + if header.trusted_height < header.height() { + let maybe_prev_cs = ctx + .prev_consensus_state(&client_id, &header.height()) + .map_err(|e| match e { + ContextError::ClientError(e) => e, + _ => ClientError::Other { + description: e.to_string(), + }, + })? + .as_ref() + .map(|cs| downcast_tm_consensus_state(cs.as_ref())) + .transpose()?; + + if let Some(prev_cs) = maybe_prev_cs { + // New (untrusted) header timestamp cannot occur before the + // previous consensus state's height + if header.signed_header.header().time < prev_cs.timestamp { + return Err(ClientError::ClientSpecific { + description: Error::HeaderTimestampTooLow { + actual: header.signed_header.header().time.to_string(), + min: prev_cs.timestamp.to_string(), + } + .to_string(), + }); + } + } + } + + Ok(UpdatedState { + client_state: client_state.with_header(header.clone())?.into_box(), + consensus_state: TmConsensusState::from(header).into_box(), + }) +} + +fn downcast_tm_client_state(cs: &dyn Ics2ClientState) -> Result<&ClientState, ClientError> { + cs.as_any() + .downcast_ref::() + .ok_or_else(|| ClientError::ClientArgsTypeMismatch { + client_type: client_type(), + }) +} + +fn downcast_tm_consensus_state(cs: &dyn ConsensusState) -> Result { + cs.as_any() + .downcast_ref::() + .ok_or_else(|| ClientError::ClientArgsTypeMismatch { + client_type: client_type(), + }) + .map(Clone::clone) +} From d4f7410de4e87b80dc96eb9b1b8818d943de468d Mon Sep 17 00:00:00 2001 From: Jun Kimura Date: Tue, 6 Feb 2024 01:02:46 +0900 Subject: [PATCH 2/2] tendermint-lc: remove some validations Signed-off-by: Jun Kimura --- modules/tendermint-lc/src/client.rs | 32 +------- modules/tendermint-lc/src/verifier.rs | 112 -------------------------- 2 files changed, 1 insertion(+), 143 deletions(-) diff --git a/modules/tendermint-lc/src/client.rs b/modules/tendermint-lc/src/client.rs index e515699f..40c724ac 100644 --- a/modules/tendermint-lc/src/client.rs +++ b/modules/tendermint-lc/src/client.rs @@ -36,6 +36,7 @@ use light_client::{ LightClientRegistry, UpdateClientResult, VerifyMembershipResult, }; use light_client::{MisbehaviourData, UpdateStateData, VerifyNonMembershipResult}; +#[allow(unused_imports)] use log::*; #[derive(Default)] @@ -239,37 +240,6 @@ impl TendermintLightClient { .into()); } - // Read consensus state from the host chain store. - let latest_consensus_state: ConsensusState = ctx - .consensus_state(&client_id, &client_state.latest_height().into()) - .map_err(|_| { - Error::ics02(ICS02Error::ConsensusStateNotFound { - client_id: client_id.clone().into(), - height: client_state.latest_height(), - }) - })? - .try_into()?; - - debug!("latest consensus state: {:?}", latest_consensus_state); - - let now = ctx.host_timestamp(); - let duration = now - .duration_since(latest_consensus_state.timestamp().into_tm_time().unwrap()) - .map_err(|_| { - Error::ics02(ICS02Error::InvalidConsensusStateTimestamp { - time1: latest_consensus_state.timestamp(), - time2: now.into(), - }) - })?; - - if client_state.expired(duration) { - return Err(Error::ics02(ICS02Error::HeaderNotWithinTrustPeriod { - latest_time: latest_consensus_state.timestamp(), - update_time: header.timestamp(), - }) - .into()); - } - let height = header.height().into(); let header_timestamp: Time = header.timestamp().into(); diff --git a/modules/tendermint-lc/src/verifier.rs b/modules/tendermint-lc/src/verifier.rs index 8a75cb61..49ad560a 100644 --- a/modules/tendermint-lc/src/verifier.rs +++ b/modules/tendermint-lc/src/verifier.rs @@ -29,25 +29,6 @@ pub(crate) fn check_header_and_update_state( client_id: ClientId, header: Any, ) -> Result { - fn maybe_consensus_state( - ctx: &dyn ValidationContext, - client_cons_state_path: &ClientConsensusStatePath, - ) -> Result>, ClientError> { - match ctx.consensus_state(client_cons_state_path) { - Ok(cs) => Ok(Some(cs)), - Err(e) => match e { - ContextError::ClientError(ClientError::ConsensusStateNotFound { - client_id: _, - height: _, - }) => Ok(None), - ContextError::ClientError(e) => Err(e), - _ => Err(ClientError::Other { - description: e.to_string(), - }), - }, - } - } - let client_state = downcast_tm_client_state(client_state)?.clone(); let header = TmHeader::try_from(header)?; @@ -61,28 +42,6 @@ pub(crate) fn check_header_and_update_state( }); } - // Check if a consensus state is already installed; if so it should - // match the untrusted header. - let header_consensus_state = TmConsensusState::from(header.clone()); - let client_cons_state_path = ClientConsensusStatePath::new(&client_id, &header.height()); - let existing_consensus_state = match maybe_consensus_state(ctx, &client_cons_state_path)? { - Some(cs) => { - let cs = downcast_tm_consensus_state(cs.as_ref())?; - // If this consensus state matches, skip verification - // (optimization) - if cs == header_consensus_state { - // Header is already installed and matches the incoming - // header (already verified) - return Ok(UpdatedState { - client_state: client_state.into_box(), - consensus_state: cs.into_box(), - }); - } - Some(cs) - } - None => None, - }; - let trusted_client_cons_state_path = ClientConsensusStatePath::new(&client_id, &header.trusted_height); let trusted_consensus_state = downcast_tm_consensus_state( @@ -137,77 +96,6 @@ pub(crate) fn check_header_and_update_state( Verdict::Invalid(detail) => Err(Error::VerificationError { detail }), }?; - // If the header has verified, but its corresponding consensus state - // differs from the existing consensus state for that height, freeze the - // client and return the installed consensus state. - if let Some(cs) = existing_consensus_state { - if cs != header_consensus_state { - return Ok(UpdatedState { - client_state: client_state.with_frozen_height(header.height()).into_box(), - consensus_state: cs.into_box(), - }); - } - } - - // Monotonicity checks for timestamps for in-the-middle updates - // (cs-new, cs-next, cs-latest) - if header.height() < client_state.latest_height() { - let maybe_next_cs = ctx - .next_consensus_state(&client_id, &header.height()) - .map_err(|e| match e { - ContextError::ClientError(e) => e, - _ => ClientError::Other { - description: e.to_string(), - }, - })? - .as_ref() - .map(|cs| downcast_tm_consensus_state(cs.as_ref())) - .transpose()?; - - if let Some(next_cs) = maybe_next_cs { - // New (untrusted) header timestamp cannot occur after next - // consensus state's height - if header.signed_header.header().time > next_cs.timestamp { - return Err(ClientError::ClientSpecific { - description: Error::HeaderTimestampTooHigh { - actual: header.signed_header.header().time.to_string(), - max: next_cs.timestamp.to_string(), - } - .to_string(), - }); - } - } - } - - // (cs-trusted, cs-prev, cs-new) - if header.trusted_height < header.height() { - let maybe_prev_cs = ctx - .prev_consensus_state(&client_id, &header.height()) - .map_err(|e| match e { - ContextError::ClientError(e) => e, - _ => ClientError::Other { - description: e.to_string(), - }, - })? - .as_ref() - .map(|cs| downcast_tm_consensus_state(cs.as_ref())) - .transpose()?; - - if let Some(prev_cs) = maybe_prev_cs { - // New (untrusted) header timestamp cannot occur before the - // previous consensus state's height - if header.signed_header.header().time < prev_cs.timestamp { - return Err(ClientError::ClientSpecific { - description: Error::HeaderTimestampTooLow { - actual: header.signed_header.header().time.to_string(), - min: prev_cs.timestamp.to_string(), - } - .to_string(), - }); - } - } - } - Ok(UpdatedState { client_state: client_state.with_header(header.clone())?.into_box(), consensus_state: TmConsensusState::from(header).into_box(),