diff --git a/.changelog/unreleased/bug-fixes/ibc/1770-deterministic-host-timestamp.md b/.changelog/unreleased/bug-fixes/ibc/1770-deterministic-host-timestamp.md new file mode 100644 index 0000000000..821801926b --- /dev/null +++ b/.changelog/unreleased/bug-fixes/ibc/1770-deterministic-host-timestamp.md @@ -0,0 +1,2 @@ +- IBC handlers now retrieve the host timestamp from the latest host consensus + state ([#1770](https://github.com/informalsystems/ibc-rs/issues/1770)) \ No newline at end of file diff --git a/modules/src/clients/ics07_tendermint/client_def.rs b/modules/src/clients/ics07_tendermint/client_def.rs index 6dc00cfc21..e9ed88deda 100644 --- a/modules/src/clients/ics07_tendermint/client_def.rs +++ b/modules/src/clients/ics07_tendermint/client_def.rs @@ -2,7 +2,6 @@ use core::convert::TryInto; use ibc_proto::ibc::core::commitment::v1::MerkleProof as RawMerkleProof; use prost::Message; -use tendermint::Time; use tendermint_light_client_verifier::types::{TrustedBlockState, UntrustedBlockState}; use tendermint_light_client_verifier::{ProdVerifier, Verdict, Verifier}; use tendermint_proto::Protobuf; @@ -50,7 +49,6 @@ impl ClientDef for TendermintClient { fn check_header_and_update_state( &self, - now: Time, ctx: &dyn ClientReader, client_id: ClientId, client_state: Self::ClientState, @@ -113,9 +111,12 @@ impl ClientDef for TendermintClient { let options = client_state.as_light_client_options()?; - let verdict = self - .verifier - .verify(untrusted_state, trusted_state, &options, now); + let verdict = self.verifier.verify( + untrusted_state, + trusted_state, + &options, + ctx.host_timestamp().into_tm_time().unwrap(), + ); match verdict { Verdict::Success => {} diff --git a/modules/src/core/ics02_client/client_def.rs b/modules/src/core/ics02_client/client_def.rs index fdfb01781a..d1a4311408 100644 --- a/modules/src/core/ics02_client/client_def.rs +++ b/modules/src/core/ics02_client/client_def.rs @@ -1,5 +1,4 @@ use ibc_proto::ibc::core::commitment::v1::MerkleProof; -use tendermint::Time; use crate::clients::ics07_tendermint::client_def::TendermintClient; use crate::core::ics02_client::client_consensus::{AnyConsensusState, ConsensusState}; @@ -30,7 +29,6 @@ pub trait ClientDef: Clone { fn check_header_and_update_state( &self, - now: Time, ctx: &dyn ClientReader, client_id: ClientId, client_state: Self::ClientState, @@ -197,7 +195,6 @@ impl ClientDef for AnyClient { /// Validates an incoming `header` against the latest consensus state of this client. fn check_header_and_update_state( &self, - now: Time, ctx: &dyn ClientReader, client_id: ClientId, client_state: AnyClientState, @@ -211,13 +208,8 @@ 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( - now, - ctx, - client_id, - client_state, - header, - )?; + let (new_state, new_consensus) = + client.check_header_and_update_state(ctx, client_id, client_state, header)?; Ok(( AnyClientState::Tendermint(new_state), @@ -233,13 +225,8 @@ 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( - now, - ctx, - client_id, - client_state, - header, - )?; + let (new_state, new_consensus) = + client.check_header_and_update_state(ctx, client_id, client_state, header)?; Ok(( AnyClientState::Mock(new_state), diff --git a/modules/src/core/ics02_client/context.rs b/modules/src/core/ics02_client/context.rs index f4da9b770c..b8c0ce4741 100644 --- a/modules/src/core/ics02_client/context.rs +++ b/modules/src/core/ics02_client/context.rs @@ -8,6 +8,7 @@ use crate::core::ics02_client::client_type::ClientType; use crate::core::ics02_client::error::{Error, ErrorDetail}; use crate::core::ics02_client::handler::ClientResult::{self, Create, Update, Upgrade}; use crate::core::ics24_host::identifier::ClientId; +use crate::timestamp::Timestamp; use crate::Height; /// Defines the read-only part of ICS2 (client functions) context. @@ -58,6 +59,20 @@ pub trait ClientReader { /// Returns the current height of the local chain. fn host_height(&self) -> Height; + /// Returns the current timestamp of the local chain. + fn host_timestamp(&self) -> Timestamp { + let pending_consensus_state = self + .pending_host_consensus_state() + .expect("host must have pending consensus state"); + pending_consensus_state.timestamp() + } + + /// Returns the `ConsensusState` of the host (local) chain at a specific height. + fn host_consensus_state(&self, height: Height) -> Result; + + /// Returns the pending `ConsensusState` of the host (local) chain. + fn pending_host_consensus_state(&self) -> Result; + /// 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; @@ -78,8 +93,16 @@ pub trait ClientKeeper { res.consensus_state, )?; self.increase_client_counter(); - self.store_update_time(res.client_id.clone(), res.client_state.latest_height())?; - self.store_update_height(res.client_id, res.client_state.latest_height())?; + self.store_update_time( + res.client_id.clone(), + res.client_state.latest_height(), + res.processed_time, + )?; + self.store_update_height( + res.client_id, + res.client_state.latest_height(), + res.processed_height, + )?; Ok(()) } Update(res) => { @@ -89,8 +112,16 @@ pub trait ClientKeeper { res.client_state.latest_height(), res.consensus_state, )?; - self.store_update_time(res.client_id.clone(), res.client_state.latest_height())?; - self.store_update_height(res.client_id, res.client_state.latest_height())?; + self.store_update_time( + res.client_id.clone(), + res.client_state.latest_height(), + res.processed_time, + )?; + self.store_update_height( + res.client_id, + res.client_state.latest_height(), + res.processed_height, + )?; Ok(()) } Upgrade(res) => { @@ -133,12 +164,22 @@ pub trait ClientKeeper { fn increase_client_counter(&mut self); /// Called upon successful client update. - /// Implementations are expected to use this to record the current (host) time as the time at - /// which this update (or header) was processed. - fn store_update_time(&mut self, client_id: ClientId, height: Height) -> Result<(), Error>; + /// Implementations are expected to use this to record the specified time as the time at which + /// this update (or header) was processed. + fn store_update_time( + &mut self, + client_id: ClientId, + height: Height, + timestamp: Timestamp, + ) -> Result<(), Error>; /// Called upon successful client update. - /// Implementations are expected to use this to record the current (host) height as the height + /// Implementations are expected to use this to record the specified height as the height at /// at which this update (or header) was processed. - fn store_update_height(&mut self, client_id: ClientId, height: Height) -> Result<(), Error>; + fn store_update_height( + &mut self, + client_id: ClientId, + height: Height, + host_height: Height, + ) -> Result<(), Error>; } diff --git a/modules/src/core/ics02_client/error.rs b/modules/src/core/ics02_client/error.rs index 33ea953da1..67a9625cd0 100644 --- a/modules/src/core/ics02_client/error.rs +++ b/modules/src/core/ics02_client/error.rs @@ -249,6 +249,10 @@ define_error! { [ Ics07Error ] | _ | { format_args!("Tendermint-specific handler error") }, + MissingLocalConsensusState + { height: Height } + | e | { format_args!("the local consensus state could not be retrieved for height {}", e.height) }, + } } diff --git a/modules/src/core/ics02_client/handler.rs b/modules/src/core/ics02_client/handler.rs index fdc487bbb7..9b2980e917 100644 --- a/modules/src/core/ics02_client/handler.rs +++ b/modules/src/core/ics02_client/handler.rs @@ -1,7 +1,5 @@ //! This module implements the processing logic for ICS2 (client abstractions and functions) msgs. -use tendermint::Time; - use crate::core::ics02_client::context::ClientReader; use crate::core::ics02_client::error::Error; use crate::core::ics02_client::msgs::ClientMsg; @@ -19,17 +17,13 @@ pub enum ClientResult { } /// General entry point for processing any message related to ICS2 (client functions) protocols. -pub fn dispatch( - now: Time, - ctx: &Ctx, - msg: ClientMsg, -) -> Result, Error> +pub fn dispatch(ctx: &Ctx, msg: ClientMsg) -> Result, Error> where Ctx: ClientReader, { match msg { - ClientMsg::CreateClient(msg) => create_client::process(now, ctx, msg), - ClientMsg::UpdateClient(msg) => update_client::process(now, ctx, msg), + ClientMsg::CreateClient(msg) => create_client::process(ctx, msg), + ClientMsg::UpdateClient(msg) => update_client::process(ctx, msg), ClientMsg::UpgradeClient(msg) => upgrade_client::process(ctx, msg), _ => { unimplemented!() diff --git a/modules/src/core/ics02_client/handler/create_client.rs b/modules/src/core/ics02_client/handler/create_client.rs index c8bb0c9f70..1baf3014d3 100644 --- a/modules/src/core/ics02_client/handler/create_client.rs +++ b/modules/src/core/ics02_client/handler/create_client.rs @@ -1,7 +1,5 @@ //! Protocol logic specific to processing ICS2 messages of type `MsgCreateAnyClient`. -use tendermint::Time; - use crate::prelude::*; use crate::core::ics02_client::client_consensus::AnyConsensusState; @@ -17,6 +15,7 @@ use crate::core::ics24_host::identifier::ClientId; use crate::events::IbcEvent; use crate::handler::{HandlerOutput, HandlerResult}; use crate::timestamp::Timestamp; + /// The result following the successful processing of a `MsgCreateAnyClient` message. Preferably /// this data type should be used with a qualified name `create_client::Result` to avoid ambiguity. #[derive(Clone, Debug, PartialEq, Eq)] @@ -30,7 +29,6 @@ pub struct Result { } pub fn process( - now: Time, ctx: &dyn ClientReader, msg: MsgCreateAnyClient, ) -> HandlerResult { @@ -52,7 +50,7 @@ pub fn process( client_type: msg.client_state().client_type(), client_state: msg.client_state(), consensus_state: msg.consensus_state(), - processed_time: now.into(), + processed_time: ctx.host_timestamp(), processed_height: ctx.host_height(), }); @@ -70,7 +68,6 @@ mod tests { use crate::prelude::*; use core::time::Duration; - use tendermint::Time; use test_log::test; use crate::clients::ics07_tendermint::client_state::{ @@ -107,7 +104,7 @@ mod tests { ) .unwrap(); - let output = dispatch(Time::now(), &ctx, ClientMsg::CreateClient(msg.clone())); + let output = dispatch(&ctx, ClientMsg::CreateClient(msg.clone())); match output { Ok(HandlerOutput { @@ -198,7 +195,7 @@ mod tests { let expected_client_id = ClientId::new(ClientType::Mock, 0).unwrap(); for msg in create_client_msgs { - let output = dispatch(Time::now(), &ctx, ClientMsg::CreateClient(msg.clone())); + let output = dispatch(&ctx, ClientMsg::CreateClient(msg.clone())); match output { Ok(HandlerOutput { @@ -260,7 +257,7 @@ mod tests { ) .unwrap(); - let output = dispatch(Time::now(), &ctx, ClientMsg::CreateClient(msg.clone())); + let output = dispatch(&ctx, ClientMsg::CreateClient(msg.clone())); match output { Ok(HandlerOutput { diff --git a/modules/src/core/ics02_client/handler/update_client.rs b/modules/src/core/ics02_client/handler/update_client.rs index e357357d92..0f8c34f19c 100644 --- a/modules/src/core/ics02_client/handler/update_client.rs +++ b/modules/src/core/ics02_client/handler/update_client.rs @@ -1,7 +1,5 @@ //! Protocol logic specific to processing ICS2 messages of type `MsgUpdateAnyClient`. -use core::convert::From; -use tendermint::Time; use tracing::debug; use crate::core::ics02_client::client_consensus::AnyConsensusState; @@ -12,6 +10,7 @@ use crate::core::ics02_client::error::Error; use crate::core::ics02_client::events::Attributes; use crate::core::ics02_client::handler::ClientResult; use crate::core::ics02_client::header::Header; +use crate::core::ics02_client::height::Height; use crate::core::ics02_client::msgs::update_client::MsgUpdateAnyClient; use crate::core::ics24_host::identifier::ClientId; use crate::events::IbcEvent; @@ -26,10 +25,11 @@ pub struct Result { pub client_id: ClientId, pub client_state: AnyClientState, pub consensus_state: AnyConsensusState, + pub processed_time: Timestamp, + pub processed_height: Height, } pub fn process( - now: Time, ctx: &dyn ClientReader, msg: MsgUpdateAnyClient, ) -> HandlerResult { @@ -62,13 +62,11 @@ pub fn process( debug!("latest consensus state: {:?}", latest_consensus_state); - let duration = Timestamp::from(now) + let now = ctx.host_timestamp(); + let duration = now .duration_since(&latest_consensus_state.timestamp()) .ok_or_else(|| { - Error::invalid_consensus_state_timestamp( - latest_consensus_state.timestamp(), - Timestamp::from(now), - ) + Error::invalid_consensus_state_timestamp(latest_consensus_state.timestamp(), now) })?; if client_state.expired(duration) { @@ -82,13 +80,15 @@ pub fn process( // 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(now, ctx, client_id.clone(), 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 { client_id: client_id.clone(), client_state: new_client_state, consensus_state: new_consensus_state, + processed_time: ctx.host_timestamp(), + processed_height: ctx.host_height(), }); let event_attributes = Attributes { @@ -103,7 +103,6 @@ pub fn process( #[cfg(test)] mod tests { use core::str::FromStr; - use tendermint::Time; use test_log::test; use crate::core::ics02_client::client_consensus::AnyConsensusState; @@ -143,7 +142,7 @@ mod tests { signer, }; - let output = dispatch(Time::now(), &ctx, ClientMsg::UpdateClient(msg.clone())); + let output = dispatch(&ctx, ClientMsg::UpdateClient(msg.clone())); match output { Ok(HandlerOutput { @@ -190,7 +189,7 @@ mod tests { signer, }; - let output = dispatch(Time::now(), &ctx, ClientMsg::UpdateClient(msg.clone())); + let output = dispatch(&ctx, ClientMsg::UpdateClient(msg.clone())); match output { Err(Error(ErrorDetail::ClientNotFound(e), _)) => { @@ -226,7 +225,7 @@ mod tests { signer: signer.clone(), }; - let output = dispatch(Time::now(), &ctx, ClientMsg::UpdateClient(msg.clone())); + let output = dispatch(&ctx, ClientMsg::UpdateClient(msg.clone())); match output { Ok(HandlerOutput { @@ -293,7 +292,7 @@ mod tests { signer, }; - let output = dispatch(Time::now(), &ctx, ClientMsg::UpdateClient(msg.clone())); + let output = dispatch(&ctx, ClientMsg::UpdateClient(msg.clone())); match output { Ok(HandlerOutput { @@ -370,7 +369,7 @@ mod tests { signer, }; - let output = dispatch(Time::now(), &ctx, ClientMsg::UpdateClient(msg.clone())); + let output = dispatch(&ctx, ClientMsg::UpdateClient(msg.clone())); match output { Ok(HandlerOutput { @@ -450,7 +449,7 @@ mod tests { signer, }; - let output = dispatch(Time::now(), &ctx, ClientMsg::UpdateClient(msg.clone())); + let output = dispatch(&ctx, ClientMsg::UpdateClient(msg.clone())); match output { Ok(HandlerOutput { @@ -524,7 +523,7 @@ mod tests { signer, }; - let output = dispatch(Time::now(), &ctx, ClientMsg::UpdateClient(msg)); + let output = dispatch(&ctx, ClientMsg::UpdateClient(msg)); match output { Ok(_) => { diff --git a/modules/src/core/ics02_client/handler/upgrade_client.rs b/modules/src/core/ics02_client/handler/upgrade_client.rs index 84e9520071..6276c6f574 100644 --- a/modules/src/core/ics02_client/handler/upgrade_client.rs +++ b/modules/src/core/ics02_client/handler/upgrade_client.rs @@ -78,7 +78,6 @@ mod tests { use crate::prelude::*; use core::str::FromStr; - use tendermint::Time; use crate::core::ics02_client::error::{Error, ErrorDetail}; use crate::core::ics02_client::handler::dispatch; @@ -110,7 +109,7 @@ mod tests { signer, }; - let output = dispatch(Time::now(), &ctx, ClientMsg::UpgradeClient(msg.clone())); + let output = dispatch(&ctx, ClientMsg::UpgradeClient(msg.clone())); match output { Ok(HandlerOutput { @@ -155,7 +154,7 @@ mod tests { signer, }; - let output = dispatch(Time::now(), &ctx, ClientMsg::UpgradeClient(msg.clone())); + let output = dispatch(&ctx, ClientMsg::UpgradeClient(msg.clone())); match output { Err(Error(ErrorDetail::ClientNotFound(e), _)) => { @@ -183,7 +182,7 @@ mod tests { signer, }; - let output = dispatch(Time::now(), &ctx, ClientMsg::UpgradeClient(msg.clone())); + let output = dispatch(&ctx, ClientMsg::UpgradeClient(msg.clone())); match output { Err(Error(ErrorDetail::LowUpgradeHeight(e), _)) => { diff --git a/modules/src/core/ics04_channel/context.rs b/modules/src/core/ics04_channel/context.rs index 9ea3b237b8..65c9aaedfe 100644 --- a/modules/src/core/ics04_channel/context.rs +++ b/modules/src/core/ics04_channel/context.rs @@ -70,7 +70,18 @@ pub trait ChannelReader { fn host_height(&self) -> Height; /// Returns the current timestamp of the local chain. - fn host_timestamp(&self) -> Timestamp; + fn host_timestamp(&self) -> Timestamp { + let pending_consensus_state = self + .pending_host_consensus_state() + .expect("host must have pending consensus state"); + pending_consensus_state.timestamp() + } + + /// Returns the `ConsensusState` of the host (local) chain at a specific height. + fn host_consensus_state(&self, height: Height) -> Result; + + /// Returns the pending `ConsensusState` of the host (local) chain. + fn pending_host_consensus_state(&self) -> Result; /// Returns the time when the client state for the given [`ClientId`] was updated with a header for the given [`Height`] fn client_update_time(&self, client_id: &ClientId, height: Height) -> Result; diff --git a/modules/src/core/ics04_channel/handler/recv_packet.rs b/modules/src/core/ics04_channel/handler/recv_packet.rs index cbf69fb3f3..f653eea275 100644 --- a/modules/src/core/ics04_channel/handler/recv_packet.rs +++ b/modules/src/core/ics04_channel/handler/recv_packet.rs @@ -251,7 +251,6 @@ mod tests { 1.into(), ) .with_height(host_height) - .with_timestamp(Timestamp::from_nanoseconds(1).unwrap()) // This `with_recv_sequence` is required for ordered channels .with_recv_sequence( packet.destination_port.clone(), @@ -269,8 +268,7 @@ mod tests { .with_port_capability(PortId::default()) .with_channel(PortId::default(), ChannelId::default(), dest_channel_end) .with_send_sequence(PortId::default(), ChannelId::default(), 1.into()) - .with_height(host_height) - .with_timestamp(Timestamp::from_nanoseconds(3).unwrap()), + .with_height(host_height), msg: msg_packet_old, want_pass: false, }, diff --git a/modules/src/core/ics04_channel/msgs/recv_packet.rs b/modules/src/core/ics04_channel/msgs/recv_packet.rs index e2c771ff30..04f4d93856 100644 --- a/modules/src/core/ics04_channel/msgs/recv_packet.rs +++ b/modules/src/core/ics04_channel/msgs/recv_packet.rs @@ -99,12 +99,19 @@ pub mod test_util { use crate::core::ics04_channel::packet::test_utils::get_dummy_raw_packet; use crate::test_utils::{get_dummy_bech32_account, get_dummy_proof}; + use crate::timestamp::Timestamp; + use core::ops::Add; + use core::time::Duration; /// Returns a dummy `RawMsgRecvPacket`, for testing only! The `height` parametrizes both the /// proof height as well as the timeout height. pub fn get_dummy_raw_msg_recv_packet(height: u64) -> RawMsgRecvPacket { + let timestamp = Timestamp::now().add(Duration::from_secs(9)); RawMsgRecvPacket { - packet: Some(get_dummy_raw_packet(height, 9)), + packet: Some(get_dummy_raw_packet( + height, + timestamp.unwrap().nanoseconds(), + )), proof_commitment: get_dummy_proof(), proof_height: Some(RawHeight { revision_number: 0, diff --git a/modules/src/core/ics26_routing/handler.rs b/modules/src/core/ics26_routing/handler.rs index 2d1fc504db..78c66b251e 100644 --- a/modules/src/core/ics26_routing/handler.rs +++ b/modules/src/core/ics26_routing/handler.rs @@ -1,7 +1,6 @@ use crate::prelude::*; use prost_types::Any; -use tendermint::Time; use crate::applications::ics20_fungible_token_transfer::relay_application_logic::send_transfer::send_transfer as ics20_msg_dispatcher; use crate::core::ics02_client::handler::dispatch as ics2_msg_dispatcher; @@ -18,7 +17,7 @@ use crate::{events::IbcEvent, handler::HandlerOutput}; /// Mimics the DeliverTx ABCI interface, but a slightly lower level. No need for authentication /// info or signature checks here. /// Returns a vector of all events that got generated as a byproduct of processing `messages`. -pub fn deliver(now: Time, ctx: &mut Ctx, messages: Vec) -> Result, Error> +pub fn deliver(ctx: &mut Ctx, messages: Vec) -> Result, Error> where Ctx: Ics26Context, { @@ -33,7 +32,7 @@ where let envelope = decode(any_msg)?; // Process the envelope, and accumulate any events that were generated. - let mut output = dispatch(now, &mut ctx_interim, envelope)?; + let mut output = dispatch(&mut ctx_interim, envelope)?; // TODO: output.log and output.result are discarded res.append(&mut output.events); } @@ -51,17 +50,13 @@ pub fn decode(message: Any) -> Result { /// Top-level ICS dispatch function. Routes incoming IBC messages to their corresponding module. /// Returns a handler output with empty result of type `HandlerOutput<()>` which contains the log /// and events produced after processing the input `msg`. -pub fn dispatch( - now: Time, - ctx: &mut Ctx, - msg: Ics26Envelope, -) -> Result, Error> +pub fn dispatch(ctx: &mut Ctx, msg: Ics26Envelope) -> Result, Error> where Ctx: Ics26Context, { let output = match msg { Ics2Msg(msg) => { - let handler_output = ics2_msg_dispatcher(now, ctx, msg).map_err(Error::ics02_client)?; + let handler_output = ics2_msg_dispatcher(ctx, msg).map_err(Error::ics02_client)?; // Apply the result to the context (host chain store). ctx.store_client_result(handler_output.result) .map_err(Error::ics02_client)?; @@ -134,7 +129,6 @@ where mod tests { use crate::prelude::*; - use tendermint::Time; use test_log::test; use crate::core::ics02_client::client_consensus::AnyConsensusState; @@ -274,7 +268,6 @@ mod tests { // First, create a client.. let res = dispatch( - Time::now(), &mut ctx, Ics26Envelope::Ics2Msg(ClientMsg::CreateClient(create_client_msg.clone())), ); @@ -475,7 +468,7 @@ mod tests { .collect(); for test in tests { - let res = dispatch(Time::now(), &mut ctx, test.msg.clone()); + let res = dispatch(&mut ctx, test.msg.clone()); assert_eq!( test.want_pass, diff --git a/modules/src/mock/client_def.rs b/modules/src/mock/client_def.rs index 9a771e5551..2691a559ba 100644 --- a/modules/src/mock/client_def.rs +++ b/modules/src/mock/client_def.rs @@ -1,5 +1,4 @@ use ibc_proto::ibc::core::commitment::v1::MerkleProof; -use tendermint::Time; use crate::core::ics02_client::client_consensus::AnyConsensusState; use crate::core::ics02_client::client_def::ClientDef; @@ -32,7 +31,6 @@ impl ClientDef for MockClient { fn check_header_and_update_state( &self, - _now: Time, _ctx: &dyn ClientReader, _client_id: ClientId, client_state: Self::ClientState, diff --git a/modules/src/mock/context.rs b/modules/src/mock/context.rs index 4332dc983a..9af4c2ee85 100644 --- a/modules/src/mock/context.rs +++ b/modules/src/mock/context.rs @@ -4,12 +4,12 @@ use crate::prelude::*; use alloc::collections::btree_map::BTreeMap; use core::cmp::min; - -use tracing::debug; +use core::ops::{Add, Sub}; +use core::time::Duration; use prost_types::Any; use sha2::Digest; -use tendermint::Time; +use tracing::debug; use crate::applications::ics20_fungible_token_transfer::context::Ics20Context; use crate::clients::ics07_tendermint::client_state::test_util::get_dummy_tendermint_client_state; @@ -43,7 +43,8 @@ use crate::relayer::ics18_relayer::error::Error as Ics18Error; use crate::signer::Signer; use crate::timestamp::Timestamp; use crate::Height; -use core::time::Duration; + +pub const DEFAULT_BLOCK_TIME_SECS: u64 = 3; /// A context implementing the dependencies necessary for testing any IBC module. #[derive(Clone, Debug)] @@ -57,12 +58,6 @@ pub struct MockContext { /// Maximum size for the history of the host chain. Any block older than this is pruned. max_history_size: usize, - /// Highest height (i.e., most recent) of the blocks in the history. - latest_height: Height, - - /// Highest timestamp, i.e., of the most recent block in the history. - timestamp: Timestamp, - /// The chain of blocks underlying this context. A vector of size up to `max_history_size` /// blocks, ascending order by their height (latest block is on the last position). history: Vec, @@ -117,6 +112,9 @@ pub struct MockContext { // Used by unordered channel packet_receipt: BTreeMap<(PortId, ChannelId, Sequence), Receipt>, + + /// Average time duration between blocks + block_time: Duration, } /// Returns a MockContext with bare minimum initialization: no clients, no connections and no channels are @@ -151,7 +149,12 @@ impl MockContext { "The chain must have a non-zero max_history_size" ); - // Compute the number of blocks to store. If latest_height is 0, nothing is stored. + assert_ne!( + latest_height.revision_height, 0, + "The chain must have a non-zero max_history_size" + ); + + // Compute the number of blocks to store. let n = min(max_history_size as u64, latest_height.revision_height); assert_eq!( @@ -160,19 +163,24 @@ impl MockContext { "The version in the chain identifier must match the version in the latest height" ); + let block_time = Duration::from_secs(DEFAULT_BLOCK_TIME_SECS); + let next_block_timestamp = Timestamp::now().add(block_time).unwrap(); MockContext { host_chain_type: host_type, host_chain_id: host_id.clone(), max_history_size, - latest_height, - timestamp: Default::default(), history: (0..n) .rev() .map(|i| { + // generate blocks with timestamps -> N, N - BT, N - 2BT, ... + // where N = now(), BT = block_time HostBlock::generate_block( host_id.clone(), host_type, latest_height.sub(i).unwrap().revision_height, + next_block_timestamp + .sub(Duration::from_secs(DEFAULT_BLOCK_TIME_SECS * (i + 1))) + .unwrap(), ) }) .collect(), @@ -193,6 +201,7 @@ impl MockContext { packet_acknowledgement: Default::default(), connection_ids_counter: 0, channel_ids_counter: 0, + block_time, } } @@ -230,6 +239,7 @@ impl MockContext { let light_block = HostBlock::generate_tm_block( self.host_chain_id.clone(), cs_height.revision_height, + Timestamp::now(), ); let consensus_state = AnyConsensusState::from(light_block.clone()); @@ -264,6 +274,7 @@ impl MockContext { let prev_cs_height = cs_height.clone().sub(1).unwrap_or(client_state_height); let client_type = client_type.unwrap_or(ClientType::Mock); + let now = Timestamp::now(); let (client_state, consensus_state) = match client_type { // If it's a mock client, create the corresponding mock states. @@ -276,6 +287,7 @@ impl MockContext { let light_block = HostBlock::generate_tm_block( self.host_chain_id.clone(), cs_height.revision_height, + now, ); let consensus_state = AnyConsensusState::from(light_block.clone()); @@ -295,6 +307,7 @@ impl MockContext { let light_block = HostBlock::generate_tm_block( self.host_chain_id.clone(), prev_cs_height.revision_height, + now.sub(self.block_time).unwrap(), ); AnyConsensusState::from(light_block) } @@ -388,21 +401,18 @@ impl MockContext { } } - pub fn with_timestamp(self, timestamp: Timestamp) -> Self { - Self { timestamp, ..self } - } - pub fn with_height(self, target_height: Height) -> Self { - if target_height.revision_number > self.latest_height.revision_number { + let latest_height = self.latest_height(); + if target_height.revision_number > latest_height.revision_number { unimplemented!() - } else if target_height.revision_number < self.latest_height.revision_number { + } else if target_height.revision_number < latest_height.revision_number { panic!("Cannot rewind history of the chain to a smaller revision number!") - } else if target_height.revision_height < self.latest_height.revision_height { + } else if target_height.revision_height < latest_height.revision_height { panic!("Cannot rewind history of the chain to a smaller revision height!") - } else if target_height.revision_height > self.latest_height.revision_height { + } else if target_height.revision_height > latest_height.revision_height { // Repeatedly advance the host chain height till we hit the desired height let mut ctx = MockContext { ..self }; - while ctx.latest_height.revision_height < target_height.revision_height { + while ctx.latest_height().revision_height < target_height.revision_height { ctx.advance_host_chain_height() } ctx @@ -431,7 +441,7 @@ impl MockContext { /// Returns `None` if the block at the requested height does not exist. 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; + let latest = self.latest_height().revision_height as usize; // Check that the block is not too advanced, nor has it been pruned. if (target > latest) || (target <= latest - self.history.len()) { @@ -443,10 +453,12 @@ impl MockContext { /// Triggers the advancing of the host chain, by extending the history of blocks (or headers). pub fn advance_host_chain_height(&mut self) { + let latest_block = self.history.last().expect("history cannot be empty"); let new_block = HostBlock::generate_block( self.host_chain_id.clone(), self.host_chain_type, - self.latest_height.increment().revision_height, + latest_block.height().increment().revision_height, + latest_block.timestamp().add(self.block_time).unwrap(), ); // Append the new header at the tip of the history. @@ -458,14 +470,13 @@ impl MockContext { // History is not full yet. self.history.push(new_block); } - self.latest_height = self.latest_height.increment(); } /// A datagram passes from the relayer to the IBC module (on host chain). /// Alternative method to `Ics18Context::send` that does not exercise any serialization. /// Used in testing the Ics18 algorithms, hence this may return a Ics18Error. pub fn deliver(&mut self, msg: Ics26Envelope) -> Result<(), Ics18Error> { - dispatch(Time::now(), self, msg).map_err(Ics18Error::transaction_failed)?; + dispatch(self, msg).map_err(Ics18Error::transaction_failed)?; // Create a new block. self.advance_host_chain_height(); Ok(()) @@ -483,7 +494,7 @@ impl MockContext { // Get the highest block. let lh = &self.history[self.history.len() - 1]; // Check latest is properly updated with highest header height. - if lh.height() != self.latest_height { + if lh.height() != self.latest_height() { return Err("latest height is not updated".to_string()); } } @@ -528,6 +539,14 @@ impl MockContext { .get(height) .unwrap() } + + #[inline] + fn latest_height(&self) -> Height { + self.history + .last() + .expect("history cannot be empty") + .height() + } } impl Ics26Context for MockContext {} @@ -668,11 +687,20 @@ impl ChannelReader for MockContext { } fn host_height(&self) -> Height { - self.latest_height + self.latest_height() } fn host_timestamp(&self) -> Timestamp { - self.timestamp + ClientReader::host_timestamp(self) + } + + fn host_consensus_state(&self, height: Height) -> Result { + ConnectionReader::host_consensus_state(self, height).map_err(Ics04Error::ics03_connection) + } + + fn pending_host_consensus_state(&self) -> Result { + ClientReader::pending_host_consensus_state(self) + .map_err(|e| Ics04Error::ics03_connection(Ics03Error::ics02_client(e))) } fn client_update_time( @@ -714,7 +742,7 @@ impl ChannelReader for MockContext { } fn max_expected_time_per_block(&self) -> Duration { - Duration::from_secs(10) + self.block_time } } @@ -835,7 +863,7 @@ impl ConnectionReader for MockContext { } fn host_current_height(&self) -> Height { - self.latest_height + self.latest_height() } fn host_oldest_height(&self) -> Height { @@ -858,10 +886,7 @@ impl ConnectionReader for MockContext { } fn host_consensus_state(&self, height: Height) -> Result { - match self.host_block(height) { - Some(block_ref) => Ok(block_ref.clone().into()), - None => Err(Ics03Error::missing_local_consensus_state(height)), - } + ClientReader::host_consensus_state(self, height).map_err(Ics03Error::ics02_client) } fn connection_counter(&self) -> Result { @@ -988,7 +1013,27 @@ impl ClientReader for MockContext { } fn host_height(&self) -> Height { - self.latest_height + self.latest_height() + } + + fn host_timestamp(&self) -> Timestamp { + self.history + .last() + .expect("history cannot be empty") + .timestamp() + .add(self.block_time) + .unwrap() + } + + fn host_consensus_state(&self, height: Height) -> Result { + match self.host_block(height) { + Some(block_ref) => Ok(block_ref.clone().into()), + None => Err(Ics02Error::missing_local_consensus_state(height)), + } + } + + fn pending_host_consensus_state(&self) -> Result { + Err(Ics02Error::missing_local_consensus_state(Height::zero())) } fn client_counter(&self) -> Result { @@ -1049,10 +1094,15 @@ impl ClientKeeper for MockContext { self.client_ids_counter += 1 } - fn store_update_time(&mut self, client_id: ClientId, height: Height) -> Result<(), Ics02Error> { + fn store_update_time( + &mut self, + client_id: ClientId, + height: Height, + timestamp: Timestamp, + ) -> Result<(), Ics02Error> { let _ = self .client_processed_times - .insert((client_id, height), ChannelReader::host_timestamp(self)); + .insert((client_id, height), timestamp); Ok(()) } @@ -1060,10 +1110,11 @@ impl ClientKeeper for MockContext { &mut self, client_id: ClientId, height: Height, + host_height: Height, ) -> Result<(), Ics02Error> { let _ = self .client_processed_heights - .insert((client_id, height), ClientReader::host_height(self)); + .insert((client_id, height), host_height); Ok(()) } } @@ -1085,7 +1136,7 @@ impl Ics18Context for MockContext { fn send(&mut self, msgs: Vec) -> Result, Ics18Error> { // Forward call to Ics26 delivery method. - let events = deliver(Time::now(), self, msgs).map_err(Ics18Error::transaction_failed)?; + let events = deliver(self, msgs).map_err(Ics18Error::transaction_failed)?; self.advance_host_chain_height(); // Advance chain height Ok(events) @@ -1119,8 +1170,8 @@ mod tests { ctx: MockContext::new( ChainId::new("mockgaia".to_string(), cv), HostType::Mock, - 1, - Height::new(cv, 0), + 2, + Height::new(cv, 1), ), }, Test { @@ -1128,8 +1179,8 @@ mod tests { ctx: MockContext::new( ChainId::new("mocksgaia".to_string(), cv), HostType::SyntheticTendermint, - 1, - Height::new(cv, 0), + 2, + Height::new(cv, 1), ), }, Test { @@ -1215,7 +1266,7 @@ mod tests { test.ctx ); - let current_height = test.ctx.latest_height; + let current_height = test.ctx.latest_height(); // After advancing the chain's height, the context should still be valid. test.ctx.advance_host_chain_height(); @@ -1228,7 +1279,8 @@ mod tests { let next_height = current_height.increment(); assert_eq!( - test.ctx.latest_height, next_height, + test.ctx.latest_height(), + next_height, "Failed while increasing height for context {:?}", test.ctx ); diff --git a/modules/src/mock/host.rs b/modules/src/mock/host.rs index 106d32bfa0..d94b90f893 100644 --- a/modules/src/mock/host.rs +++ b/modules/src/mock/host.rs @@ -1,9 +1,7 @@ //! Host chain types and methods, used by context mock. -use tendermint::time::Time; use tendermint_testgen::light_block::TmLightBlock; use tendermint_testgen::{Generator, LightBlock as TestgenLightBlock}; -use time::OffsetDateTime; use crate::clients::ics07_tendermint::consensus_state::ConsensusState as TMConsensusState; use crate::clients::ics07_tendermint::header::Header as TMHeader; @@ -46,25 +44,42 @@ impl HostBlock { } } + /// Returns the timestamp of a block. + pub fn timestamp(&self) -> Timestamp { + match self { + HostBlock::Mock(header) => header.timestamp, + HostBlock::SyntheticTendermint(light_block) => { + light_block.signed_header.header.time.into() + } + } + } + /// Generates a new block at `height` for the given chain identifier and chain type. - pub fn generate_block(chain_id: ChainId, chain_type: HostType, height: u64) -> HostBlock { + pub fn generate_block( + chain_id: ChainId, + chain_type: HostType, + height: u64, + timestamp: Timestamp, + ) -> HostBlock { match chain_type { HostType::Mock => HostBlock::Mock(MockHeader { height: Height::new(chain_id.version(), height), - timestamp: Timestamp::now(), + timestamp, }), - HostType::SyntheticTendermint => { - HostBlock::SyntheticTendermint(Box::new(Self::generate_tm_block(chain_id, height))) - } + HostType::SyntheticTendermint => HostBlock::SyntheticTendermint(Box::new( + Self::generate_tm_block(chain_id, height, timestamp), + )), } } - pub fn generate_tm_block(chain_id: ChainId, height: u64) -> TmLightBlock { - let time: Time = OffsetDateTime::now_utc().try_into().unwrap(); - - TestgenLightBlock::new_default_with_time_and_chain_id(chain_id.to_string(), time, height) - .generate() - .unwrap() + pub fn generate_tm_block(chain_id: ChainId, height: u64, timestamp: Timestamp) -> TmLightBlock { + TestgenLightBlock::new_default_with_time_and_chain_id( + chain_id.to_string(), + timestamp.into_tm_time().unwrap(), + height, + ) + .generate() + .unwrap() } } diff --git a/modules/src/timestamp.rs b/modules/src/timestamp.rs index eee1272d84..64097f477d 100644 --- a/modules/src/timestamp.rs +++ b/modules/src/timestamp.rs @@ -134,6 +134,11 @@ impl Timestamp { self.time.map(Into::into) } + /// Convert a `Timestamp` to an optional [`Tendermint::Time`] + pub fn into_tm_time(self) -> Option