diff --git a/.changelog/unreleased/breaking-changes/ibc/1214-ics07.md b/.changelog/unreleased/breaking-changes/ibc/1214-ics07.md new file mode 100644 index 0000000000..42080cdefc --- /dev/null +++ b/.changelog/unreleased/breaking-changes/ibc/1214-ics07.md @@ -0,0 +1,3 @@ +- The `check_header_and_update_state` method of the `ClientDef` + trait (ICS02) has been expanded to facilitate ICS07 + ([#1214](https://github.com/informalsystems/ibc-rs/issues/1214)) \ No newline at end of file diff --git a/.changelog/unreleased/features/ibc/1214-ics07.md b/.changelog/unreleased/features/ibc/1214-ics07.md new file mode 100644 index 0000000000..460b04b45a --- /dev/null +++ b/.changelog/unreleased/features/ibc/1214-ics07.md @@ -0,0 +1,2 @@ +- Add ICS07 verification functionality by using `tendermint-light-client` + ([#1214](https://github.com/informalsystems/ibc-rs/issues/1214)) diff --git a/Cargo.lock b/Cargo.lock index 724ceb6feb..53de095be3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1300,6 +1300,7 @@ dependencies = [ "sha2", "subtle-encoding", "tendermint", + "tendermint-light-client", "tendermint-proto", "tendermint-rpc", "tendermint-testgen", diff --git a/modules/Cargo.toml b/modules/Cargo.toml index b0f1c742b8..7dc559213c 100644 --- a/modules/Cargo.toml +++ b/modules/Cargo.toml @@ -17,9 +17,10 @@ description = """ default = ["std", "eyre_tracer"] std = ["flex-error/std"] eyre_tracer = ["flex-error/eyre_tracer"] + # This feature grants access to development-time mocking libraries, such as `MockContext` or `MockHeader`. # Depends on the `testgen` suite for generating Tendermint light blocks. -mocks = [ "tendermint-testgen", "sha2" ] +mocks = ["tendermint-testgen", "sha2"] [dependencies] # Proto definitions for all IBC-related interfaces, e.g., connections or channels. @@ -45,6 +46,10 @@ version = "=0.22.0" [dependencies.tendermint-proto] version = "=0.22.0" +[dependencies.tendermint-light-client] +version = "=0.22.0" +default-features = false + [dependencies.tendermint-testgen] version = "=0.22.0" optional = true diff --git a/modules/src/application/ics20_fungible_token_transfer/msgs/transfer.rs b/modules/src/application/ics20_fungible_token_transfer/msgs/transfer.rs index d5eec5c87a..336b5c2586 100644 --- a/modules/src/application/ics20_fungible_token_transfer/msgs/transfer.rs +++ b/modules/src/application/ics20_fungible_token_transfer/msgs/transfer.rs @@ -101,14 +101,17 @@ impl From for RawMsgTransfer { #[cfg(test)] pub mod test_util { + use std::ops::Add; + use std::time::Duration; + use crate::{ ics24_host::identifier::{ChannelId, PortId}, test_utils::get_dummy_account_id, + timestamp::Timestamp, Height, }; use super::MsgTransfer; - use crate::timestamp::Timestamp; // Returns a dummy `RawMsgTransfer`, for testing only! pub fn get_dummy_msg_transfer(height: u64) -> MsgTransfer { @@ -120,7 +123,7 @@ pub mod test_util { token: None, sender: id.clone(), receiver: id, - timeout_timestamp: Timestamp::from_nanoseconds(1).unwrap(), + timeout_timestamp: Timestamp::now().add(Duration::from_secs(10)).unwrap(), timeout_height: Height { revision_number: 0, revision_height: height, diff --git a/modules/src/ics02_client/client_def.rs b/modules/src/ics02_client/client_def.rs index d19adc2f7c..d97f8fa6bb 100644 --- a/modules/src/ics02_client/client_def.rs +++ b/modules/src/ics02_client/client_def.rs @@ -4,6 +4,7 @@ use crate::downcast; use crate::ics02_client::client_consensus::{AnyConsensusState, ConsensusState}; use crate::ics02_client::client_state::{AnyClientState, ClientState}; use crate::ics02_client::client_type::ClientType; +use crate::ics02_client::context::ClientReader; use crate::ics02_client::error::Error; use crate::ics02_client::header::{AnyHeader, Header}; use crate::ics03_connection::connection::ConnectionEnd; @@ -23,13 +24,15 @@ pub trait ClientDef: Clone { type ClientState: ClientState; type ConsensusState: ConsensusState; - /// TODO fn check_header_and_update_state( &self, + ctx: &dyn ClientReader, + client_id: ClientId, client_state: Self::ClientState, header: Self::Header, ) -> Result<(Self::ClientState, Self::ConsensusState), Error>; + /// TODO fn verify_upgrade_and_update_state( &self, client_state: &Self::ClientState, @@ -156,7 +159,7 @@ pub enum AnyClient { impl AnyClient { pub fn from_client_type(client_type: ClientType) -> AnyClient { match client_type { - ClientType::Tendermint => Self::Tendermint(TendermintClient), + ClientType::Tendermint => Self::Tendermint(TendermintClient::default()), #[cfg(any(test, feature = "mocks"))] ClientType::Mock => Self::Mock(MockClient), @@ -173,6 +176,8 @@ impl ClientDef for AnyClient { /// Validates an incoming `header` against the latest consensus state of this client. fn check_header_and_update_state( &self, + ctx: &dyn ClientReader, + client_id: ClientId, client_state: AnyClientState, header: AnyHeader, ) -> Result<(AnyClientState, AnyConsensusState), Error> { @@ -185,7 +190,7 @@ impl ClientDef for AnyClient { .ok_or_else(|| Error::client_args_type_mismatch(ClientType::Tendermint))?; let (new_state, new_consensus) = - client.check_header_and_update_state(client_state, header)?; + client.check_header_and_update_state(ctx, client_id, client_state, header)?; Ok(( AnyClientState::Tendermint(new_state), @@ -202,7 +207,7 @@ impl ClientDef for AnyClient { .ok_or_else(|| Error::client_args_type_mismatch(ClientType::Mock))?; let (new_state, new_consensus) = - client.check_header_and_update_state(client_state, header)?; + client.check_header_and_update_state(ctx, client_id, client_state, header)?; Ok(( AnyClientState::Mock(new_state), diff --git a/modules/src/ics02_client/context.rs b/modules/src/ics02_client/context.rs index 611d4ae8de..aaf08ee1d4 100644 --- a/modules/src/ics02_client/context.rs +++ b/modules/src/ics02_client/context.rs @@ -5,7 +5,7 @@ use crate::ics02_client::client_consensus::AnyConsensusState; use crate::ics02_client::client_state::AnyClientState; use crate::ics02_client::client_type::ClientType; -use crate::ics02_client::error::Error; +use crate::ics02_client::error::{Error, ErrorDetail}; use crate::ics02_client::handler::ClientResult::{self, Create, Update, Upgrade}; use crate::ics24_host::identifier::ClientId; use crate::Height; @@ -14,12 +14,47 @@ use crate::Height; pub trait ClientReader { fn client_type(&self, client_id: &ClientId) -> Result; fn client_state(&self, client_id: &ClientId) -> Result; + + /// Retrieve the consensus state for the given client ID at the specified + /// height. + /// + /// Returns an error if no such state exists. fn consensus_state( &self, client_id: &ClientId, height: Height, ) -> Result; + /// Similar to `consensus_state`, attempt to retrieve the consensus state, + /// but return `None` if no state exists at the given height. + fn maybe_consensus_state( + &self, + client_id: &ClientId, + height: Height, + ) -> Result, Error> { + match self.consensus_state(client_id, height) { + Ok(cs) => Ok(Some(cs)), + Err(e) => match e.detail() { + ErrorDetail::ConsensusStateNotFound(_) => Ok(None), + _ => Err(e), + }, + } + } + + /// Search for the lowest consensus state higher than `height`. + fn next_consensus_state( + &self, + client_id: &ClientId, + height: Height, + ) -> Result, Error>; + + /// Search for the highest consensus state lower than `height`. + fn prev_consensus_state( + &self, + client_id: &ClientId, + height: Height, + ) -> Result, Error>; + /// Returns a natural number, counting how many clients have been created thus far. /// The value of this counter should increase only via method `ClientKeeper::increase_client_counter`. fn client_counter(&self) -> Result; diff --git a/modules/src/ics02_client/error.rs b/modules/src/ics02_client/error.rs index 4eb0c54957..5839a44c78 100644 --- a/modules/src/ics02_client/error.rs +++ b/modules/src/ics02_client/error.rs @@ -9,8 +9,12 @@ use crate::ics07_tendermint::error::Error as Ics07Error; use crate::ics23_commitment::error::Error as Ics23Error; use crate::ics24_host::error::ValidationError; use crate::ics24_host::identifier::ClientId; +use crate::timestamp::Timestamp; use crate::Height; +use tendermint::Error as TendermintError; +use tendermint_proto::Error as TendermintProtoError; + define_error! { #[derive(Debug, PartialEq, Eq)] Error { @@ -58,7 +62,7 @@ define_error! { FailedTrustThresholdConversion { numerator: u64, denominator: u64 } - [ tendermint::Error ] + [ TendermintError ] | e | { format_args!("failed to build Tendermint domain type trust threshold from fraction: {}/{}", e.numerator, e.denominator) }, UnknownClientStateType @@ -105,14 +109,14 @@ define_error! { }, DecodeRawClientState - [ TraceError ] + [ TraceError ] | _ | { "error decoding raw client state" }, MissingRawClientState | _ | { "missing raw client state" }, InvalidRawConsensusState - [ TraceError ] + [ TraceError ] | _ | { "invalid raw client consensus state" }, MissingRawConsensusState @@ -134,14 +138,14 @@ define_error! { | _ | { "invalid client identifier" }, InvalidRawHeader - [ TraceError ] + [ TraceError ] | _ | { "invalid raw header" }, MissingRawHeader | _ | { "missing raw header" }, DecodeRawMisbehaviour - [ TraceError ] + [ TraceError ] | _ | { "invalid raw misbehaviour" }, InvalidRawMisbehaviour @@ -180,6 +184,12 @@ define_error! { e.client_type) }, + InsufficientVotingPower + { reason: String } + | e | { + format_args!("Insufficient overlap {}", e.reason) + }, + RawClientAndConsensusStateTypesMismatch { state_type: ClientType, @@ -206,8 +216,37 @@ define_error! { client_height: Height, } | e | { - format_args!("upgraded client height {0} must be at greater than current client height {1}", + format_args!("upgraded client height {} must be at greater than current client height {}", e.upgraded_height, e.client_height) }, + + InvalidConsensusStateTimestamp + { + time1: Timestamp, + time2: Timestamp, + } + | e | { + format_args!("timestamp is invalid or missing, timestamp={0}, now={1}", e.time1, e.time2) + }, + + HeaderNotWithinTrustPeriod + { + latest_time:Timestamp, + update_time: Timestamp, + } + | e | { + format_args!("header not withing trusting period: expires_at={0} now={1}", e.latest_time, e.update_time) + }, + + TendermintHandlerError + [ Ics07Error ] + | _ | { format_args!("Tendermint-specific handler error") }, + + } +} + +impl From for Error { + fn from(e: Ics07Error) -> Error { + Error::tendermint_handler_error(e) } } diff --git a/modules/src/ics02_client/handler/update_client.rs b/modules/src/ics02_client/handler/update_client.rs index 0c993d28f4..94aa2fcb84 100644 --- a/modules/src/ics02_client/handler/update_client.rs +++ b/modules/src/ics02_client/handler/update_client.rs @@ -1,17 +1,21 @@ //! Protocol logic specific to processing ICS2 messages of type `MsgUpdateAnyClient`. +use tracing::debug; + use crate::events::IbcEvent; use crate::handler::{HandlerOutput, HandlerResult}; use crate::ics02_client::client_consensus::AnyConsensusState; use crate::ics02_client::client_def::{AnyClient, ClientDef}; -use crate::ics02_client::client_state::AnyClientState; +use crate::ics02_client::client_state::{AnyClientState, ClientState}; use crate::ics02_client::context::ClientReader; use crate::ics02_client::error::Error; use crate::ics02_client::events::Attributes; use crate::ics02_client::handler::ClientResult; +use crate::ics02_client::header::Header; use crate::ics02_client::msgs::update_client::MsgUpdateAnyClient; use crate::ics24_host::identifier::ClientId; use crate::prelude::*; +use crate::timestamp::Timestamp; /// The result following the successful processing of a `MsgUpdateAnyClient` message. Preferably /// this data type should be used with a qualified name `update_client::Result` to avoid ambiguity. @@ -42,14 +46,40 @@ pub fn process( // Read client state from the host chain store. let client_state = ctx.client_state(&client_id)?; - let latest_height = client_state.latest_height(); - ctx.consensus_state(&client_id, latest_height)?; + if client_state.is_frozen() { + return Err(Error::client_frozen(client_id)); + } + + // Read consensus state from the host chain store. + let latest_consensus_state = ctx + .consensus_state(&client_id, client_state.latest_height()) + .map_err(|_| { + Error::consensus_state_not_found(client_id.clone(), client_state.latest_height()) + })?; + + debug!("latest consensus state: {:?}", latest_consensus_state); + + let duration = Timestamp::now() + .duration_since(&latest_consensus_state.timestamp()) + .ok_or_else(|| { + Error::invalid_consensus_state_timestamp( + latest_consensus_state.timestamp(), + header.timestamp(), + ) + })?; + + if client_state.expired(duration) { + return Err(Error::header_not_within_trust_period( + latest_consensus_state.timestamp(), + header.timestamp(), + )); + } // Use client_state to validate the new header against the latest consensus_state. // This function will return the new client_state (its latest_height changed) and a // consensus_state obtained from header. These will be later persisted by the keeper. let (new_client_state, new_consensus_state) = client_def - .check_header_and_update_state(client_state, header) + .check_header_and_update_state(ctx, client_id.clone(), client_state, header) .map_err(|e| Error::header_verification_failure(e.to_string()))?; let result = ClientResult::Update(Result { @@ -74,19 +104,23 @@ mod tests { use crate::events::IbcEvent; use crate::handler::HandlerOutput; - use crate::ics02_client::client_state::AnyClientState; + use crate::ics02_client::client_consensus::AnyConsensusState; + use crate::ics02_client::client_state::{AnyClientState, ClientState}; + use crate::ics02_client::client_type::ClientType; use crate::ics02_client::error::{Error, ErrorDetail}; use crate::ics02_client::handler::dispatch; use crate::ics02_client::handler::ClientResult::Update; - use crate::ics02_client::header::Header; + use crate::ics02_client::header::{AnyHeader, Header}; use crate::ics02_client::msgs::update_client::MsgUpdateAnyClient; use crate::ics02_client::msgs::ClientMsg; - use crate::ics24_host::identifier::ClientId; + use crate::ics24_host::identifier::{ChainId, ClientId}; use crate::mock::client_state::MockClientState; use crate::mock::context::MockContext; use crate::mock::header::MockHeader; + use crate::mock::host::HostType; use crate::prelude::*; use crate::test_utils::get_dummy_account_id; + use crate::timestamp::Timestamp; use crate::Height; #[test] @@ -94,10 +128,14 @@ mod tests { let client_id = ClientId::default(); let signer = get_dummy_account_id(); + let timestamp = Timestamp::now(); + let ctx = MockContext::default().with_client(&client_id, Height::new(0, 42)); let msg = MsgUpdateAnyClient { client_id: client_id.clone(), - header: MockHeader::new(Height::new(0, 46)).into(), + header: MockHeader::new(Height::new(0, 46)) + .with_timestamp(timestamp) + .into(), signer, }; @@ -121,9 +159,9 @@ mod tests { assert_eq!(upd_res.client_id, client_id); assert_eq!( upd_res.client_state, - AnyClientState::Mock(MockClientState(MockHeader::new( - msg.header.height() - ))) + AnyClientState::Mock(MockClientState( + MockHeader::new(msg.header.height()).with_timestamp(timestamp) + )) ) } _ => panic!("update handler result has incorrect type"), @@ -205,4 +243,293 @@ mod tests { } } } + + #[test] + fn test_update_synthetic_tendermint_client_adjacent_ok() { + let client_id = ClientId::new(ClientType::Tendermint, 0).unwrap(); + let client_height = Height::new(1, 20); + let update_height = Height::new(1, 21); + + let ctx = MockContext::new( + ChainId::new("mockgaiaA".to_string(), 1), + HostType::Mock, + 5, + Height::new(1, 1), + ) + .with_client_parametrized( + &client_id, + client_height, + Some(ClientType::Tendermint), // The target host chain (B) is synthetic TM. + Some(client_height), + ); + + let ctx_b = MockContext::new( + ChainId::new("mockgaiaB".to_string(), 1), + HostType::SyntheticTendermint, + 5, + update_height, + ); + + let signer = get_dummy_account_id(); + + let block_ref = ctx_b.host_block(update_height); + let mut latest_header: AnyHeader = block_ref.cloned().map(Into::into).unwrap(); + + latest_header = match latest_header { + AnyHeader::Tendermint(mut theader) => { + theader.trusted_height = client_height; + AnyHeader::Tendermint(theader) + } + AnyHeader::Mock(m) => AnyHeader::Mock(m), + }; + + let msg = MsgUpdateAnyClient { + client_id: client_id.clone(), + header: latest_header, + signer, + }; + + let output = dispatch(&ctx, ClientMsg::UpdateClient(msg.clone())); + + match output { + Ok(HandlerOutput { + result, + mut events, + log, + }) => { + assert_eq!(events.len(), 1); + let event = events.pop().unwrap(); + assert!( + matches!(event, IbcEvent::UpdateClient(e) if e.client_id() == &msg.client_id) + ); + assert!(log.is_empty()); + // Check the result + match result { + Update(upd_res) => { + assert_eq!(upd_res.client_id, client_id); + assert!(!upd_res.client_state.is_frozen()); + assert_eq!(upd_res.client_state.latest_height(), msg.header.height(),) + } + _ => panic!("update handler result has incorrect type"), + } + } + Err(err) => { + panic!("unexpected error: {}", err); + } + } + } + + #[test] + fn test_update_synthetic_tendermint_client_non_adjacent_ok() { + let client_id = ClientId::new(ClientType::Tendermint, 0).unwrap(); + let client_height = Height::new(1, 20); + let update_height = Height::new(1, 21); + + let ctx = MockContext::new( + ChainId::new("mockgaiaA".to_string(), 1), + HostType::Mock, + 5, + Height::new(1, 1), + ) + .with_client_parametrized_history( + &client_id, + client_height, + Some(ClientType::Tendermint), // The target host chain (B) is synthetic TM. + Some(client_height), + ); + + let ctx_b = MockContext::new( + ChainId::new("mockgaiaB".to_string(), 1), + HostType::SyntheticTendermint, + 5, + update_height, + ); + + let signer = get_dummy_account_id(); + + let block_ref = ctx_b.host_block(update_height); + let mut latest_header: AnyHeader = block_ref.cloned().map(Into::into).unwrap(); + + let trusted_height = client_height.clone().sub(1).unwrap_or_default(); + + latest_header = match latest_header { + AnyHeader::Tendermint(mut theader) => { + theader.trusted_height = trusted_height; + AnyHeader::Tendermint(theader) + } + AnyHeader::Mock(m) => AnyHeader::Mock(m), + }; + + let msg = MsgUpdateAnyClient { + client_id: client_id.clone(), + header: latest_header, + signer, + }; + + let output = dispatch(&ctx, ClientMsg::UpdateClient(msg.clone())); + + match output { + Ok(HandlerOutput { + result, + mut events, + log, + }) => { + assert_eq!(events.len(), 1); + let event = events.pop().unwrap(); + assert!( + matches!(event, IbcEvent::UpdateClient(e) if e.client_id() == &msg.client_id) + ); + assert!(log.is_empty()); + // Check the result + match result { + Update(upd_res) => { + assert_eq!(upd_res.client_id, client_id); + assert!(!upd_res.client_state.is_frozen()); + assert_eq!(upd_res.client_state.latest_height(), msg.header.height(),) + } + _ => panic!("update handler result has incorrect type"), + } + } + Err(err) => { + panic!("unexpected error: {}", err); + } + } + } + + #[test] + fn test_update_synthetic_tendermint_client_duplicate_ok() { + let client_id = ClientId::new(ClientType::Tendermint, 0).unwrap(); + let client_height = Height::new(1, 20); + + let chain_start_height = Height::new(1, 11); + + let ctx = MockContext::new( + ChainId::new("mockgaiaA".to_string(), 1), + HostType::Mock, + 5, + chain_start_height, + ) + .with_client_parametrized( + &client_id, + client_height, + Some(ClientType::Tendermint), // The target host chain (B) is synthetic TM. + Some(client_height), + ); + + let ctx_b = MockContext::new( + ChainId::new("mockgaiaB".to_string(), 1), + HostType::SyntheticTendermint, + 5, + client_height, + ); + + let signer = get_dummy_account_id(); + + let block_ref = ctx_b.host_block(client_height); + let latest_header: AnyHeader = match block_ref.cloned().map(Into::into).unwrap() { + AnyHeader::Tendermint(mut theader) => { + let cons_state = ctx + .latest_consensus_states(&client_id, &client_height) + .clone(); + if let AnyConsensusState::Tendermint(tcs) = cons_state { + theader.signed_header.header.time = tcs.timestamp; + theader.trusted_height = Height::new(1, 11) + } + AnyHeader::Tendermint(theader) + } + AnyHeader::Mock(header) => AnyHeader::Mock(header), + }; + + let msg = MsgUpdateAnyClient { + client_id: client_id.clone(), + header: latest_header, + signer, + }; + + let output = dispatch(&ctx, ClientMsg::UpdateClient(msg.clone())); + + match output { + Ok(HandlerOutput { + result, + mut events, + log, + }) => { + assert_eq!(events.len(), 1); + let event = events.pop().unwrap(); + assert!( + matches!(event, IbcEvent::UpdateClient(e) if e.client_id() == &msg.client_id) + ); + assert!(log.is_empty()); + // Check the result + match result { + Update(upd_res) => { + assert_eq!(upd_res.client_id, client_id); + assert!(!upd_res.client_state.is_frozen()); + assert_eq!( + upd_res.client_state, + ctx.latest_client_states(&client_id).clone() + ); + assert_eq!(upd_res.client_state.latest_height(), msg.header.height(),) + } + _ => panic!("update handler result has incorrect type"), + } + } + Err(err) => { + panic!("unexpected error: {:?}", err); + } + } + } + + #[test] + fn test_update_synthetic_tendermint_client_lower_height() { + let client_id = ClientId::new(ClientType::Tendermint, 0).unwrap(); + let client_height = Height::new(1, 20); + + let client_update_height = Height::new(1, 19); + + let chain_start_height = Height::new(1, 11); + + let ctx = MockContext::new( + ChainId::new("mockgaiaA".to_string(), 1), + HostType::Mock, + 5, + chain_start_height, + ) + .with_client_parametrized( + &client_id, + client_height, + Some(ClientType::Tendermint), // The target host chain (B) is synthetic TM. + Some(client_height), + ); + + let ctx_b = MockContext::new( + ChainId::new("mockgaiaB".to_string(), 1), + HostType::SyntheticTendermint, + 5, + client_height, + ); + + let signer = get_dummy_account_id(); + + let block_ref = ctx_b.host_block(client_update_height); + let latest_header: AnyHeader = block_ref.cloned().map(Into::into).unwrap(); + + let msg = MsgUpdateAnyClient { + client_id, + header: latest_header, + signer, + }; + + let output = dispatch(&ctx, ClientMsg::UpdateClient(msg)); + + match output { + Ok(_) => { + panic!("update handler result has incorrect type"); + } + Err(err) => match err.detail() { + ErrorDetail::HeaderVerificationFailure(_) => {} + _ => panic!("unexpected error: {:?}", err), + }, + } + } } diff --git a/modules/src/ics02_client/header.rs b/modules/src/ics02_client/header.rs index caf23d598c..675b375a22 100644 --- a/modules/src/ics02_client/header.rs +++ b/modules/src/ics02_client/header.rs @@ -10,6 +10,7 @@ use crate::ics02_client::error::Error; use crate::ics07_tendermint::header::{decode_header, Header as TendermintHeader}; #[cfg(any(test, feature = "mocks"))] use crate::mock::header::MockHeader; +use crate::timestamp::Timestamp; use crate::Height; pub const TENDERMINT_HEADER_TYPE_URL: &str = "/ibc.lightclients.tendermint.v1.Header"; @@ -23,6 +24,9 @@ pub trait Header: Clone + core::fmt::Debug + Send + Sync { /// The height of the consensus state fn height(&self) -> Height; + /// The timestamp of the consensus state + fn timestamp(&self) -> Timestamp; + /// Wrap into an `AnyHeader` fn wrap_any(self) -> AnyHeader; } @@ -55,6 +59,15 @@ impl Header for AnyHeader { } } + fn timestamp(&self) -> Timestamp { + match self { + Self::Tendermint(header) => header.timestamp(), + + #[cfg(any(test, feature = "mocks"))] + Self::Mock(header) => header.timestamp, + } + } + fn wrap_any(self) -> AnyHeader { self } diff --git a/modules/src/ics04_channel/handler/send_packet.rs b/modules/src/ics04_channel/handler/send_packet.rs index 603b4a9288..0b118a21d8 100644 --- a/modules/src/ics04_channel/handler/send_packet.rs +++ b/modules/src/ics04_channel/handler/send_packet.rs @@ -127,7 +127,10 @@ mod tests { use crate::ics04_channel::packet::Packet; use crate::ics24_host::identifier::{ChannelId, ClientId, ConnectionId, PortId}; use crate::mock::context::MockContext; + use crate::timestamp::Timestamp; use crate::timestamp::ZERO_DURATION; + use std::ops::Add; + use std::time::Duration; #[test] fn send_packet_processing() { @@ -140,7 +143,11 @@ mod tests { let context = MockContext::default(); - let mut packet: Packet = get_dummy_raw_packet(1, 6).try_into().unwrap(); + let timestamp = Timestamp::now().add(Duration::from_secs(10)); + //CD:TODO remove unwrap + let mut packet: Packet = get_dummy_raw_packet(1, timestamp.unwrap().as_nanoseconds()) + .try_into() + .unwrap(); packet.sequence = 1.into(); packet.data = vec![0]; diff --git a/modules/src/ics07_tendermint/client_def.rs b/modules/src/ics07_tendermint/client_def.rs index 2cb6c9d756..a3b47bc97f 100644 --- a/modules/src/ics07_tendermint/client_def.rs +++ b/modules/src/ics07_tendermint/client_def.rs @@ -1,23 +1,44 @@ +use std::convert::TryInto; + use ibc_proto::ibc::core::commitment::v1::MerkleProof; +use tendermint::Time; +use tendermint_light_client::components::verifier::{ProdVerifier, Verdict, Verifier}; +use tendermint_light_client::types::{TrustedBlockState, UntrustedBlockState}; use crate::ics02_client::client_consensus::AnyConsensusState; use crate::ics02_client::client_def::ClientDef; use crate::ics02_client::client_state::AnyClientState; -use crate::ics02_client::error::Error; +use crate::ics02_client::client_type::ClientType; +use crate::ics02_client::context::ClientReader; +use crate::ics02_client::error::Error as Ics02Error; use crate::ics03_connection::connection::ConnectionEnd; use crate::ics04_channel::channel::ChannelEnd; use crate::ics04_channel::packet::Sequence; use crate::ics07_tendermint::client_state::ClientState; use crate::ics07_tendermint::consensus_state::ConsensusState; +use crate::ics07_tendermint::error::Error; use crate::ics07_tendermint::header::Header; + use crate::ics23_commitment::commitment::{CommitmentPrefix, CommitmentProofBytes, CommitmentRoot}; use crate::ics24_host::identifier::ConnectionId; use crate::ics24_host::identifier::{ChannelId, ClientId, PortId}; use crate::prelude::*; use crate::Height; +use crate::downcast; + #[derive(Clone, Debug, PartialEq, Eq)] -pub struct TendermintClient; +pub struct TendermintClient { + verifier: ProdVerifier, +} + +impl Default for TendermintClient { + fn default() -> Self { + Self { + verifier: ProdVerifier::default(), + } + } +} impl ClientDef for TendermintClient { type Header = Header; @@ -26,17 +47,137 @@ impl ClientDef for TendermintClient { fn check_header_and_update_state( &self, + ctx: &dyn ClientReader, + client_id: ClientId, client_state: Self::ClientState, header: Self::Header, - ) -> Result<(Self::ClientState, Self::ConsensusState), Error> { - if client_state.latest_height() >= header.height() { - return Err(Error::low_header_height( - header.height(), - client_state.latest_height(), + ) -> Result<(Self::ClientState, Self::ConsensusState), Ics02Error> { + if header.height().revision_number != client_state.chain_id.version() { + return Err(Ics02Error::tendermint_handler_error( + Error::mismatched_revisions( + client_state.chain_id.version(), + header.height().revision_number, + ), )); } - // TODO: Additional verifications should be implemented here. + // Check if a consensus state is already installed; if so it should + // match the untrusted header. + let header_consensus_state = ConsensusState::from(header.clone()); + let existing_consensus_state = + match ctx.maybe_consensus_state(&client_id, header.height())? { + Some(cs) => { + let cs = downcast_consensus_state(cs)?; + // 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((client_state, cs)); + } + Some(cs) + } + None => None, + }; + + let trusted_consensus_state = + downcast_consensus_state(ctx.consensus_state(&client_id, header.trusted_height)?)?; + + let trusted_state = TrustedBlockState { + header_time: trusted_consensus_state.timestamp, + height: header + .trusted_height + .revision_height + .try_into() + .map_err(|_| { + Ics02Error::tendermint_handler_error(Error::invalid_header_height( + header.trusted_height, + )) + })?, + 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()?; + + match self + .verifier + .verify(untrusted_state, trusted_state, &options, Time::now()) + { + Verdict::Success => {} + Verdict::NotEnoughTrust(voting_power_tally) => { + return Err(Error::not_enough_trusted_vals_signed(format!( + "voting power tally: {}", + voting_power_tally + )) + .into()) + } + Verdict::Invalid(detail) => { + return Err(Ics02Error::tendermint_handler_error( + Error::verification_error(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((client_state.with_set_frozen(header.height()), cs)); + } + } + + // 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(downcast_consensus_state) + .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(Ics02Error::tendermint_handler_error( + Error::header_timestamp_too_high( + header.signed_header.header().time.to_string(), + next_cs.timestamp.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(downcast_consensus_state) + .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(Ics02Error::tendermint_handler_error( + Error::header_timestamp_too_low( + header.signed_header.header().time.to_string(), + prev_cs.timestamp.to_string(), + ), + )); + } + } + } Ok(( client_state.with_header(header.clone()), @@ -53,7 +194,7 @@ impl ClientDef for TendermintClient { _client_id: &ClientId, _consensus_height: Height, _expected_consensus_state: &AnyConsensusState, - ) -> Result<(), Error> { + ) -> Result<(), Ics02Error> { todo!() } @@ -65,7 +206,7 @@ impl ClientDef for TendermintClient { _proof: &CommitmentProofBytes, _connection_id: Option<&ConnectionId>, _expected_connection_end: &ConnectionEnd, - ) -> Result<(), Error> { + ) -> Result<(), Ics02Error> { todo!() } @@ -78,7 +219,7 @@ impl ClientDef for TendermintClient { _port_id: &PortId, _channel_id: &ChannelId, _expected_channel_end: &ChannelEnd, - ) -> Result<(), Error> { + ) -> Result<(), Ics02Error> { todo!() } @@ -91,7 +232,7 @@ impl ClientDef for TendermintClient { _client_id: &ClientId, _proof: &CommitmentProofBytes, _expected_client_state: &AnyClientState, - ) -> Result<(), Error> { + ) -> Result<(), Ics02Error> { unimplemented!() } @@ -104,7 +245,7 @@ impl ClientDef for TendermintClient { _channel_id: &ChannelId, _seq: &Sequence, _data: String, - ) -> Result<(), Error> { + ) -> Result<(), Ics02Error> { todo!() } @@ -117,7 +258,7 @@ impl ClientDef for TendermintClient { _channel_id: &ChannelId, _seq: &Sequence, _data: Vec, - ) -> Result<(), Error> { + ) -> Result<(), Ics02Error> { todo!() } @@ -129,7 +270,7 @@ impl ClientDef for TendermintClient { _port_id: &PortId, _channel_id: &ChannelId, _seq: &Sequence, - ) -> Result<(), Error> { + ) -> Result<(), Ics02Error> { todo!() } @@ -141,7 +282,7 @@ impl ClientDef for TendermintClient { _port_id: &PortId, _channel_id: &ChannelId, _seq: &Sequence, - ) -> Result<(), Error> { + ) -> Result<(), Ics02Error> { todo!() } @@ -151,7 +292,14 @@ impl ClientDef for TendermintClient { _consensus_state: &Self::ConsensusState, _proof_upgrade_client: MerkleProof, _proof_upgrade_consensus_state: MerkleProof, - ) -> Result<(Self::ClientState, Self::ConsensusState), Error> { + ) -> Result<(Self::ClientState, Self::ConsensusState), Ics02Error> { todo!() } } + +fn downcast_consensus_state(cs: AnyConsensusState) -> Result { + downcast!( + cs => AnyConsensusState::Tendermint + ) + .ok_or_else(|| Ics02Error::client_args_type_mismatch(ClientType::Tendermint)) +} diff --git a/modules/src/ics07_tendermint/client_state.rs b/modules/src/ics07_tendermint/client_state.rs index 7fe85a0d73..ea2bcbadd4 100644 --- a/modules/src/ics07_tendermint/client_state.rs +++ b/modules/src/ics07_tendermint/client_state.rs @@ -4,12 +4,14 @@ use core::str::FromStr; use core::time::Duration; use serde::{Deserialize, Serialize}; +use tendermint_light_client::light_client::Options; use tendermint_proto::Protobuf; use ibc_proto::ibc::lightclients::tendermint::v1::ClientState as RawClientState; use crate::ics02_client::client_state::AnyClientState; use crate::ics02_client::client_type::ClientType; +use crate::ics02_client::error::Error as Ics02Error; use crate::ics02_client::trust_threshold::TrustThreshold; use crate::ics07_tendermint::error::Error; use crate::ics07_tendermint::header::Header; @@ -116,7 +118,14 @@ impl ClientState { } } - /// Helper function for the upgrade chain & client procedures. + pub fn with_set_frozen(self, h: Height) -> Self { + Self { + frozen_height: h, + ..self + } + } + + /// Helper function to verify the upgrade client procedure. /// Resets all fields except the blockchain-specific ones. pub fn zero_custom_fields(mut client_state: Self) -> Self { client_state.trusting_period = ZERO_DURATION; @@ -138,6 +147,20 @@ impl ClientState { pub fn expired(&self, elapsed: Duration) -> bool { elapsed > self.trusting_period } + + /// Helper method to produce a + /// [`tendermint_light_client::light_client::Options`] struct for use in + /// Tendermint-specific light client verification. + pub fn as_light_client_options(&self) -> Result { + Ok(Options { + trust_threshold: self + .trust_level + .try_into() + .map_err(|e: Ics02Error| Error::invalid_trust_threshold(e.to_string()))?, + trusting_period: self.trusting_period, + clock_drift: self.max_clock_drift, + }) + } } impl crate::ics02_client::client_state::ClientState for ClientState { diff --git a/modules/src/ics07_tendermint/error.rs b/modules/src/ics07_tendermint/error.rs index a67b69e742..83029f6177 100644 --- a/modules/src/ics07_tendermint/error.rs +++ b/modules/src/ics07_tendermint/error.rs @@ -3,6 +3,10 @@ use crate::prelude::*; use flex_error::{define_error, TraceError}; use crate::ics24_host::error::ValidationError; +use crate::Height; +use tendermint::account::Id; +use tendermint::hash::Hash; +use tendermint::Error as TendermintError; define_error! { #[derive(Debug, PartialEq, Eq)] @@ -16,11 +20,11 @@ define_error! { |e| { format_args!("invalid unbonding period: {}", e.reason) }, InvalidAddress - | _ | { "invalid address" }, + |_| { "invalid address" }, InvalidHeader { reason: String } - [ tendermint::Error ] + [ TendermintError ] |e| { format_args!("invalid header, failed basic validation: {}", e.reason) }, InvalidTrustThreshold @@ -86,18 +90,134 @@ define_error! { InvalidRawConsensusState { reason: String } - |e| { format_args!("invalid raw client consensus state: {}", e.reason) }, + | e | { format_args!("invalid raw client consensus state: {}", e.reason) }, InvalidRawHeader - [ tendermint::Error ] - |_| { "invalid raw header" }, + [ TendermintError ] + | _ | { "invalid raw header" }, InvalidRawMisbehaviour { reason: String } - |e| { format_args!("invalid raw misbehaviour: {}", e.reason) }, + | e | { format_args!("invalid raw misbehaviour: {}", e.reason) }, Decode [ TraceError ] - |_| { "decode error" }, + | _ | { "decode error" }, + + InsufficientVotingPower + { reason: String } + | e | { + format_args!("insufficient overlap: {}", e.reason) + }, + + LowUpdateTimestamp + { + low: String, + high: String + } + | e | { + format_args!("header timestamp {0} must be greater than current client consensus state timestamp {1}", e.low, e.high) + }, + + HeaderTimestampOutsideTrustingTime + { + low: String, + high: String + } + | e | { + format_args!("header timestamp {0} is outside the trusting period w.r.t. consensus state timestamp {1}", e.low, e.high) + }, + + HeaderTimestampTooHigh + { + actual: String, + max: String, + } + | e | { + format_args!("given other previous updates, header timestamp should be at most {0}, but was {1}", e.max, e.actual) + }, + + HeaderTimestampTooLow + { + actual: String, + min: String, + } + | e | { + format_args!("given other previous updates, header timestamp should be at least {0}, but was {1}", e.min, e.actual) + }, + + InvalidHeaderHeight + { height: Height } + | e | { + format_args!("header height = {0} is invalid", e.height) + }, + + InvalidTrustedHeaderHeight + { + trusted_header_height: Height, + height_header: Height + } + | e | { + format_args!("header height is {0} and is lower than the trusted header height, which is {1} ", e.height_header, e.trusted_header_height) + }, + + LowUpdateHeight + { + low: Height, + high: Height + } + | e | { + format_args!("header height is {0} but it must be greater than the current client height which is {1}", e.low, e.high) + }, + + MismatchedRevisions + { + current_revision: u64, + update_revision: u64, + } + | e | { + format_args!("the header's current/trusted revision number ({0}) and the update's revision number ({1}) should be the same", e.current_revision, e.update_revision) + }, + + InvalidValidatorSet + { + hash1: Hash, + hash2: Hash, + } + | e | { + format_args!("invalid validator set: header_validators_hash={} and validators_hash={}", e.hash1, e.hash2) + }, + + NotEnoughTrustedValsSigned + { reason: String } + | e | { + format_args!("not enough trust because insufficient validators overlap: {}", e.reason) + }, + + VerificationError + { detail: tendermint_light_client::predicates::errors::VerificationErrorDetail } + | e | { + format_args!("verification failed: {}", e.detail) + } + } +} + +define_error! { + #[derive(Debug, PartialEq, Eq)] + VerificationError { + InvalidSignature + | _ | { "couldn't verify validator signature" }, + + DuplicateValidator + { id: Id } + | e | { + format_args!("duplicate validator in commit signatures with address {}", e.id) + }, + + InsufficientOverlap + { q1: u64, q2: u64 } + | e | { + format_args!("insufficient signers overlap between {0} and {1}", e.q1, e.q2) + }, } } diff --git a/modules/src/ics07_tendermint/header.rs b/modules/src/ics07_tendermint/header.rs index abe8654058..bd16911ce5 100644 --- a/modules/src/ics07_tendermint/header.rs +++ b/modules/src/ics07_tendermint/header.rs @@ -9,6 +9,8 @@ use tendermint::validator::Set as ValidatorSet; use tendermint::Time; use tendermint_proto::Protobuf; +use crate::timestamp::Timestamp; + use ibc_proto::ibc::lightclients::tendermint::v1::Header as RawHeader; use crate::ics02_client::client_type::ClientType; @@ -24,6 +26,7 @@ pub struct Header { pub signed_header: SignedHeader, // contains the commitment root pub validator_set: ValidatorSet, // the validator set that signed Header pub trusted_height: Height, // the height of a trusted header seen by client less than or equal to Header + // TODO(thane): Rename this to trusted_next_validator_set? pub trusted_validator_set: ValidatorSet, // the last trusted validator set at trusted height } @@ -79,6 +82,10 @@ impl crate::ics02_client::header::Header for Header { self.height() } + fn timestamp(&self) -> Timestamp { + self.time().into() + } + fn wrap_any(self) -> AnyHeader { AnyHeader::Tendermint(self) } @@ -90,7 +97,7 @@ impl TryFrom for Header { type Error = Error; fn try_from(raw: RawHeader) -> Result { - Ok(Self { + let header = Self { signed_header: raw .signed_header .ok_or_else(Error::missing_signed_header)? @@ -110,7 +117,16 @@ impl TryFrom for Header { .ok_or_else(Error::missing_trusted_validator_set)? .try_into() .map_err(Error::invalid_raw_header)?, - }) + }; + + if header.height().revision_number != header.trusted_height.revision_number { + return Err(Error::mismatched_revisions( + header.trusted_height.revision_number, + header.height().revision_number, + )); + } + + Ok(header) } } diff --git a/modules/src/ics18_relayer/utils.rs b/modules/src/ics18_relayer/utils.rs index 4bf7671edb..05166e0fc0 100644 --- a/modules/src/ics18_relayer/utils.rs +++ b/modules/src/ics18_relayer/utils.rs @@ -50,7 +50,7 @@ where #[cfg(test)] mod tests { use crate::ics02_client::client_type::ClientType; - use crate::ics02_client::header::Header; + use crate::ics02_client::header::{AnyHeader, Header}; use crate::ics18_relayer::context::Ics18Context; use crate::ics18_relayer::utils::build_client_update_datagram; use crate::ics24_host::identifier::{ChainId, ClientId}; @@ -60,6 +60,7 @@ mod tests { use crate::prelude::*; use crate::Height; use test_env_log::test; + use tracing::debug; #[test] /// Serves to test both ICS 26 `dispatch` & `build_client_update_datagram` functions. @@ -150,7 +151,17 @@ mod tests { // Update client on chain B to latest height of B. // - create the client update message with the latest header from B - let b_latest_header = ctx_b.query_latest_header().unwrap(); + // The test uses LightClientBlock that does not store the trusted height + let b_latest_header = match ctx_b.query_latest_header().unwrap() { + AnyHeader::Tendermint(header) => { + let th = header.height(); + let mut hheader = header.clone(); + hheader.trusted_height = th.decrement().unwrap(); + hheader.wrap_any() + } + AnyHeader::Mock(header) => header.wrap_any(), + }; + assert_eq!( b_latest_header.client_type(), ClientType::Tendermint, @@ -171,6 +182,8 @@ mod tests { let client_msg_a = client_msg_a_res.unwrap(); + debug!("client_msg_a = {:?}", client_msg_a); + // - send the message to A let dispatch_res_a = ctx_a.deliver(Ics26Envelope::Ics2Msg(client_msg_a)); let validation_res = ctx_a.validate(); diff --git a/modules/src/ics26_routing/handler.rs b/modules/src/ics26_routing/handler.rs index d83b86c6f5..f6d35e788a 100644 --- a/modules/src/ics26_routing/handler.rs +++ b/modules/src/ics26_routing/handler.rs @@ -169,6 +169,7 @@ mod tests { use crate::mock::context::MockContext; use crate::mock::header::MockHeader; use crate::test_utils::get_dummy_account_id; + use crate::timestamp::Timestamp; use crate::Height; #[test] @@ -255,7 +256,6 @@ mod tests { .unwrap(); let msg_transfer = get_dummy_msg_transfer(35); - let msg_transfer_two = get_dummy_msg_transfer(36); let mut msg_to_on_close = @@ -299,7 +299,9 @@ mod tests { name: "Client update successful".to_string(), msg: Ics26Envelope::Ics2Msg(ClientMsg::UpdateClient(MsgUpdateAnyClient { client_id: client_id.clone(), - header: MockHeader::new(update_client_height).into(), + header: MockHeader::new(update_client_height) + .with_timestamp(Timestamp::now()) + .into(), signer: default_signer.clone(), })), want_pass: true, @@ -374,10 +376,12 @@ mod tests { // The client update is required in this test, because the proof associated with // msg_recv_packet has the same height as the packet TO height (see get_dummy_raw_msg_recv_packet) Test { - name: "Client update successful".to_string(), + name: "Client update successful #2".to_string(), msg: Ics26Envelope::Ics2Msg(ClientMsg::UpdateClient(MsgUpdateAnyClient { client_id: client_id.clone(), - header: MockHeader::new(update_client_height_after_send).into(), + header: MockHeader::new(update_client_height_after_send) + .with_timestamp(Timestamp::now()) + .into(), signer: default_signer.clone(), })), want_pass: true, diff --git a/modules/src/mock/client_def.rs b/modules/src/mock/client_def.rs index 4b520f1da6..de4e019489 100644 --- a/modules/src/mock/client_def.rs +++ b/modules/src/mock/client_def.rs @@ -3,6 +3,7 @@ use ibc_proto::ibc::core::commitment::v1::MerkleProof; use crate::ics02_client::client_consensus::AnyConsensusState; use crate::ics02_client::client_def::ClientDef; use crate::ics02_client::client_state::AnyClientState; +use crate::ics02_client::context::ClientReader; use crate::ics02_client::error::Error; use crate::ics03_connection::connection::ConnectionEnd; use crate::ics04_channel::channel::ChannelEnd; @@ -26,6 +27,8 @@ impl ClientDef for MockClient { fn check_header_and_update_state( &self, + _ctx: &dyn ClientReader, + _client_id: ClientId, client_state: Self::ClientState, header: Self::Header, ) -> Result<(Self::ClientState, Self::ConsensusState), Error> { diff --git a/modules/src/mock/context.rs b/modules/src/mock/context.rs index 76828496b1..c6631b6f32 100644 --- a/modules/src/mock/context.rs +++ b/modules/src/mock/context.rs @@ -1,9 +1,12 @@ //! Implementation of a global context mock. Used in testing handlers of all IBC modules. use crate::prelude::*; -use alloc::collections::btree_map::BTreeMap as HashMap; + +use alloc::collections::btree_map::BTreeMap; use core::cmp::min; +use tracing::debug; + use prost_types::Any; use sha2::Digest; @@ -63,49 +66,49 @@ pub struct MockContext { history: Vec, /// The set of all clients, indexed by their id. - clients: HashMap, + clients: BTreeMap, /// Counter for the client identifiers, necessary for `increase_client_counter` and the /// `client_counter` methods. client_ids_counter: u64, /// Association between client ids and connection ids. - client_connections: HashMap, + client_connections: BTreeMap, /// All the connections in the store. - connections: HashMap, + connections: BTreeMap, /// Counter for connection identifiers (see `increase_connection_counter`). connection_ids_counter: u64, /// Association between connection ids and channel ids. - connection_channels: HashMap>, + connection_channels: BTreeMap>, /// Counter for channel identifiers (see `increase_channel_counter`). channel_ids_counter: u64, /// All the channels in the store. TODO Make new key PortId X ChanneId - channels: HashMap<(PortId, ChannelId), ChannelEnd>, + channels: BTreeMap<(PortId, ChannelId), ChannelEnd>, /// Tracks the sequence number for the next packet to be sent. - next_sequence_send: HashMap<(PortId, ChannelId), Sequence>, + next_sequence_send: BTreeMap<(PortId, ChannelId), Sequence>, /// Tracks the sequence number for the next packet to be received. - next_sequence_recv: HashMap<(PortId, ChannelId), Sequence>, + next_sequence_recv: BTreeMap<(PortId, ChannelId), Sequence>, /// Tracks the sequence number for the next packet to be acknowledged. - next_sequence_ack: HashMap<(PortId, ChannelId), Sequence>, + next_sequence_ack: BTreeMap<(PortId, ChannelId), Sequence>, - packet_acknowledgement: HashMap<(PortId, ChannelId, Sequence), String>, + packet_acknowledgement: BTreeMap<(PortId, ChannelId, Sequence), String>, /// Maps ports to their capabilities - port_capabilities: HashMap, + port_capabilities: BTreeMap, /// Constant-size commitments to packets data fields - packet_commitment: HashMap<(PortId, ChannelId, Sequence), String>, + packet_commitment: BTreeMap<(PortId, ChannelId, Sequence), String>, // Used by unordered channel - packet_receipt: HashMap<(PortId, ChannelId, Sequence), Receipt>, + packet_receipt: BTreeMap<(PortId, ChannelId, Sequence), Receipt>, } /// Returns a MockContext with bare minimum initialization: no clients, no connections and no channels are @@ -218,6 +221,7 @@ impl MockContext { self.host_chain_id.clone(), cs_height.revision_height, ); + let consensus_state = AnyConsensusState::from(light_block.clone()); let client_state = get_dummy_tendermint_client_state(light_block.signed_header.header); @@ -228,6 +232,8 @@ impl MockContext { }; let consensus_states = vec![(cs_height, consensus_state)].into_iter().collect(); + debug!("consensus states: {:?}", consensus_states); + let client_record = MockClientRecord { client_type, client_state, @@ -237,6 +243,72 @@ impl MockContext { self } + pub fn with_client_parametrized_history( + mut self, + client_id: &ClientId, + client_state_height: Height, + client_type: Option, + consensus_state_height: Option, + ) -> Self { + let cs_height = consensus_state_height.unwrap_or(client_state_height); + let prev_cs_height = cs_height.clone().sub(1).unwrap_or(client_state_height); + + let client_type = client_type.unwrap_or(ClientType::Mock); + + let (client_state, consensus_state) = match client_type { + // If it's a mock client, create the corresponding mock states. + ClientType::Mock => ( + Some(MockClientState(MockHeader::new(client_state_height)).into()), + MockConsensusState::new(MockHeader::new(cs_height)).into(), + ), + // If it's a Tendermint client, we need TM states. + ClientType::Tendermint => { + let light_block = HostBlock::generate_tm_block( + self.host_chain_id.clone(), + cs_height.revision_height, + ); + + let consensus_state = AnyConsensusState::from(light_block.clone()); + let client_state = + get_dummy_tendermint_client_state(light_block.signed_header.header); + + // Return the tuple. + (Some(client_state), consensus_state) + } + }; + + let prev_consensus_state = match client_type { + // If it's a mock client, create the corresponding mock states. + ClientType::Mock => MockConsensusState::new(MockHeader::new(prev_cs_height)).into(), + // If it's a Tendermint client, we need TM states. + ClientType::Tendermint => { + let light_block = HostBlock::generate_tm_block( + self.host_chain_id.clone(), + prev_cs_height.revision_height, + ); + AnyConsensusState::from(light_block) + } + }; + + let consensus_states = vec![ + (prev_cs_height, prev_consensus_state), + (cs_height, consensus_state), + ] + .into_iter() + .collect(); + + debug!("consensus states: {:?}", consensus_states); + + let client_record = MockClientRecord { + client_type, + client_state, + consensus_states, + }; + + self.clients.insert(client_id.clone(), client_record); + self + } + /// Associates a connection to this context. pub fn with_connection( mut self, @@ -347,7 +419,7 @@ impl MockContext { /// Accessor for a block of the local (host) chain from this context. /// Returns `None` if the block at the requested height does not exist. - fn host_block(&self, target_height: Height) -> Option<&HostBlock> { + pub fn host_block(&self, target_height: Height) -> Option<&HostBlock> { let target = target_height.revision_height as usize; let latest = self.latest_height.revision_height as usize; @@ -431,6 +503,21 @@ impl MockContext { }) .collect() } + + pub fn latest_client_states(&self, client_id: &ClientId) -> &AnyClientState { + self.clients[client_id].client_state.as_ref().unwrap() + } + + pub fn latest_consensus_states( + &self, + client_id: &ClientId, + height: &Height, + ) -> &AnyConsensusState { + self.clients[client_id] + .consensus_states + .get(height) + .unwrap() + } } impl Ics26Context for MockContext {} @@ -798,6 +885,60 @@ impl ClientReader for MockContext { } } + /// Search for the lowest consensus state higher than `height`. + fn next_consensus_state( + &self, + client_id: &ClientId, + height: Height, + ) -> Result, Ics02Error> { + let client_record = self + .clients + .get(client_id) + .ok_or_else(|| Ics02Error::client_not_found(client_id.clone()))?; + + // Get the consensus state heights and sort them in ascending order. + let mut heights: Vec = client_record.consensus_states.keys().cloned().collect(); + heights.sort(); + + // Search for next state. + for h in heights { + if h > height { + // unwrap should never happen, as the consensus state for h must exist + return Ok(Some( + client_record.consensus_states.get(&h).unwrap().clone(), + )); + } + } + Ok(None) + } + + /// Search for the highest consensus state lower than `height`. + fn prev_consensus_state( + &self, + client_id: &ClientId, + height: Height, + ) -> Result, Ics02Error> { + let client_record = self + .clients + .get(client_id) + .ok_or_else(|| Ics02Error::client_not_found(client_id.clone()))?; + + // Get the consensus state heights and sort them in descending order. + let mut heights: Vec = client_record.consensus_states.keys().cloned().collect(); + heights.sort_by(|a, b| b.cmp(a)); + + // Search for previous state. + for h in heights { + if h < height { + // unwrap should never happen, as the consensus state for h must exist + return Ok(Some( + client_record.consensus_states.get(&h).unwrap().clone(), + )); + } + } + Ok(None) + } + fn client_counter(&self) -> Result { Ok(self.client_ids_counter) } diff --git a/modules/src/mock/header.rs b/modules/src/mock/header.rs index 13af52f11b..3031fc4866 100644 --- a/modules/src/mock/header.rs +++ b/modules/src/mock/header.rs @@ -52,9 +52,13 @@ impl MockHeader { pub fn new(height: Height) -> Self { Self { height, - timestamp: Default::default(), + timestamp: Timestamp::now(), } } + + pub fn with_timestamp(self, timestamp: Timestamp) -> Self { + Self { timestamp, ..self } + } } impl From for AnyHeader { @@ -72,6 +76,10 @@ impl Header for MockHeader { self.height } + fn timestamp(&self) -> Timestamp { + self.timestamp + } + fn wrap_any(self) -> AnyHeader { AnyHeader::Mock(self) } @@ -89,14 +97,14 @@ mod tests { #[test] fn encode_any() { - let header = MockHeader::new(Height::new(1, 10)); + let header = MockHeader::new(Height::new(1, 10)).with_timestamp(Timestamp::none()); let bytes = header.wrap_any().encode_vec().unwrap(); assert_eq!( &bytes, &[ 10, 16, 47, 105, 98, 99, 46, 109, 111, 99, 107, 46, 72, 101, 97, 100, 101, 114, 18, - 6, 10, 4, 8, 1, 16, 10, + 6, 10, 4, 8, 1, 16, 10 ] ); } diff --git a/modules/src/mock/host.rs b/modules/src/mock/host.rs index 5409c1c569..6bcba721e5 100644 --- a/modules/src/mock/host.rs +++ b/modules/src/mock/host.rs @@ -1,9 +1,6 @@ //! Host chain types and methods, used by context mock. -use crate::prelude::*; -use core::convert::TryFrom; - -use tendermint::chain::Id as TMChainId; +use tendermint::time::Time; use tendermint_testgen::light_block::TmLightBlock; use tendermint_testgen::{Generator, LightBlock as TestgenLightBlock}; @@ -13,6 +10,7 @@ use crate::ics07_tendermint::consensus_state::ConsensusState as TMConsensusState use crate::ics07_tendermint::header::Header as TMHeader; use crate::ics24_host::identifier::ChainId; use crate::mock::header::MockHeader; +use crate::prelude::*; use crate::timestamp::Timestamp; use crate::Height; @@ -52,7 +50,7 @@ impl HostBlock { match chain_type { HostType::Mock => HostBlock::Mock(MockHeader { height: Height::new(chain_id.version(), height), - timestamp: Timestamp::from_nanoseconds(1).unwrap(), + timestamp: Timestamp::now(), }), HostType::SyntheticTendermint => { HostBlock::SyntheticTendermint(Box::new(Self::generate_tm_block(chain_id, height))) @@ -61,10 +59,18 @@ impl HostBlock { } pub fn generate_tm_block(chain_id: ChainId, height: u64) -> TmLightBlock { - let mut block = TestgenLightBlock::new_default(height).generate().unwrap(); - block.signed_header.header.chain_id = TMChainId::try_from(chain_id.to_string()).unwrap(); + // Sleep is required otherwise the generator produces blocks with the + // same timestamp as two block can be generated per second. + let ten_millis = core::time::Duration::from_millis(1000); + std::thread::sleep(ten_millis); + let time = Time::now() + .duration_since(Time::unix_epoch()) + .unwrap() + .as_secs(); - block + TestgenLightBlock::new_default_with_time_and_chain_id(chain_id.to_string(), time, height) + .generate() + .unwrap() } } diff --git a/modules/src/timestamp.rs b/modules/src/timestamp.rs index fe08476b08..a527200bf2 100644 --- a/modules/src/timestamp.rs +++ b/modules/src/timestamp.rs @@ -9,6 +9,7 @@ use core::time::Duration; use chrono::{offset::Utc, DateTime, TimeZone}; use flex_error::{define_error, TraceError}; use serde_derive::{Deserialize, Serialize}; +use tendermint::Time; pub const ZERO_DURATION: Duration = Duration::from_secs(0); @@ -59,6 +60,13 @@ impl Timestamp { } } + /// Returns a `Timestamp` representation of the current time. + pub fn now() -> Timestamp { + Timestamp { + time: Some(Utc::now()), + } + } + /// Returns a `Timestamp` representation of a timestamp not being set. pub fn none() -> Self { Timestamp { time: None } @@ -178,6 +186,14 @@ impl FromStr for Timestamp { } } +impl From