diff --git a/ibc-clients/ics07-tendermint/cw-contract/src/tests/fixture.rs b/ibc-clients/ics07-tendermint/cw-contract/src/tests/fixture.rs index 96ec1434a..8961f10f1 100644 --- a/ibc-clients/ics07-tendermint/cw-contract/src/tests/fixture.rs +++ b/ibc-clients/ics07-tendermint/cw-contract/src/tests/fixture.rs @@ -1,11 +1,10 @@ -use std::ops::Add; use std::time::Duration; -use cosmwasm_std::testing::mock_env; -use cosmwasm_std::{from_json, Deps, DepsMut, Empty, StdError}; +use cosmwasm_std::{from_json, Deps, DepsMut, Empty, Response, StdError, StdResult}; use ibc_client_cw::types::{ - CheckForMisbehaviourMsgRaw, ExportMetadataMsg, GenesisMetadata, InstantiateMsg, QueryMsg, - QueryResponse, StatusMsg, VerifyClientMessageRaw, + CheckForMisbehaviourMsgRaw, ContractError, ExportMetadataMsg, GenesisMetadata, InstantiateMsg, + MigrationPrefix, QueryMsg, QueryResponse, StatusMsg, UpdateStateMsgRaw, + UpdateStateOnMisbehaviourMsgRaw, VerifyClientMessageRaw, }; use ibc_client_cw::utils::AnyCodec; use ibc_client_tendermint::client_state::ClientState as TmClientState; @@ -15,9 +14,10 @@ use ibc_core::client::types::{Height, Status}; use ibc_core::host::types::identifiers::ChainId; use ibc_core::primitives::Timestamp; use ibc_testkit::fixtures::clients::tendermint::ClientStateConfig; +use tendermint::Time; use tendermint_testgen::{Generator, Validator}; -use super::helper::{dummy_checksum, dummy_sov_consensus_state}; +use super::helper::{dummy_checksum, dummy_sov_consensus_state, mock_env_with_timestamp_now}; use crate::entrypoint::TendermintContext; /// Test fixture @@ -26,63 +26,70 @@ pub struct Fixture { pub chain_id: ChainId, pub trusted_timestamp: Timestamp, pub trusted_height: Height, - pub target_height: Height, pub validators: Vec, - pub migration_mode: bool, + pub migration_prefix: MigrationPrefix, } impl Default for Fixture { fn default() -> Self { Fixture { chain_id: ChainId::new("test-chain").unwrap(), - // Returns a dummy timestamp for testing purposes. The value corresponds to the - // timestamp of the `mock_env()`. - trusted_timestamp: Timestamp::from_nanoseconds(1_571_797_419_879_305_533) - .expect("never fails"), + trusted_timestamp: Timestamp::now(), trusted_height: Height::new(0, 5).unwrap(), - target_height: Height::new(0, 10).unwrap(), validators: vec![ Validator::new("1").voting_power(40), Validator::new("2").voting_power(30), Validator::new("3").voting_power(30), ], - migration_mode: false, + migration_prefix: MigrationPrefix::None, } } } impl Fixture { - pub fn migration_mode(mut self) -> Self { - self.migration_mode = true; - self + pub fn set_migration_prefix(&mut self, migration_mode: MigrationPrefix) { + self.migration_prefix = migration_mode; } pub fn ctx_ref<'a>(&self, deps: Deps<'a, Empty>) -> TendermintContext<'a> { - let mut ctx = TendermintContext::new_ref(deps, mock_env()).expect("never fails"); - - if self.migration_mode { - ctx.set_subject_prefix(); + let mut ctx = + TendermintContext::new_ref(deps, mock_env_with_timestamp_now()).expect("never fails"); + + match self.migration_prefix { + MigrationPrefix::None => {} + MigrationPrefix::Subject => { + ctx.set_subject_prefix(); + } + MigrationPrefix::Substitute => { + ctx.set_substitute_prefix(); + } }; ctx } pub fn ctx_mut<'a>(&self, deps: DepsMut<'a, Empty>) -> TendermintContext<'a> { - let mut ctx = TendermintContext::new_mut(deps, mock_env()).expect("never fails"); - - if self.migration_mode { - ctx.set_subject_prefix(); + let mut ctx = + TendermintContext::new_mut(deps, mock_env_with_timestamp_now()).expect("never fails"); + + match self.migration_prefix { + MigrationPrefix::None => {} + MigrationPrefix::Subject => { + ctx.set_subject_prefix(); + } + MigrationPrefix::Substitute => { + ctx.set_substitute_prefix(); + } }; ctx } pub fn dummy_instantiate_msg(&self) -> InstantiateMsg { - // Setting the `trusting_period` to 1 second allows the quick client - // freeze for the `happy_cw_client_recovery` test. - + // Setting the `trusting_period` to 1 second allows the quick + // client expiry for the tests. let tm_client_state: TmClientState = ClientStateConfig::builder() - .chain_id("test-chain".parse().unwrap()) + .chain_id(self.chain_id.clone()) .trusting_period(Duration::from_secs(1)) .latest_height(self.trusted_height) .build() @@ -99,19 +106,10 @@ impl Fixture { } fn dummy_header(&self, header_height: Height) -> Vec { - // NOTE: since mock context has a fixed timestamp, we only can add up - // to allowed clock drift (3s) - let future_time = self - .trusted_timestamp - .add(Duration::from_secs(2)) - .expect("never fails") - .into_tm_time() - .expect("Time exists"); - let header = tendermint_testgen::Header::new(&self.validators) .chain_id(self.chain_id.as_str()) .height(header_height.revision_height()) - .time(future_time) + .time(Time::now()) .next_validators(&self.validators) .app_hash(vec![0; 32].try_into().expect("never fails")); @@ -129,8 +127,8 @@ impl Fixture { Header::encode_to_any_vec(tm_header) } - pub fn dummy_client_message(&self) -> Vec { - self.dummy_header(self.target_height) + pub fn dummy_client_message(&self, target_height: Height) -> Vec { + self.dummy_header(target_height) } /// Constructs a dummy misbehaviour message that is one block behind the @@ -142,7 +140,9 @@ impl Fixture { } pub fn verify_client_message(&self, deps: Deps<'_>, client_message: Vec) { - let resp = self.query(deps, VerifyClientMessageRaw { client_message }.into()); + let resp = self + .query(deps, VerifyClientMessageRaw { client_message }.into()) + .unwrap(); assert!(resp.is_valid); assert!(resp.status.is_none()); @@ -150,31 +150,73 @@ impl Fixture { } pub fn check_for_misbehaviour(&self, deps: Deps<'_>, client_message: Vec) { - let resp = self.query(deps, CheckForMisbehaviourMsgRaw { client_message }.into()); + let resp = self + .query(deps, CheckForMisbehaviourMsgRaw { client_message }.into()) + .unwrap(); assert!(resp.is_valid); assert_eq!(resp.found_misbehaviour, Some(true)); } pub fn check_client_status(&self, deps: Deps<'_>, expected: Status) { - let resp = self.query(deps, StatusMsg {}.into()); + let resp = self.query(deps, StatusMsg {}.into()).unwrap(); assert_eq!(resp.status, Some(expected.to_string())); } pub fn get_metadata(&self, deps: Deps<'_>) -> Option> { self.query(deps, ExportMetadataMsg {}.into()) - .genesis_metadata + .map(|resp| resp.genesis_metadata) + .unwrap() } - pub fn query(&self, deps: Deps<'_>, msg: QueryMsg) -> QueryResponse { + pub fn query(&self, deps: Deps<'_>, msg: QueryMsg) -> StdResult { let ctx = self.ctx_ref(deps); let resp_bytes = ctx .query(msg) - .map_err(|e| StdError::generic_err(e.to_string())) + .map_err(|e| StdError::generic_err(e.to_string()))?; + + from_json(resp_bytes) + } + + pub fn create_client(&self, deps_mut: DepsMut<'_>) -> Result { + let mut ctx = self.ctx_mut(deps_mut); + + let instantiate_msg = self.dummy_instantiate_msg(); + + let data = ctx.instantiate(instantiate_msg)?; + + Ok(Response::default().set_data(data)) + } + + pub fn update_client( + &self, + deps_mut: DepsMut<'_>, + target_height: Height, + ) -> Result { + let client_message = self.dummy_client_message(target_height); + + self.verify_client_message(deps_mut.as_ref(), client_message.clone()); + + let mut ctx = self.ctx_mut(deps_mut); + + let data = ctx.sudo(UpdateStateMsgRaw { client_message }.into())?; + + Ok(Response::default().set_data(data)) + } + + pub fn update_client_on_misbehaviour(&self, deps_mut: DepsMut<'_>) -> Response { + let client_message = self.dummy_misbehaviour_message(); + + self.check_for_misbehaviour(deps_mut.as_ref(), client_message.clone()); + + let mut ctx = self.ctx_mut(deps_mut); + + let data = ctx + .sudo(UpdateStateOnMisbehaviourMsgRaw { client_message }.into()) .unwrap(); - from_json(resp_bytes).unwrap() + Response::default().set_data(data) } } diff --git a/ibc-clients/ics07-tendermint/cw-contract/src/tests/helper.rs b/ibc-clients/ics07-tendermint/cw-contract/src/tests/helper.rs index 57bf8c9e3..45b5137b0 100644 --- a/ibc-clients/ics07-tendermint/cw-contract/src/tests/helper.rs +++ b/ibc-clients/ics07-tendermint/cw-contract/src/tests/helper.rs @@ -1,9 +1,9 @@ use std::str::FromStr; -use cosmwasm_std::testing::mock_info; -use cosmwasm_std::{coins, MessageInfo}; +use cosmwasm_std::testing::{mock_env, mock_info}; +use cosmwasm_std::{coins, Env, MessageInfo, Timestamp as CwTimestamp}; use ibc_client_tendermint::types::ConsensusState; -use ibc_core::primitives::Timestamp; +use ibc_core::primitives::Timestamp as IbcTimestamp; use tendermint::Hash; pub fn dummy_msg_info() -> MessageInfo { @@ -15,7 +15,7 @@ pub fn dummy_checksum() -> Vec { .expect("Never fails") } -pub fn dummy_sov_consensus_state(timestamp: Timestamp) -> ConsensusState { +pub fn dummy_sov_consensus_state(timestamp: IbcTimestamp) -> ConsensusState { ConsensusState::new( vec![0].into(), timestamp.into_tm_time().expect("Time exists"), @@ -24,3 +24,12 @@ pub fn dummy_sov_consensus_state(timestamp: Timestamp) -> ConsensusState { .expect("Never fails"), ) } + +/// Returns a mock environment with the current timestamp. This is define to use +/// for testing client expiry and other time-sensitive operations. +pub fn mock_env_with_timestamp_now() -> Env { + let mut env = mock_env(); + let now_nanos = IbcTimestamp::now().nanoseconds(); + env.block.time = CwTimestamp::from_nanos(now_nanos); + env +} diff --git a/ibc-clients/ics07-tendermint/cw-contract/src/tests/mod.rs b/ibc-clients/ics07-tendermint/cw-contract/src/tests/mod.rs index 3918b52d6..9c302f1f2 100644 --- a/ibc-clients/ics07-tendermint/cw-contract/src/tests/mod.rs +++ b/ibc-clients/ics07-tendermint/cw-contract/src/tests/mod.rs @@ -1,26 +1,25 @@ pub mod fixture; pub mod helper; +use std::time::Duration; + use cosmwasm_std::from_json; use cosmwasm_std::testing::{mock_dependencies, mock_env}; use ibc_client_cw::types::{ - ContractResult, MigrateClientStoreMsg, UpdateStateMsgRaw, UpdateStateOnMisbehaviourMsgRaw, + ContractResult, MigrateClientStoreMsg, MigrationPrefix, VerifyClientMessageRaw, }; -use ibc_core::client::types::Status; +use ibc_core::client::types::{Height, Status}; -use crate::entrypoint::{instantiate, sudo}; +use crate::entrypoint::sudo; use crate::tests::fixture::Fixture; -use crate::tests::helper::dummy_msg_info; #[test] -fn happy_cw_create_client() { +fn test_cw_create_client_ok() { let fxt = Fixture::default(); let mut deps = mock_dependencies(); - let instantiate_msg = fxt.dummy_instantiate_msg(); - - let resp = instantiate(deps.as_mut(), mock_env(), dummy_msg_info(), instantiate_msg).unwrap(); + let resp = fxt.create_client(deps.as_mut()).unwrap(); assert_eq!(0, resp.messages.len()); @@ -32,29 +31,20 @@ fn happy_cw_create_client() { } #[test] -fn happy_cw_update_client() { +fn test_cw_update_client_ok() { let fxt = Fixture::default(); let mut deps = mock_dependencies(); // ------------------- Create client ------------------- - let instantiate_msg = fxt.dummy_instantiate_msg(); - - instantiate(deps.as_mut(), mock_env(), dummy_msg_info(), instantiate_msg).unwrap(); + fxt.create_client(deps.as_mut()).unwrap(); // ------------------- Verify and Update client ------------------- - let client_message = fxt.dummy_client_message(); - - fxt.verify_client_message(deps.as_ref(), client_message.clone()); + let target_height = Height::new(0, 10).unwrap(); - let resp = sudo( - deps.as_mut(), - mock_env(), - UpdateStateMsgRaw { client_message }.into(), - ) - .unwrap(); + let resp = fxt.update_client(deps.as_mut(), target_height).unwrap(); // ------------------- Check response ------------------- @@ -62,41 +52,32 @@ fn happy_cw_update_client() { let contract_result: ContractResult = from_json(resp.data.unwrap()).unwrap(); - assert_eq!(contract_result.heights, Some(vec![fxt.target_height])); + assert_eq!(contract_result.heights, Some(vec![target_height])); fxt.check_client_status(deps.as_ref(), Status::Active); } #[test] -fn happy_cw_recovery_client() { - let fxt = Fixture::default().migration_mode(); +fn test_cw_recovery_client_ok() { + let mut fxt = Fixture::default(); let mut deps = mock_dependencies(); - let mut ctx = fxt.ctx_mut(deps.as_mut()); - // ------------------- Create subject client ------------------- - let instantiate_msg = fxt.dummy_instantiate_msg(); + fxt.set_migration_prefix(MigrationPrefix::Subject); - ctx.instantiate(instantiate_msg.clone()).unwrap(); + fxt.create_client(deps.as_mut()).unwrap(); // ------------------- Freeze subject client ------------------- - let client_message = fxt.dummy_misbehaviour_message(); - - fxt.check_for_misbehaviour(deps.as_ref(), client_message.clone()); - - let mut ctx = fxt.ctx_mut(deps.as_mut()); - - ctx.sudo(UpdateStateOnMisbehaviourMsgRaw { client_message }.into()) - .unwrap(); + fxt.update_client_on_misbehaviour(deps.as_mut()); // ------------------- Create substitute client ------------------- - ctx.set_substitute_prefix(); + fxt.set_migration_prefix(MigrationPrefix::Substitute); - ctx.instantiate(instantiate_msg).unwrap(); + fxt.create_client(deps.as_mut()).unwrap(); // ------------------- Recover subject client ------------------- @@ -106,3 +87,38 @@ fn happy_cw_recovery_client() { fxt.check_client_status(deps.as_ref(), Status::Active); } + +#[test] +fn test_cw_client_expiry() { + let fxt = Fixture::default(); + + let mut deps = mock_dependencies(); + + // ------------------- Create client ------------------- + + fxt.create_client(deps.as_mut()).unwrap(); + + // ------------------- Expire client ------------------- + + std::thread::sleep(Duration::from_millis(1200)); + + // ------------------- Try update client ------------------- + + let target_height = Height::new(0, 10).unwrap(); + + let client_message = fxt.dummy_client_message(target_height); + + let resp = fxt.query( + deps.as_ref(), + VerifyClientMessageRaw { + client_message: client_message.clone(), + } + .into(), + ); + + assert!(resp.is_err()); + + // ------------------- Check client status ------------------- + + fxt.check_client_status(deps.as_ref(), Status::Expired); +}