Skip to content

Commit

Permalink
tendermint-lc: fork check_header_and_update_state function from ibc-rs
Browse files Browse the repository at this point in the history
Signed-off-by: Jun Kimura <[email protected]>
  • Loading branch information
bluele committed Feb 5, 2024
1 parent e594d36 commit efbbf26
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 11 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions enclave/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions modules/tendermint-lc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
23 changes: 12 additions & 11 deletions modules/tendermint-lc/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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::<TendermintClientState, TendermintConsensusState>::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::<TendermintClientState, TendermintConsensusState>::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::<TendermintClientState>(new_client_state.as_ref())
Expand Down
1 change: 1 addition & 0 deletions modules/tendermint-lc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ pub mod client;
pub mod errors;
pub mod message;
pub mod state;
mod verifier;
232 changes: 232 additions & 0 deletions modules/tendermint-lc/src/verifier.rs
Original file line number Diff line number Diff line change
@@ -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<UpdatedState, ClientError> {
fn maybe_consensus_state(
ctx: &dyn ValidationContext,
client_cons_state_path: &ClientConsensusStatePath,
) -> Result<Option<Box<dyn ConsensusState>>, 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::<ClientState>()
.ok_or_else(|| ClientError::ClientArgsTypeMismatch {
client_type: client_type(),
})
}

fn downcast_tm_consensus_state(cs: &dyn ConsensusState) -> Result<TmConsensusState, ClientError> {
cs.as_any()
.downcast_ref::<TmConsensusState>()
.ok_or_else(|| ClientError::ClientArgsTypeMismatch {
client_type: client_type(),
})
.map(Clone::clone)
}

0 comments on commit efbbf26

Please sign in to comment.