diff --git a/modules/Cargo.toml b/modules/Cargo.toml index fede13d7bf..4242284aba 100644 --- a/modules/Cargo.toml +++ b/modules/Cargo.toml @@ -14,6 +14,11 @@ description = """ Implementation of the Inter-Blockchain Communication Protocol (IBC). """ +[features] +# This feature grants access to development-time mocking libraries, such as `MockContext` or `MockHeader`. +# Dependens on the testgen suite for generating Tendermint light blocks. +mocks = [ "tendermint-testgen" ] + [dependencies] tendermint-proto = "0.1.0" @@ -35,11 +40,19 @@ regex = "1" [dependencies.tendermint] version = "0.17.0-rc1" +git = "https://github.com/informalsystems/tendermint-rs" [dependencies.tendermint-rpc] version = "0.17.0-rc1" features = ["http-client", "websocket-client"] +git = "https://github.com/informalsystems/tendermint-rs" + +[dependencies.tendermint-testgen] +version = "0.17.0-rc1" +git = "https://github.com/informalsystems/tendermint-rs" +optional = true [dev-dependencies] tokio = { version = "0.2", features = ["macros"] } subtle-encoding = { version = "0.5" } +tendermint-testgen = { version = "0.17.0-rc1", git = "https://github.com/informalsystems/tendermint-rs" } # Needed for generating (synthetic) light blocks. \ No newline at end of file diff --git a/modules/src/ics02_client/client_def.rs b/modules/src/ics02_client/client_def.rs index 4c31f8ebc7..14d2f06902 100644 --- a/modules/src/ics02_client/client_def.rs +++ b/modules/src/ics02_client/client_def.rs @@ -24,7 +24,7 @@ pub const TENDERMINT_CONSENSUS_STATE_TYPE_URL: &str = pub const MOCK_CLIENT_STATE_TYPE_URL: &str = "/ibc.mock.ClientState"; pub const MOCK_CONSENSUS_STATE_TYPE_URL: &str = "/ibc.mock.ConsensusState"; -#[cfg(test)] +#[cfg(any(test, feature = "mocks"))] use { crate::mock_client::client_def::MockClient, crate::mock_client::header::MockHeader, @@ -92,7 +92,7 @@ pub trait ClientDef: Clone { pub enum AnyHeader { Tendermint(tendermint::header::Header), - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] Mock(MockHeader), } @@ -101,7 +101,7 @@ impl Header for AnyHeader { match self { Self::Tendermint(header) => header.client_type(), - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] Self::Mock(header) => header.client_type(), } } @@ -110,7 +110,7 @@ impl Header for AnyHeader { match self { Self::Tendermint(header) => header.height(), - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] Self::Mock(header) => header.height(), } } @@ -120,7 +120,7 @@ impl Header for AnyHeader { pub enum AnyClientState { Tendermint(TendermintClientState), - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] Mock(MockClientState), } @@ -129,7 +129,7 @@ impl AnyClientState { match self { Self::Tendermint(tm_state) => tm_state.latest_height(), - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] Self::Mock(mock_state) => mock_state.latest_height(), } } @@ -137,7 +137,7 @@ impl AnyClientState { match self { Self::Tendermint(state) => state.client_type(), - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] Self::Mock(state) => state.client_type(), } } @@ -155,7 +155,7 @@ impl TryFrom for AnyClientState { TendermintClientState::decode_vec(&raw.value)?, )), - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] MOCK_CLIENT_STATE_TYPE_URL => Ok(AnyClientState::Mock(MockClientState::decode_vec( &raw.value, )?)), @@ -174,7 +174,7 @@ impl From for Any { type_url: TENDERMINT_CLIENT_STATE_TYPE_URL.to_string(), value: value.encode_vec().unwrap(), }, - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] AnyClientState::Mock(value) => Any { type_url: MOCK_CLIENT_STATE_TYPE_URL.to_string(), value: value.encode_vec().unwrap(), @@ -200,7 +200,7 @@ impl ClientState for AnyClientState { match self { AnyClientState::Tendermint(tm_state) => tm_state.is_frozen(), - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] AnyClientState::Mock(mock_state) => mock_state.is_frozen(), } } @@ -210,7 +210,7 @@ impl ClientState for AnyClientState { pub enum AnyConsensusState { Tendermint(crate::ics07_tendermint::consensus_state::ConsensusState), - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] Mock(MockConsensusState), } @@ -219,7 +219,7 @@ impl AnyConsensusState { match self { AnyConsensusState::Tendermint(_cs) => ClientType::Tendermint, - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] AnyConsensusState::Mock(_cs) => ClientType::Mock, } } @@ -236,7 +236,7 @@ impl TryFrom for AnyConsensusState { TendermintConsensusState::decode_vec(&value.value)?, )), - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] MOCK_CONSENSUS_STATE_TYPE_URL => Ok(AnyConsensusState::Mock( MockConsensusState::decode_vec(&value.value)?, )), @@ -255,7 +255,7 @@ impl From for Any { type_url: TENDERMINT_CONSENSUS_STATE_TYPE_URL.to_string(), value: value.encode_vec().unwrap(), }, - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] AnyConsensusState::Mock(value) => Any { type_url: MOCK_CONSENSUS_STATE_TYPE_URL.to_string(), value: value.encode_vec().unwrap(), @@ -282,7 +282,7 @@ impl ConsensusState for AnyConsensusState { pub enum AnyClient { Tendermint(TendermintClient), - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] Mock(MockClient), } @@ -291,7 +291,7 @@ impl AnyClient { match client_type { ClientType::Tendermint => Self::Tendermint(TendermintClient), - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] ClientType::Mock => Self::Mock(MockClient), } } @@ -325,7 +325,7 @@ impl ClientDef for AnyClient { )) } - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] Self::Mock(client) => { let (client_state, header) = downcast!( client_state => AnyClientState::Mock, @@ -372,7 +372,7 @@ impl ClientDef for AnyClient { ) } - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] Self::Mock(client) => { let client_state = downcast!( client_state => AnyClientState::Mock @@ -416,7 +416,7 @@ impl ClientDef for AnyClient { ) } - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] Self::Mock(client) => { let client_state = downcast!(client_state => AnyClientState::Mock) .ok_or_else(|| error::Kind::ClientArgsTypeMismatch(ClientType::Mock))?; @@ -461,7 +461,7 @@ impl ClientDef for AnyClient { ) } - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] Self::Mock(client) => { let client_state = downcast!( client_state => AnyClientState::Mock diff --git a/modules/src/ics02_client/client_type.rs b/modules/src/ics02_client/client_type.rs index fa61c5c7ce..ad0e006de8 100644 --- a/modules/src/ics02_client/client_type.rs +++ b/modules/src/ics02_client/client_type.rs @@ -6,7 +6,7 @@ use serde_derive::{Deserialize, Serialize}; pub enum ClientType { Tendermint = 1, - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] Mock = 9999, } @@ -16,7 +16,7 @@ impl ClientType { match self { Self::Tendermint => "Tendermint", - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] Self::Mock => "mock", } } @@ -29,7 +29,7 @@ impl std::str::FromStr for ClientType { match s { "Tendermint" => Ok(Self::Tendermint), - #[cfg(test)] + #[cfg(any(test, feature = "mocks"))] "mock" => Ok(Self::Mock), _ => Err(error::Kind::UnknownClientType(s.to_string()).into()), diff --git a/modules/src/ics02_client/handler/create_client.rs b/modules/src/ics02_client/handler/create_client.rs index f674f90730..66c1c435d4 100644 --- a/modules/src/ics02_client/handler/create_client.rs +++ b/modules/src/ics02_client/handler/create_client.rs @@ -53,9 +53,9 @@ mod tests { use crate::ics07_tendermint::client_state::ClientState; use crate::ics07_tendermint::header::test_util::get_dummy_header; use crate::ics24_host::identifier::ClientId; + use crate::mock::context::MockContext; use crate::mock_client::header::MockHeader; use crate::mock_client::state::{MockClientState, MockConsensusState}; - use crate::mock_context::MockContext; use crate::Height; use std::str::FromStr; use std::time::Duration; diff --git a/modules/src/ics02_client/handler/update_client.rs b/modules/src/ics02_client/handler/update_client.rs index 1379758485..864320f518 100644 --- a/modules/src/ics02_client/handler/update_client.rs +++ b/modules/src/ics02_client/handler/update_client.rs @@ -71,9 +71,9 @@ mod tests { use crate::ics02_client::msgs::{ClientMsg, MsgUpdateAnyClient}; use crate::ics03_connection::msgs::test_util::get_dummy_account_id; use crate::ics24_host::identifier::ClientId; + use crate::mock::context::MockContext; use crate::mock_client::header::MockHeader; use crate::mock_client::state::MockClientState; - use crate::mock_context::MockContext; use crate::Height; use std::str::FromStr; diff --git a/modules/src/ics03_connection/handler/conn_open_ack.rs b/modules/src/ics03_connection/handler/conn_open_ack.rs index 78b4af56ec..cc290a0501 100644 --- a/modules/src/ics03_connection/handler/conn_open_ack.rs +++ b/modules/src/ics03_connection/handler/conn_open_ack.rs @@ -101,8 +101,9 @@ mod tests { use crate::ics03_connection::msgs::conn_open_ack::MsgConnectionOpenAck; use crate::ics03_connection::msgs::ConnectionMsg; use crate::ics23_commitment::commitment::CommitmentPrefix; - use crate::ics24_host::identifier::ClientId; - use crate::mock_context::MockContext; + use crate::ics24_host::identifier::{ChainId, ClientId}; + use crate::mock::context::MockContext; + use crate::mock::host::HostType; use crate::Height; #[test] @@ -163,6 +164,8 @@ mod tests { // Parametrize the (correct) host chain to have a height at least as recent as the // the height of the proofs in the Ack msg. let correct_context = MockContext::new( + ChainId::new("mockgaia", 1).unwrap(), + HostType::Mock, 5, Height::new(1, msg_ack.proofs().height().increment().version_height), ); diff --git a/modules/src/ics03_connection/handler/conn_open_confirm.rs b/modules/src/ics03_connection/handler/conn_open_confirm.rs index 220646d210..31b1230690 100644 --- a/modules/src/ics03_connection/handler/conn_open_confirm.rs +++ b/modules/src/ics03_connection/handler/conn_open_confirm.rs @@ -86,7 +86,7 @@ mod tests { use crate::ics03_connection::msgs::ConnectionMsg; use crate::ics23_commitment::commitment::CommitmentPrefix; use crate::ics24_host::identifier::ClientId; - use crate::mock_context::MockContext; + use crate::mock::context::MockContext; use crate::Height; #[test] diff --git a/modules/src/ics03_connection/handler/conn_open_init.rs b/modules/src/ics03_connection/handler/conn_open_init.rs index ae31b488c9..12cb44a4a5 100644 --- a/modules/src/ics03_connection/handler/conn_open_init.rs +++ b/modules/src/ics03_connection/handler/conn_open_init.rs @@ -54,7 +54,7 @@ mod tests { use crate::ics03_connection::msgs::conn_open_init::test_util::get_dummy_msg_conn_open_init; use crate::ics03_connection::msgs::conn_open_init::MsgConnectionOpenInit; use crate::ics03_connection::msgs::ConnectionMsg; - use crate::mock_context::MockContext; + use crate::mock::context::MockContext; use crate::Height; #[test] diff --git a/modules/src/ics03_connection/handler/conn_open_try.rs b/modules/src/ics03_connection/handler/conn_open_try.rs index ec4f4cc481..0fa9fc4ab1 100644 --- a/modules/src/ics03_connection/handler/conn_open_try.rs +++ b/modules/src/ics03_connection/handler/conn_open_try.rs @@ -114,7 +114,9 @@ mod tests { use crate::ics03_connection::msgs::conn_open_try::test_util::get_dummy_msg_conn_open_try; use crate::ics03_connection::msgs::conn_open_try::MsgConnectionOpenTry; use crate::ics03_connection::msgs::ConnectionMsg; - use crate::mock_context::MockContext; + use crate::ics24_host::identifier::ChainId; + use crate::mock::context::MockContext; + use crate::mock::host::HostType; use crate::Height; #[test] @@ -127,7 +129,12 @@ mod tests { } let host_chain_height = Height::new(1, 35); - let context = MockContext::new(5, host_chain_height); + let context = MockContext::new( + ChainId::new("mockgaia", 1).unwrap(), + HostType::Mock, + 5, + host_chain_height, + ); let pruning_window = context.host_chain_history_size() as u64; let client_consensus_state_height = 10; diff --git a/modules/src/ics07_tendermint/client_state.rs b/modules/src/ics07_tendermint/client_state.rs index 6bf157c512..7c5bccae20 100644 --- a/modules/src/ics07_tendermint/client_state.rs +++ b/modules/src/ics07_tendermint/client_state.rs @@ -165,6 +165,21 @@ impl From for RawClientState { } } +#[cfg(test)] +pub mod test_util { + // pub fn get_dummy_msg_conn_open_confirm() -> RawMsgConnectionOpenConfirm { + // RawMsgConnectionOpenConfirm { + // connection_id: "srcconnection".to_string(), + // proof_ack: get_dummy_proof(), + // proof_height: Some(Height { + // version_number: 0, + // version_height: 10, + // }), + // signer: get_dummy_account_id_raw(), + // } + // } +} + #[cfg(test)] mod tests { use std::time::Duration; diff --git a/modules/src/ics07_tendermint/header.rs b/modules/src/ics07_tendermint/header.rs index da097bfb3d..04daae1cc5 100644 --- a/modules/src/ics07_tendermint/header.rs +++ b/modules/src/ics07_tendermint/header.rs @@ -4,11 +4,15 @@ use tendermint::block::signed_header::SignedHeader; use tendermint::validator::Set as ValidatorSet; use crate::ics02_client::client_type::ClientType; -use crate::ics07_tendermint::consensus_state::ConsensusState; -use crate::ics23_commitment::commitment::CommitmentRoot; use crate::ics24_host::identifier::ChainId; use crate::Height; +#[cfg(test)] +use { + crate::ics07_tendermint::consensus_state::ConsensusState, + crate::ics23_commitment::commitment::CommitmentRoot, +}; + /// Tendermint consensus header #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Header { @@ -18,6 +22,7 @@ pub struct Header { pub trusted_validator_set: ValidatorSet, // the last trusted validator set at trusted height } +#[cfg(test)] impl Header { pub(crate) fn consensus_state(&self) -> ConsensusState { ConsensusState { diff --git a/modules/src/ics18_relayer/mod.rs b/modules/src/ics18_relayer/mod.rs index 9f502c98f1..71e8e42ed9 100644 --- a/modules/src/ics18_relayer/mod.rs +++ b/modules/src/ics18_relayer/mod.rs @@ -3,5 +3,4 @@ pub mod context; pub mod error; -#[cfg(test)] -pub mod utils; // Currently only used in tests. +pub mod utils; diff --git a/modules/src/ics18_relayer/utils.rs b/modules/src/ics18_relayer/utils.rs index 378c0dc1b5..6826f27960 100644 --- a/modules/src/ics18_relayer/utils.rs +++ b/modules/src/ics18_relayer/utils.rs @@ -1,11 +1,13 @@ use crate::ics02_client::client_def::AnyHeader; use crate::ics02_client::header::Header; use crate::ics02_client::msgs::{ClientMsg, MsgUpdateAnyClient}; -use crate::ics03_connection::msgs::test_util::get_dummy_account_id; use crate::ics18_relayer::context::ICS18Context; use crate::ics18_relayer::error::{Error, Kind}; use crate::ics24_host::identifier::ClientId; +use std::str::FromStr; +use tendermint::account::Id as AccountId; + /// Creates a `ClientMsg::UpdateClient` for a client with id `client_id` running on the `dest` /// context, assuming that the latest header on the source context is `src_header`. pub fn create_client_update_datagram( @@ -43,10 +45,11 @@ where }; // Client on destination chain can be updated. + // TODO: signer hardcode fix. Ok(ClientMsg::UpdateClient(MsgUpdateAnyClient { client_id: client_id.clone(), header: src_header, - signer: get_dummy_account_id(), + signer: AccountId::from_str("0CDA3F47EF3C4906693B170EF650EB968C5F4B2C").unwrap(), })) } @@ -54,10 +57,11 @@ where mod tests { use crate::ics18_relayer::context::ICS18Context; use crate::ics18_relayer::utils::create_client_update_datagram; - use crate::ics24_host::identifier::ClientId; + use crate::ics24_host::identifier::{ChainId, ClientId}; use crate::ics26_routing::msgs::ICS26Envelope; - use crate::mock_context::MockContext; + use crate::mock::context::MockContext; + use crate::mock::host::HostType; use crate::Height; use std::str::FromStr; @@ -76,10 +80,20 @@ mod tests { let client_on_b_for_a = ClientId::from_str("ibczeroclient").unwrap(); // Create two mock contexts, one for each chain. - let mut ctx_a = MockContext::new(5, chain_a_start_height) - .with_client(&client_on_a_for_b, client_on_a_for_b_height); - let mut ctx_b = MockContext::new(5, chain_b_start_height) - .with_client(&client_on_b_for_a, client_on_b_for_a_height); + let mut ctx_a = MockContext::new( + ChainId::new("mockgaia", 1).unwrap(), + HostType::Mock, + 5, + chain_a_start_height, + ) + .with_client(&client_on_a_for_b, client_on_a_for_b_height); + let mut ctx_b = MockContext::new( + ChainId::new("mockgaia", 1).unwrap(), + HostType::Mock, + 5, + chain_b_start_height, + ) + .with_client(&client_on_b_for_a, client_on_b_for_a_height); for _i in 0..num_iterations { // Update client on chain B to latest height of A. diff --git a/modules/src/ics26_routing/handler.rs b/modules/src/ics26_routing/handler.rs index 9ab855a53b..6d45b9d6d0 100644 --- a/modules/src/ics26_routing/handler.rs +++ b/modules/src/ics26_routing/handler.rs @@ -73,9 +73,9 @@ mod tests { use crate::ics24_host::identifier::ClientId; use crate::ics26_routing::handler::dispatch; use crate::ics26_routing::msgs::ICS26Envelope; + use crate::mock::context::MockContext; use crate::mock_client::header::MockHeader; use crate::mock_client::state::{MockClientState, MockConsensusState}; - use crate::mock_context::MockContext; use crate::Height; use std::convert::TryFrom; diff --git a/modules/src/lib.rs b/modules/src/lib.rs index 66686d38cd..5b0fa909e6 100644 --- a/modules/src/lib.rs +++ b/modules/src/lib.rs @@ -9,7 +9,6 @@ unused_qualifications, rust_2018_idioms )] -#![allow(dead_code)] //! Implementation of the following ICS modules: //! @@ -39,7 +38,7 @@ pub mod macros; pub mod proofs; pub mod tx_msg; -#[cfg(test)] +#[cfg(any(test, feature = "mocks"))] pub mod mock_client; /// Re-export of ICS 002 Height domain type @@ -48,5 +47,5 @@ pub type Height = crate::ics02_client::height::Height; #[cfg(test)] mod test; -#[cfg(test)] -mod mock_context; // Context mock: for testing all handlers. +#[cfg(any(test, feature = "mocks"))] +pub mod mock; // Context mock: for testing all handlers. diff --git a/modules/src/mock_context.rs b/modules/src/mock/context.rs similarity index 77% rename from modules/src/mock_context.rs rename to modules/src/mock/context.rs index 551e407eca..7dee04db05 100644 --- a/modules/src/mock_context.rs +++ b/modules/src/mock/context.rs @@ -1,6 +1,5 @@ //! Implementation of a global context mock. Used in testing handlers of all IBC modules. -use crate::ics02_client; use crate::ics02_client::client_def::{AnyClientState, AnyConsensusState, AnyHeader}; use crate::ics02_client::client_type::ClientType; use crate::ics02_client::context::{ClientKeeper, ClientReader}; @@ -12,7 +11,7 @@ use crate::ics03_connection::error::Error as ICS3Error; use crate::ics18_relayer::context::ICS18Context; use crate::ics18_relayer::error::{Error as ICS18Error, Kind as ICS18ErrorKind}; use crate::ics23_commitment::commitment::CommitmentPrefix; -use crate::ics24_host::identifier::{ClientId, ConnectionId}; +use crate::ics24_host::identifier::{ChainId, ClientId, ConnectionId}; use crate::ics26_routing::context::ICS26Context; use crate::ics26_routing::handler::dispatch; use crate::ics26_routing::msgs::ICS26Envelope; @@ -20,20 +19,29 @@ use crate::mock_client::header::MockHeader; use crate::mock_client::state::{MockClientRecord, MockClientState, MockConsensusState}; use crate::Height; +use crate::mock::host::{HostBlock, HostType}; use std::cmp::min; use std::collections::HashMap; use std::error::Error; +/// This context implements the dependencies necessary for testing any IBC module. #[derive(Clone, Debug)] pub struct MockContext { - /// Maximum size of the history. + /// The type of host chain underlying this mock context. + host_chain_type: HostType, + + /// Host chain identifier. + host_chain_id: ChainId, + + /// Maximum size for the history of the host chain. Any block older than this is pruned. max_history_size: usize, - /// Highest height of the headers in the history. + /// Highest height (i.e., most recent) of the headers in the history. latest_height: Height, - /// A list of `max_history_size` headers, ascending order by their height (latest is last). - history: Vec, + /// 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, /// The set of all clients, indexed by their id. clients: HashMap, @@ -50,7 +58,12 @@ pub struct MockContext { /// creation of new domain objects. impl Default for MockContext { fn default() -> Self { - Self::new(5, Height::new(1, 5)) + Self::new( + ChainId::new("mockgaia", 1).unwrap(), + HostType::Mock, + 5, + Height::new(1, 5), + ) } } @@ -59,22 +72,42 @@ impl Default for MockContext { impl MockContext { /// Creates a mock context. Parameter `max_history_size` determines how many headers will /// the chain maintain in its history, which also determines the pruning window. Parameter - /// `latest_height` determines the current height of the chain. - pub fn new(max_history_size: usize, latest_height: Height) -> Self { + /// `latest_height` determines the current height of the chain. This context + /// has support to emulate two type of underlying chains: + pub fn new( + host_id: ChainId, + host_type: HostType, + max_history_size: usize, + latest_height: Height, + ) -> Self { // Compute the number of headers to store. If h is 0, nothing is stored. let n = min(max_history_size as u64, latest_height.version_height); + assert_eq!( + ChainId::chain_version(host_id.to_string()), + latest_height.version_number, + "The version in the chain identifier does not match the version in the latest height" + ); + assert_ne!( max_history_size, 0, "The chain must have a non-zero max_history_size" ); MockContext { + host_chain_type: host_type, + host_chain_id: host_id.clone(), max_history_size, latest_height, history: (0..n) .rev() - .map(|i| MockHeader(latest_height.sub(i).unwrap())) + .map(|i| { + HostBlock::generate_block( + host_id.clone(), + host_type, + latest_height.sub(i).unwrap().version_height, + ) + }) .collect(), connections: Default::default(), clients: Default::default(), @@ -131,9 +164,9 @@ impl MockContext { } } - /// Accessor for a header of the local (host) chain of this context. - /// May return `None` if the header for the requested height does not exist. - fn host_header(&self, target_height: Height) -> Option { + /// 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> { let target = target_height.version_height as usize; let latest = self.latest_height.version_height as usize; @@ -141,24 +174,28 @@ impl MockContext { if (target > latest) || (target <= latest - self.history.len()) { None // Header for requested height does not exist in history. } else { - Some(self.history[self.history.len() + target - latest - 1]) + Some(&self.history[self.history.len() + target - latest - 1]) } } /// Triggers the advancing of the host chain, by extending the history of blocks (headers). pub fn advance_host_chain_height(&mut self) { - let new_header = MockHeader(self.latest_height.increment()); + let new_block = HostBlock::generate_block( + self.host_chain_id.clone(), + self.host_chain_type, + self.latest_height.increment().version_height, + ); // Append the new header at the tip of the history. if self.history.len() >= self.max_history_size { // History is full, we rotate and replace the tip with the new header. self.history.rotate_left(1); - self.history[self.max_history_size - 1] = new_header; + self.history[self.max_history_size - 1] = new_block; } else { // History is not full yet. - self.history.push(new_header); + self.history.push(new_block); } - self.latest_height = new_header.height(); + self.latest_height = self.latest_height.increment(); } /// A datagram passes from the relayer to the IBC module (on host chain). @@ -180,7 +217,7 @@ impl MockContext { // Check the content of the history. if !self.history.is_empty() { // Get the highest header. - let lh = self.history[self.history.len() - 1]; + let lh = &self.history[self.history.len() - 1]; // Check latest is properly updated with highest header height. if lh.height() != self.latest_height { return Err("latest height is not updated".to_string().into()); @@ -189,8 +226,8 @@ impl MockContext { // Check that headers in the history are in sequential order. for i in 1..self.history.len() { - let ph = self.history[i - 1]; - let h = self.history[i]; + let ph = &self.history[i - 1]; + let h = &self.history[i]; if ph.height().increment() != h.height() { return Err("headers in history not sequential".to_string().into()); } @@ -208,7 +245,7 @@ impl ConnectionReader for MockContext { fn client_state(&self, client_id: &ClientId) -> Option { // Forward method call to the ICS2 Client-specific method. - ics02_client::context::ClientReader::client_state(self, client_id) + ClientReader::client_state(self, client_id) } fn host_current_height(&self) -> Height { @@ -234,8 +271,8 @@ impl ConnectionReader for MockContext { } fn host_consensus_state(&self, height: Height) -> Option { - let hi = self.host_header(height)?; - Some(hi.into()) + let block_ref = self.host_block(height); + block_ref.cloned().map(Into::into) } fn get_compatible_versions(&self) -> Vec { @@ -363,12 +400,12 @@ impl ICS18Context for MockContext { fn query_client_full_state(&self, client_id: &ClientId) -> Option { // Forward call to ICS2. - ics02_client::context::ClientReader::client_state(self, client_id) + ClientReader::client_state(self, client_id) } fn query_latest_header(&self) -> Option { - let hi = self.host_header(self.host_current_height())?; - Some(hi.into()) + let block_ref = self.host_block(self.host_current_height()); + block_ref.cloned().map(Into::into) } fn send(&mut self, msg: ICS26Envelope) -> Result<(), ICS18Error> { @@ -378,7 +415,9 @@ impl ICS18Context for MockContext { #[cfg(test)] mod tests { - use crate::mock_context::MockContext; + use crate::ics24_host::identifier::ChainId; + use crate::mock::context::MockContext; + use crate::mock::host::HostType; use crate::Height; #[test] @@ -392,23 +431,48 @@ mod tests { let tests: Vec = vec![ Test { name: "Empty history, small pruning window".to_string(), - ctx: MockContext::new(1, Height::new(cv, 0)), + ctx: MockContext::new( + ChainId::new("mockgaia", cv).unwrap(), + HostType::Mock, + 1, + Height::new(cv, 0), + ), }, Test { name: "Large pruning window".to_string(), - ctx: MockContext::new(30, Height::new(cv, 2)), + ctx: MockContext::new( + ChainId::new("mockgaia", cv).unwrap(), + HostType::Mock, + 30, + Height::new(cv, 2), + ), }, Test { name: "Small pruning window".to_string(), - ctx: MockContext::new(3, Height::new(cv, 30)), + ctx: MockContext::new( + ChainId::new("mockgaia", cv).unwrap(), + HostType::Mock, + 3, + Height::new(cv, 30), + ), }, Test { name: "Small pruning window, small starting height".to_string(), - ctx: MockContext::new(3, Height::new(cv, 2)), + ctx: MockContext::new( + ChainId::new("mockgaia", cv).unwrap(), + HostType::Mock, + 3, + Height::new(cv, 2), + ), }, Test { name: "Large pruning window, large starting height".to_string(), - ctx: MockContext::new(50, Height::new(cv, 2000)), + ctx: MockContext::new( + ChainId::new("mockgaia", cv).unwrap(), + HostType::Mock, + 50, + Height::new(cv, 2000), + ), }, ]; @@ -416,7 +480,8 @@ mod tests { // All tests should yield a valid context after initialization. assert!( test.ctx.validate().is_ok(), - "Failed while validating context {:?}", + "Failed in test {} while validating context {:?}", + test.name, test.ctx ); @@ -426,7 +491,8 @@ mod tests { test.ctx.advance_host_chain_height(); assert!( test.ctx.validate().is_ok(), - "Failed while validating context {:?}", + "Failed in test {} while validating context {:?}", + test.name, test.ctx ); @@ -438,7 +504,7 @@ mod tests { ); if current_height > Height::new(cv, 0) { assert_eq!( - test.ctx.host_header(current_height).unwrap().height(), + test.ctx.host_block(current_height).unwrap().height(), current_height, "Failed while fetching height {:?} of context {:?}", current_height, diff --git a/modules/src/mock/host.rs b/modules/src/mock/host.rs new file mode 100644 index 0000000000..9378907862 --- /dev/null +++ b/modules/src/mock/host.rs @@ -0,0 +1,96 @@ +//! Host chain types and methods, used by context mock. + +use crate::ics02_client::client_def::{AnyConsensusState, AnyHeader}; +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_client::header::MockHeader; +use crate::Height; + +use tendermint::chain::Id as TMChainId; +use tendermint_testgen::light_block::TMLightBlock; +use tendermint_testgen::{Generator, LightBlock as TestgenLightBlock}; + +use std::convert::TryFrom; + +/// Defines the different types of host chains that a mock context can emulate, as follows: +/// - `Mock` defines that the context history comprises `MockHeader` blocks. +/// - `SyntheticTendermint`: the context has synthetically-generated Tendermint (light) blocks. +#[derive(Clone, Debug, Copy)] +pub enum HostType { + Mock, + SyntheticTendermint, +} + +/// Depending on `HostType` (the type of host chain underlying a context mock), this enum defines +/// the blocks that compose the history of the host chain. +#[derive(Clone, Debug)] +pub enum HostBlock { + Mock(MockHeader), + SyntheticTendermint(Box), +} + +impl HostBlock { + /// Returns the height of a block. + pub fn height(&self) -> Height { + match self { + HostBlock::Mock(header) => header.height(), + HostBlock::SyntheticTendermint(light_block) => Height::new( + ChainId::chain_version(light_block.signed_header.header.chain_id.to_string()), + light_block.signed_header.header.height.value(), + ), + } + } + + /// 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 { + match chain_type { + HostType::Mock => HostBlock::Mock(MockHeader(Height::new( + ChainId::chain_version(chain_id.to_string()), + height, + ))), + HostType::SyntheticTendermint => { + let mut block = TestgenLightBlock::new_default(height).generate().unwrap(); + block.signed_header.header.chain_id = + TMChainId::try_from(chain_id.to_string()).unwrap(); + HostBlock::SyntheticTendermint(Box::new(block)) + } + } + } +} + +impl From for AnyConsensusState { + fn from(any_block: HostBlock) -> Self { + match any_block { + HostBlock::Mock(mock_header) => mock_header.into(), + HostBlock::SyntheticTendermint(light_block) => { + let cs = TMConsensusState::from(light_block.signed_header); + AnyConsensusState::Tendermint(cs) + } + } + } +} + +impl From for AnyHeader { + fn from(any_block: HostBlock) -> Self { + match any_block { + HostBlock::Mock(mock_header) => mock_header.into(), + HostBlock::SyntheticTendermint(light_block_box) => { + // Conversion between TMLightBlock and AnyHeader + AnyHeader::Tendermint((*light_block_box).into()) + } + } + } +} + +impl From for TMHeader { + fn from(light_block: TMLightBlock) -> Self { + // TODO: This conversion is incorrect for `trusted_height` and `trusted_validator_set`. + TMHeader { + signed_header: light_block.signed_header, + validator_set: light_block.validators, + trusted_height: Default::default(), + trusted_validator_set: light_block.next_validators, + } + } +} diff --git a/modules/src/mock/mod.rs b/modules/src/mock/mod.rs new file mode 100644 index 0000000000..4613992b15 --- /dev/null +++ b/modules/src/mock/mod.rs @@ -0,0 +1,4 @@ +//! Implementation of mocks for context and host chain. + +pub mod context; +pub mod host; diff --git a/modules/src/mock_client/state.rs b/modules/src/mock_client/state.rs index f50328dee7..d6bdd07a49 100644 --- a/modules/src/mock_client/state.rs +++ b/modules/src/mock_client/state.rs @@ -48,7 +48,6 @@ impl MockClientState { header: AnyHeader, ) -> Result<(MockClientState, MockConsensusState), Box> { match header { - #[cfg(test)] AnyHeader::Mock(mock_header) => { if self.latest_height() >= header.height() { return Err("header height is lower than client latest".into()); @@ -140,7 +139,6 @@ impl From for RawMockConsensusState { } } -#[cfg(test)] impl From for AnyConsensusState { fn from(mcs: MockConsensusState) -> Self { Self::Mock(mcs) diff --git a/modules/tests/support/signed_header.json b/modules/tests/support/signed_header.json index cff5688dc9..a681f3a73d 100644 --- a/modules/tests/support/signed_header.json +++ b/modules/tests/support/signed_header.json @@ -4,7 +4,7 @@ "round": 0, "block_id": { "hash": "760E050B2404A4BC661635CA552FF45876BCD927C367ADF88961E389C01D32FF", - "parts": { + "part_set_header": { "total": 1, "hash": "485070D01F9543827B3F9BAF11BDCFFBFD2BDED0B63D7192FA55649B94A1D5DE" } @@ -30,7 +30,7 @@ "total_txs": "4", "last_block_id": { "hash": "42C70F10EF1835CED7248114514B4EF3D06F0D7FD24F6486E3315DEE310D305C", - "parts": { + "part_set_header": { "total": 1, "hash": "F51D1B8E6ED859CE23F6B0539E0101653ED4025B13DAA3E76FCC779D5FD96ABE" } diff --git a/relayer-cli/Cargo.toml b/relayer-cli/Cargo.toml index 6158332ab8..f9e542a39e 100644 --- a/relayer-cli/Cargo.toml +++ b/relayer-cli/Cargo.toml @@ -28,13 +28,16 @@ prost-types = { version = "0.6.1" } [dependencies.tendermint] version = "0.17.0-rc1" +git = "https://github.com/informalsystems/tendermint-rs" [dependencies.tendermint-rpc] version = "0.17.0-rc1" features = ["http-client", "websocket-client"] +git = "https://github.com/informalsystems/tendermint-rs" [dependencies.tendermint-light-client] version = "0.17.0-rc1" +git = "https://github.com/informalsystems/tendermint-rs" [dependencies.abscissa_core] version = "0.5.2" diff --git a/relayer/Cargo.toml b/relayer/Cargo.toml index d98e7e88b3..7434eefd93 100644 --- a/relayer/Cargo.toml +++ b/relayer/Cargo.toml @@ -31,13 +31,16 @@ crossbeam-channel = "0.5.0" [dependencies.tendermint] version = "0.17.0-rc1" +git = "https://github.com/informalsystems/tendermint-rs" [dependencies.tendermint-rpc] version = "0.17.0-rc1" features = ["http-client", "websocket-client"] +git = "https://github.com/informalsystems/tendermint-rs" [dependencies.tendermint-light-client] version = "0.17.0-rc1" +git = "https://github.com/informalsystems/tendermint-rs" # Needed for tx sign when ready in tendermint upgrade #k256 = { version = "0.4", features = ["ecdsa-core", "ecdsa", "sha256"]} @@ -47,4 +50,6 @@ version = "0.17.0-rc1" #ripemd160 = { version = "0.9", optional = true } #sha2 = { version = "0.9.1", default-features = false } -[dev-dependencies] +[dev-dependencies.ibc] +path = "../modules" +features = [ "mocks" ] \ No newline at end of file diff --git a/relayer/src/chain.rs b/relayer/src/chain.rs index 1dba7096df..78736c1957 100644 --- a/relayer/src/chain.rs +++ b/relayer/src/chain.rs @@ -19,6 +19,10 @@ use crate::error; use std::error::Error; pub(crate) mod cosmos; + +#[cfg(test)] +pub(crate) mod local; // Local chain, used for relayer integration testing against IBC modules. + pub use cosmos::CosmosSDKChain; pub mod handle; diff --git a/relayer/src/chain/local.rs b/relayer/src/chain/local.rs new file mode 100644 index 0000000000..15ea66dddf --- /dev/null +++ b/relayer/src/chain/local.rs @@ -0,0 +1,171 @@ +use crate::error::{Error, Kind}; +use ibc::handler::HandlerOutput; +use ibc::ics02_client::client_def::{AnyClientState, AnyConsensusState, AnyHeader}; +use ibc::ics02_client::client_type::ClientType; +use ibc::ics02_client::context::{ClientKeeper, ClientReader}; +use ibc::ics02_client::error::Error as ICS02Error; +use ibc::ics02_client::msgs::{ClientMsg, MsgCreateAnyClient}; +use ibc::ics03_connection::connection::ConnectionEnd; +use ibc::ics03_connection::context::{ConnectionKeeper, ConnectionReader}; +use ibc::ics03_connection::error::Error as ICS03Error; +use ibc::ics18_relayer::context::ICS18Context; +use ibc::ics18_relayer::error::Error as ICS18Error; +use ibc::ics18_relayer::error::Kind as ICS18Kind; +use ibc::ics23_commitment::commitment::CommitmentPrefix; +use ibc::ics24_host::identifier::{ClientId, ConnectionId}; +use ibc::ics26_routing::context::ICS26Context; +use ibc::ics26_routing::msgs::ICS26Envelope; +use ibc::Height; + +use ibc::mock::context::MockContext; + +use ibc::ics26_routing::handler::dispatch; +use std::str::FromStr; +use tendermint::account::Id as AccountId; +use tendermint_light_client::types::LightBlock; + +/// A Tendermint chain locally running in-process with the relayer. Wraps over a `MockContext`, +/// which does most of the heavy lifting of implementing IBC dependencies. +pub struct LocalChain { + config: LocalChainConfig, + context: MockContext, +} + +/// Internal interface, for writing relayer tests. +impl LocalChain { + pub fn from_config(config: LocalChainConfig) -> Result { + Ok(Self { + config, + context: MockContext::default(), + }) + } + + /// Submits an IBC message for creating an IBC client on the chain. It is assumed that this is + /// a client for a mock chain. + pub fn create_client(&mut self, client_id: &ClientId) -> Result<(), Error> { + let client_message = ClientMsg::CreateClient( + MsgCreateAnyClient::new( + client_id.clone(), + todo!(), + todo!(), + AccountId::from_str("0CDA3F47EF3C4906693B170EF650EB968C5F4B2C").unwrap(), + ) + .unwrap(), + ); + + self.send(ICS26Envelope::ICS2Msg(client_message)) + .map_err(|_| { + Kind::CreateClient(client_id.clone(), "tx submission failed".into()).into() + }) + } +} + +/// The relayer-facing interface. +impl ICS18Context for LocalChain { + fn query_latest_height(&self) -> Height { + todo!() + } + + fn query_client_full_state(&self, client_id: &ClientId) -> Option { + unimplemented!() + } + + fn query_latest_header(&self) -> Option { + unimplemented!() + } + + fn send(&mut self, msg: ICS26Envelope) -> Result<(), ICS18Error> { + // Forward the datagram directly into ICS26 routing handler. + // dispatch(self.context, msg).map_err(|e| ICS18Kind::TransactionFailed.context(e))?; + Ok(()) + } +} + +#[cfg(test)] // TODO: This module is already configured for `tests`. +mod tests { + use crate::chain::local::LocalChain; + use crate::config::LocalChainConfig; + use ibc::ics18_relayer::context::ICS18Context; + use ibc::ics18_relayer::utils::create_client_update_datagram; + use ibc::ics24_host::identifier::ClientId; + use ibc::ics26_routing::msgs::ICS26Envelope; + use ibc::Height; + use std::str::FromStr; + use tendermint::chain::Id as ChainId; + + #[test] + fn create_local_chain_and_client() { + let _client_id = ClientId::from_str("clientonlocalchain").unwrap(); + let cfg = LocalChainConfig { + id: ChainId::from_str("not-gaia").unwrap(), + client_ids: vec![String::from("client_one"), String::from("client_two")], + }; + + let c = LocalChain::from_config(cfg); + assert!(c.is_ok()); + + let mut _chain = c.unwrap(); + // assert!(chain.create_client(&client_id).is_ok()); + } + + #[test] + // Simple test for the advance chain function. + fn chain_advance() { + let update_count = 4; // Number of advance chain iterations. + let cfg = LocalChainConfig { + id: ChainId::from_str("chain-a").unwrap(), + client_ids: vec![String::from("client_on_a_for_b")], + }; + let chain_res = LocalChain::from_config(cfg); + assert!(chain_res.is_ok()); + // let mut chain = chain_res.unwrap(); + + // let mut current_height = chain.query_latest_height(); + // + // for _i in 0..update_count { + // chain.advance(); + // let new_height = chain.query_latest_height(); + // assert_eq!( + // new_height, + // current_height.increment(), + // "advance(): fails to increase the latest height" + // ); + // + // current_height = new_height; + // } + } + + #[test] + /// Tests the relayer `create_client_update_datagram` of ICS18 against two generated + /// Tendermint chains (see `testgen` crate). + /// Note: This is a more realistic version of test `client_update_ping_pong` of ICS18. + fn tm_client_update_ping_pong() { + let _update_count = 4; // Number of ping-pong (client update) iterations. + let client_on_a_for_b = ClientId::from_str("client_on_a_for_b").unwrap(); + let client_on_b_for_a = ClientId::from_str("client_on_b_for_a").unwrap(); + + let _cfg_a = LocalChainConfig { + id: ChainId::from_str("chain-a").unwrap(), + client_ids: vec![client_on_a_for_b.to_string()], + }; + let _cfg_b = LocalChainConfig { + id: ChainId::from_str("chain-b").unwrap(), + client_ids: vec![client_on_b_for_a.to_string()], + }; + + // let chain_a = LocalChain::from_config(cfg_a).unwrap(); + // let mut chain_b = LocalChain::from_config(cfg_b).unwrap(); + + // for _i in 0..update_count { + // // Figure out if we need to create a ClientUpdate datagram for client of A on chain B. + // let a_latest_header = chain_a.query_latest_header().unwrap(); + // let client_msg_b_res = + // create_client_update_datagram(&chain_b, &client_on_b_for_a, a_latest_header); + // assert!(client_msg_b_res.is_ok()); + // + // let client_msg_b = client_msg_b_res.unwrap(); + // // Submit the datagram to chain B. + // let dispatch_res_b = chain_b.send(ICS26Envelope::ICS2Msg(client_msg_b)); + // } + } +}