diff --git a/.changelog/unreleased/improvements/1044-introduce-host-trait.md b/.changelog/unreleased/improvements/1044-introduce-host-trait.md new file mode 100644 index 000000000..d8b95983e --- /dev/null +++ b/.changelog/unreleased/improvements/1044-introduce-host-trait.md @@ -0,0 +1,3 @@ +- [ibc-testkit] Replace `HostBlock` and `HostType` enums with a `Host` trait to + eliminate manual delegations by utilizing monomorphization. + ([\#1044](https://github.com/cosmos/ibc-rs/issues/1044)) diff --git a/Cargo.toml b/Cargo.toml index 184204497..c879eca85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,7 @@ ibc-core-channel = { version = "0.52.0", path = "./ibc-core/ics04-channel", d ibc-core-host = { version = "0.52.0", path = "./ibc-core/ics24-host", default-features = false } ibc-core-handler = { version = "0.52.0", path = "./ibc-core/ics25-handler", default-features = false } ibc-core-router = { version = "0.52.0", path = "./ibc-core/ics26-routing", default-features = false } +ibc-query = { version = "0.52.0", path = "./ibc-query", default-features = false } ibc-client-cw = { version = "0.52.0", path = "./ibc-clients/cw-context", default-features = false } ibc-client-tendermint = { version = "0.52.0", path = "./ibc-clients/ics07-tendermint", default-features = false } diff --git a/docs/architecture/adr-005-handlers-redesign.md b/docs/architecture/adr-005-handlers-redesign.md index 855a2b668..e808f2892 100644 --- a/docs/architecture/adr-005-handlers-redesign.md +++ b/docs/architecture/adr-005-handlers-redesign.md @@ -421,7 +421,6 @@ trait ExecutionContext { /// Called upon client creation. /// Increases the counter which keeps track of how many clients have been created. - /// Should never fail. fn increase_client_counter(&mut self); /// Called upon successful client update. @@ -452,7 +451,6 @@ trait ExecutionContext { /// Called upon connection identifier creation (Init or Try process). /// Increases the counter which keeps track of how many connections have been created. - /// Should never fail. fn increase_connection_counter(&mut self); fn store_packet_commitment( @@ -514,7 +512,6 @@ trait ExecutionContext { /// Called upon channel identifier creation (Init or Try message processing). /// Increases the counter which keeps track of how many channels have been created. - /// Should never fail. fn increase_channel_counter(&mut self); /// Ibc events diff --git a/docs/architecture/adr-009-revamp-testkit.md b/docs/architecture/adr-009-revamp-testkit.md new file mode 100644 index 000000000..8bf5af3e2 --- /dev/null +++ b/docs/architecture/adr-009-revamp-testkit.md @@ -0,0 +1,349 @@ +# ADR 009: Revamp IBC integration test framework + +## Changelog + +- 04-03-2024: Initial draft + +## Context + +The current framework in the IBC testkit uses +[existing types and injects state and/or dependencies +manually](https://github.com/cosmos/ibc-rs/blob/v0.51.0/ibc-testkit/tests/core/ics02_client/update_client.rs#L574-L578). +Sometimes, it uses +[semantically incorrect data as mock data](https://github.com/cosmos/ibc-rs/blob/v0.51.0/ibc-testkit/src/testapp/ibc/core/types.rs#L320). +Because of this, tests with customizable steps and fixed fixtures became ad-hoc +and unmaintainable. + +To overcome this, we need to improve our test framework such that it allows: + +- testing different implementations and traits. +- composition of succinct tests with useful `util` methods. +- validating code paths that were not easily testable before, i.e. Merkle proof + generation. +- simulating a more realistic IBC workflow, using real IBC and relayer + interfaces. + +## Decision + +The main goal of this proposal is to create a test framework that is modular and +closer to a real blockchain environment. This should also make the existing +tests more succinct and readable. Instead of bootstrapping the mock data that +tests use, we should use valid steps to generate it - so that we know the exact +steps to reach a state to reproduce in a real environment. + +To achieve this, we have broken down the proposal into sub-proposals: + +### 1. Adopt a Merkle store for the test framework + +The current framework uses `HashMap` and `HashSet` to store data. This works for +many test scenarios, but it fails to test proof-sensitive scenarios. Because of +this, we don't have any connection, channel handshake, or packet relay tests +that cover the Tendermint light client. + +We generalize +[`MockContext`](https://github.com/cosmos/ibc-rs/blob/v0.51.0/ibc-testkit/src/testapp/ibc/core/types.rs#L103) +to use a Merkle store which is used for IBC Context's store. For concrete or +default implementations, we can use the IAVL Merkle store implementation from +`informalsystems/basecoin-rs`. + +### 2. Modularize the host environment + +Currently, we are using `Mock` and `SyntheticTendermint` variants of the +[`HostType`](https://github.com/cosmos/ibc-rs/blob/v0.51.0/ibc-testkit/src/hosts/block.rs#L33-L36) +enum as host environments. To manage these two different environments, we also +introduced +[`HostBlocks`](https://github.com/cosmos/ibc-rs/blob/v0.51.0/ibc-testkit/src/hosts/block.rs#L72-75) +for encapsulating the possible host variants that need to be covered by +`ibc-testkit`. + +However, this creates friction in the case when we need to add new host +variants. It creates the same problem that the `ibc-derive` crate was designed +to solve for `ClientState` and `ConsensusState` types, namely: dispatching +methods to underlying variants of a top-level enum. But, a concrete +`MockContext` will always have a unique `HostType` and its corresponding +`HostBlocks`. So we can refactor `HostTypes` and `HockBlocks` with a generic +`TestHost` trait that maintains its corresponding types e.g. `Block` types, via +associated types. Finally, we generalize the `MockContext` once more to use this +`TestHost` trait instead of a concrete enum variant. + +This `TestHost` trait should be responsible for generating blocks, headers, +client, and consensus states specific to that host environment. + +```rs +/// TestHost is a trait that defines the interface for a host blockchain. +pub trait TestHost: Default + Debug + Sized { + /// The type of block produced by the host. + type Block: TestBlock; + + /// The type of client state produced by the host. + type ClientState: Into + Debug; + + /// The type of block parameters to produce a block. + type BlockParams: Debug + Default; + + /// The type of light client parameters to produce a light client state. + type LightClientParams: Debug + Default; + + /// The history of blocks produced by the host chain. + fn history(&self) -> &Vec; + + /// Commit a block with commitment root to the blockchain, by extending the history of blocks. + fn commit_block( + &mut self, + commitment_root: Vec, + block_time: Duration, + params: &Self::BlockParams, + ); + + /// Generate a block at the given height and timestamp, using the provided parameters. + fn generate_block( + &self, + commitment_root: Vec, + height: u64, + timestamp: Timestamp, + params: &Self::BlockParams, + ) -> Self::Block; + + /// Generate a client state using the block at the given height and the provided parameters. + fn generate_client_state( + &self, + latest_height: &Height, + params: &Self::LightClientParams, + ) -> Self::ClientState; +} + +/// TestBlock is a trait that defines the interface for a block produced by a host blockchain. +pub trait TestBlock: Clone + Debug { + /// The type of header that can be extracted from the block. + type Header: TestHeader; + + /// The height of the block. + fn height(&self) -> Height; + + /// The timestamp of the block. + fn timestamp(&self) -> Timestamp; + + /// Extract the header from the block. + fn into_header(self) -> Self::Header; +} + +/// TestHeader is a trait that defines the interface for a header +/// submitted by relayer from the host blockchain. +pub trait TestHeader: Clone + Debug + Into { + /// The type of consensus state can be extracted from the header. + type ConsensusState: ConsensusState + Into + From + Clone + Debug; + + /// The height of the block, as recorded in the header. + fn height(&self) -> Height; + + /// The timestamp of the block, as recorded in the header. + fn timestamp(&self) -> Timestamp; + + /// Extract the consensus state from the header. + fn into_consensus_state(self) -> Self::ConsensusState; +} +``` + +### 3. Decoupling IbcContext and Host environment + +Currently, `MockContext` implements the top-level +[validation](https://github.com/cosmos/ibc-rs/blob/v0.51.0/ibc-core/ics25-handler/src/entrypoint.rs#L45) +and +[execution](https://github.com/cosmos/ibc-rs/blob/v0.51.0/ibc-core/ics25-handler/src/entrypoint.rs#L112) +contexts of `ibc-rs`, as opposed to the more granular contexts of each of the +individual handlers. It contains other host-specific data e.g. `host_chain_id`, +`block_time` - that are not directly relevant to the IBC context. If we think of +`MockContext` as a real blockchain context, the `MockContext` represents the +top-level runtime; it contains `MockIbcStore`, which is a more appropriate +candidate to implement the validation and execution contexts for than the +`MockContext` itself. + +With this, the `MockContext` contains two decoupled parts - the host and the IBC +module. + +### 4. Chain-like interface for `MockContext` + +With the above changes, we can refactor the `MockContext` to have +blockchain-like interfaces. + +The `MockContext` should have `end_block`, `produce_block`, and `begin_block` to +mimic real blockchain environments, such as the Cosmos-SDK. + +```rs +impl MockContext +where + S: ProvableStore + Debug, + H: TestHost, +{ + // Access ibc module store + pub fn ibc_store_mut(&mut self) -> &mut MockIbcStore; + + // Advance the first block height. + pub fn advance_genesis_height(&mut self, genesis_time: Timestamp); + // Routine procedures at the beginning of a block + // Just after committing last block state. + pub fn begin_block(&mut self); + // Advance the current block height. + pub fn advance_block_height(&mut self, block_time: Duration); + // Routine procedures at the end of a block + // Just before committing current block state. + pub fn end_block(&mut self); +} +``` + +### 5. ICS23 compatible proof generation + +With the new proof generation capabilities, we can now test the Tendermint light +clients. But we need our proofs to be ICS23 compatible. ICS23 expects the IBC +store root to be committed at a commitment prefix at a top-level store in the +host environment. + +For this, we add an extra store in `MockContext` where the `MockIbcStore` +commits its storage root at its +[commitment prefix](https://github.com/cosmos/ibc-rs/blob/v0.51.0/ibc-testkit/src/testapp/ibc/core/core_ctx.rs#L127-L129) +key. + +So the `MockContext` is finalized as: + +```rs +pub struct MockContext +where + S: ProvableStore + Debug, + H: TestHost +{ + pub main_store: S, + pub host: H, + pub ibc_store: MockIbcStore, +} +``` + +Now the `MockIbcStore` can generate proofs that contain the proofs in its store +and commitment prefix. But it has to know the proofs of its commitment prefix of +the previous heights. + +So we add an extra store in `MockIbcStore` to store the proofs from previous +heights. This is similar to storing `HostConsensusState`s of previous heights. + +```rs +#[derive(Debug)] +pub struct MockIbcStore +where + S: ProvableStore + Debug, +{ + ... + /// Map of host consensus states. + pub host_consensus_states: Arc>>, + /// Map of proofs of ibc commitment prefix. + pub ibc_commiment_proofs: Arc>>, +} +``` + +The storing of the IBC store root at the IBC commitment prefix happens in the +`end_block` procedure. `produce_block` commits the main store, produces a block +with its latest root, and pushes the block to the blockchain. The storing of +proofs and host consensus states happens in the `begin_block` of the +`MockContext`. + +### 6. Integration Tests via `RelayerContext` + +With all the above changes, we can now write an integration test that tests the +IBC workflow - client creation, connection handshake, channel handshake, and +packet relaying for any host environment that implements `TestHost`. + +This can be done by reading the +[IBC events](https://github.com/cosmos/ibc-rs/blob/v0.51.0/ibc-testkit/src/testapp/ibc/core/types.rs#L95) +from `MockIbcStore` and creating and sending the IBC messages via +[`MockContext::deliver`](https://github.com/cosmos/ibc-rs/blob/v0.51.0/ibc-testkit/src/testapp/ibc/core/types.rs#L696). + +### Miscellaneous + +To achieve blockchain-like interfaces, we removed `max_history_size` and +`host_chain_id` from `MockContext`. + +- `max_history_size`: We generate all the blocks till a block height. This gives + us reproducibility. If we need to prune some older block data, we use a + dedicated `prune_block_till` to prune older blocks. This makes our tests more + descriptive about the assumption of the test scenarios. +- `host_chain_id`: The IBC runtime does not depend on `host_chain_id` directly. + The `TestHost` trait implementation is responsible for generating the blocks + with the necessary data. + +Also to minimize verbosity while writing tests (as Rust doesn't support default +arguments to function parameters), we want to use some parameter builders. For +that, we can use the [`TypedBuilder`](https://crates.io/crates/typed-builder) +crate. + +## Status + +Proposed + +## Consequences + +This ADR pays the technical debt of the existing testing framework. + +### Positive + +Future tests will be more readable and maintainable. The test framework becomes +modular and leverages Rust's trait system. `ibc-rs` users may benefit from this +framework, which allows them to test their host implementations of `ibc-rs` +components. + +### Negative + +This requires a significant refactoring of the existing tests. Since this may +take some time, the parallel development on the `main` branch may conflict with +this work. + +## References + +This work is being tracked at +[cosmos/ibc-rs#1109](https://github.com/cosmos/ibc-rs/pull/1109). + +The following provides the concrete implementations of the proposed changes: + +#### MockIbcStore + +The modified `MockIbcStore` with Merkle store lives at +[`testapp/ibc/core/types.rs`](https://github.com/cosmos/ibc-rs/blob/feat/refactor-testkit/ibc-testkit/src/testapp/ibc/core/types.rs#L43-L96). + +#### TestHost + +The Rust trait lives at +[`hosts/mod.rs`](https://github.com/cosmos/ibc-rs/blob/feat/refactor-testkit/ibc-testkit/src/hosts/mod.rs#L27). +The `Mock` and `Tendermint` host implementations live in +[`hosts/mock.rs`](https://github.com/cosmos/ibc-rs/blob/feat/refactor-testkit/ibc-testkit/src/hosts/mock.rs#L30) +and +[`hosts/tendermint.rs`](https://github.com/cosmos/ibc-rs/blob/feat/refactor-testkit/ibc-testkit/src/hosts/tendermint.rs#L42) +respectively. + +#### Renaming `MockContext` to `StoreGenericTestContext` + +There was confusion about what is a _Test_ component and what is a _Mock_ +component. We have `MockContext` with `MockClient` and `TendermintClient`. + +To avoid this confusion, we renamed `MockContext` to `StoreGenericTestContext`. +This means that going forward all our general frameworks and traits should have +`Test` in their name. But a dummy concrete implementation of these traits may +have `Mock` in their name. + +#### StoreGenericTestContext + +[`StoreGenericTestContext`](https://github.com/cosmos/ibc-rs/blob/feat/refactor-testkit/ibc-testkit/src/context.rs#L34-L52) +is actually what is described as `MockContext` in the ADR. For convenience, we +defined `TestContext` to have a concrete store implementation - +[`MockStore`](https://github.com/cosmos/ibc-rs/blob/feat/refactor-testkit/ibc-testkit/src/context.rs#L55-L56). + +```rs +// A mock store type using basecoin-storage implementations. +pub type MockStore = RevertibleStore>; + +pub type TestContext = StoreGenericTestContext; +``` + +With this, we can now define `MockContext` which uses `MockStore` and `MockHost` +and `TendermintContext` which uses `MockStore` and `TendermintHost`. + +```rs +pub type MockContext = TestContext; +pub type TendermintContext = TestContext; +``` diff --git a/ibc-core/ics24-host/src/context.rs b/ibc-core/ics24-host/src/context.rs index b390d6b83..612768a32 100644 --- a/ibc-core/ics24-host/src/context.rs +++ b/ibc-core/ics24-host/src/context.rs @@ -151,7 +151,6 @@ pub trait ExecutionContext: ValidationContext { /// Called upon client creation. /// Increases the counter which keeps track of how many clients have been created. - /// Should never fail. fn increase_client_counter(&mut self) -> Result<(), ContextError>; /// Stores the given connection_end at path @@ -170,7 +169,6 @@ pub trait ExecutionContext: ValidationContext { /// Called upon connection identifier creation (Init or Try process). /// Increases the counter which keeps track of how many connections have been created. - /// Should never fail. fn increase_connection_counter(&mut self) -> Result<(), ContextError>; /// Stores the given packet commitment at the given store path @@ -233,7 +231,6 @@ pub trait ExecutionContext: ValidationContext { /// Called upon channel identifier creation (Init or Try message processing). /// Increases the counter which keeps track of how many channels have been created. - /// Should never fail. fn increase_channel_counter(&mut self) -> Result<(), ContextError>; /// Emit the given IBC event diff --git a/ibc-testkit/Cargo.toml b/ibc-testkit/Cargo.toml index 3962bbb83..0ab0ed09e 100644 --- a/ibc-testkit/Cargo.toml +++ b/ibc-testkit/Cargo.toml @@ -33,6 +33,10 @@ ibc = { workspace = true, features = [ "std" ] } ibc-proto = { workspace = true } ibc-client-cw = { workspace = true } ibc-client-tendermint-cw = { workspace = true } +ibc-query = { workspace = true } + +# basecoin dependencies +basecoin-store = { git = "https://github.com/informalsystems/basecoin-rs", rev = "8496b3f" } # cosmos dependencies tendermint = { workspace = true } diff --git a/ibc-testkit/src/context.rs b/ibc-testkit/src/context.rs new file mode 100644 index 000000000..74d141f2e --- /dev/null +++ b/ibc-testkit/src/context.rs @@ -0,0 +1,612 @@ +use core::fmt::Debug; +use core::time::Duration; + +use basecoin_store::context::ProvableStore; +use basecoin_store::impls::{GrowingStore, InMemoryStore, RevertibleStore}; +use ibc::core::channel::types::channel::ChannelEnd; +use ibc::core::channel::types::commitment::PacketCommitment; +use ibc::core::client::context::client_state::ClientStateValidation; +use ibc::core::client::context::{ClientExecutionContext, ClientValidationContext}; +use ibc::core::client::types::Height; +use ibc::core::connection::types::ConnectionEnd; +use ibc::core::entrypoint::{dispatch, execute, validate}; +use ibc::core::handler::types::error::ContextError; +use ibc::core::handler::types::events::IbcEvent; +use ibc::core::handler::types::msgs::MsgEnvelope; +use ibc::core::host::types::identifiers::{ChannelId, ClientId, ConnectionId, PortId, Sequence}; +use ibc::core::host::types::path::{ + ChannelEndPath, ClientConsensusStatePath, ClientStatePath, CommitmentPath, ConnectionPath, + SeqAckPath, SeqRecvPath, SeqSendPath, +}; +use ibc::core::host::{ExecutionContext, ValidationContext}; +use ibc::primitives::prelude::*; +use ibc::primitives::Timestamp; + +use super::testapp::ibc::core::types::{LightClientState, MockIbcStore}; +use crate::fixtures::core::context::TestContextConfig; +use crate::hosts::{HostClientState, MockHost, TendermintHost, TestBlock, TestHeader, TestHost}; +use crate::relayer::error::RelayerError; +use crate::testapp::ibc::clients::{AnyClientState, AnyConsensusState}; +use crate::testapp::ibc::core::router::MockRouter; +use crate::testapp::ibc::core::types::DEFAULT_BLOCK_TIME_SECS; + +/// A context implementing the dependencies necessary for testing any IBC module. +#[derive(Debug)] +pub struct StoreGenericTestContext +where + S: ProvableStore + Debug, + H: TestHost, + HostClientState: ClientStateValidation>, +{ + /// The multi store of the context. + /// This is where the IBC store root is stored at IBC commitment prefix. + pub multi_store: S, + + /// The type of host chain underlying this mock context. + pub host: H, + + /// An object that stores all IBC related data. + pub ibc_store: MockIbcStore, + + /// A router that can route messages to the appropriate IBC application. + pub ibc_router: MockRouter, +} + +/// A mock store type using basecoin-storage implementations. +pub type MockStore = RevertibleStore>; +/// A [`StoreGenericTestContext`] using [`MockStore`]. +pub type TestContext = StoreGenericTestContext; +/// A [`StoreGenericTestContext`] using [`MockStore`] and [`MockHost`]. +pub type MockContext = TestContext; +/// A [`StoreGenericTestContext`] using [`MockStore`] and [`TendermintHost`]. +pub type TendermintContext = TestContext; + +/// Returns a [`StoreGenericTestContext`] with bare minimum initialization: no clients, no connections, and no channels are +/// present, and the chain has Height(5). This should be used sparingly, mostly for testing the +/// creation of new domain objects. +impl Default for StoreGenericTestContext +where + S: ProvableStore + Debug + Default, + H: TestHost, + HostClientState: ClientStateValidation>, +{ + fn default() -> Self { + TestContextConfig::builder().build() + } +} + +/// Implementation of internal interface for use in testing. The methods in this interface should +/// _not_ be accessible to any ICS handler. +impl StoreGenericTestContext +where + S: ProvableStore + Debug, + H: TestHost, + HostClientState: ClientStateValidation>, +{ + /// Returns a immutable reference to the IBC store. + pub fn ibc_store(&self) -> &MockIbcStore { + &self.ibc_store + } + + /// Returns a mutable reference to the IBC store. + pub fn ibc_store_mut(&mut self) -> &mut MockIbcStore { + &mut self.ibc_store + } + + /// Returns a immutable reference to the IBC router. + pub fn ibc_router(&self) -> &MockRouter { + &self.ibc_router + } + + /// Returns a mutable reference to the IBC router. + pub fn ibc_router_mut(&mut self) -> &mut MockRouter { + &mut self.ibc_router + } + + /// Returns the block at the given height from the host chain, if exists. + pub fn host_block(&self, target_height: &Height) -> Option { + self.host.get_block(target_height) + } + + /// Returns the latest block from the host chain. + pub fn query_latest_block(&self) -> Option { + self.host.get_block(&self.latest_height()) + } + + /// Returns the latest height of client state for the given [`ClientId`]. + pub fn light_client_latest_height(&self, client_id: &ClientId) -> Height { + self.ibc_store + .client_state(client_id) + .expect("client state exists") + .latest_height() + } + + /// Advances the host chain height to the given target height. + pub fn advance_block_up_to_height(mut self, target_height: Height) -> Self { + let latest_height = self.host.latest_height(); + if target_height.revision_number() != latest_height.revision_number() { + panic!("Cannot advance history of the chain to a different revision number!") + } else if target_height.revision_height() < latest_height.revision_height() { + panic!("Cannot rewind history of the chain to a smaller revision height!") + } else { + // Repeatedly advance the host chain height till we hit the desired height + while self.host.latest_height().revision_height() < target_height.revision_height() { + self.advance_block_height() + } + } + self + } + + /// Advance the first height of the host chain by generating a genesis block. + /// + /// This method is exactly the same as [`Self::advance_genesis_height`]. + /// But it bootstraps the genesis block by height 1 and `genesis_time`. + /// + /// The method starts and ends with [`Self::end_block`] and [`Self::begin_block`], just + /// like the [`Self::advance_block_height_with_params`], so that it can advance to next height + /// i.e. height 2 - just by calling [`Self::advance_block_height_with_params`]. + pub fn advance_genesis_height(&mut self, genesis_time: Timestamp, params: &H::BlockParams) { + self.end_block(); + + // commit multi store + let multi_store_commitment = self.multi_store.commit().expect("no error"); + + // generate a genesis block + // this is basically self.host.produce_block() but with + // block height 1 and block timestamp `genesis_time`. + let genesis_block = + self.host + .generate_block(multi_store_commitment, 1, genesis_time, params); + + // push the genesis block to the host + self.host.push_block(genesis_block); + + self.begin_block(); + } + + /// Begin a new block on the context. + /// + /// This method commits the required metadata from the last block generation + /// and consensus, and prepares the context for the next block. This includes + /// the latest consensus state and the latest IBC commitment proof. + pub fn begin_block(&mut self) { + let consensus_state = self + .host + .latest_block() + .into_header() + .into_consensus_state() + .into(); + + let ibc_commitment_proof = self + .multi_store + .get_proof( + self.host.latest_height().revision_height().into(), + &self + .ibc_store + .commitment_prefix() + .as_bytes() + .try_into() + .expect("valid utf8 prefix"), + ) + .expect("no error"); + + self.ibc_store.begin_block( + self.host.latest_height().revision_height(), + consensus_state, + ibc_commitment_proof, + ); + } + + /// End the current block on the context. + /// + /// This method commits the state of the IBC store and the host's multi store. + pub fn end_block(&mut self) { + // commit ibc store + let ibc_store_commitment = self.ibc_store.end_block().expect("no error"); + + // commit ibc store commitment in multi store + self.multi_store + .set( + self.ibc_store + .commitment_prefix() + .as_bytes() + .try_into() + .expect("valid utf8 prefix"), + ibc_store_commitment, + ) + .expect("no error"); + } + + /// Commit store state to the current block of the host chain by: + /// - Committing the state to the context's multi store. + /// - Generating a new block with the commitment. + /// - Adding the generated block to the host's block history. + pub fn commit_state_to_host(&mut self, block_time: Duration, params: &H::BlockParams) { + // commit the multi store + let multi_store_commitment = self.multi_store.commit().expect("no error"); + // generate a new block and add it to the block history + self.host + .commit_block(multi_store_commitment, block_time, params); + } + + /// Advances the host chain height by ending the current block, producing a new block, and + /// beginning the next block. + pub fn advance_block_height_with_params( + &mut self, + block_time: Duration, + params: &H::BlockParams, + ) { + self.end_block(); + self.commit_state_to_host(block_time, params); + self.begin_block(); + } + + /// Convenience method to advance the host chain height using default parameters. + pub fn advance_block_height(&mut self) { + self.advance_block_height_with_params( + Duration::from_secs(DEFAULT_BLOCK_TIME_SECS), + &Default::default(), + ) + } + + /// Returns the latest height of the host chain. + pub fn latest_height(&self) -> Height { + let latest_ibc_height = self.ibc_store.host_height().expect("Never fails"); + let latest_host_height = self.host.latest_height(); + assert_eq!( + latest_ibc_height, latest_host_height, + "The IBC store and the host chain must have the same height" + ); + latest_ibc_height + } + + /// Returns the latest timestamp of the host chain. + pub fn latest_timestamp(&self) -> Timestamp { + self.host.latest_block().timestamp() + } + + /// Returns the timestamp at the given height. + pub fn timestamp_at(&self, height: Height) -> Timestamp { + self.host + .get_block(&height) + .expect("block exists") + .timestamp() + } + + /// Bootstraps the context with a client state and its corresponding [`ClientId`]. + pub fn with_client_state(mut self, client_id: &ClientId, client_state: AnyClientState) -> Self { + let client_state_path = ClientStatePath::new(client_id.clone()); + self.ibc_store + .store_client_state(client_state_path, client_state) + .expect("error writing to store"); + self + } + + /// Bootstraps the context with a consensus state and its corresponding [`ClientId`] and [`Height`]. + pub fn with_consensus_state( + mut self, + client_id: &ClientId, + height: Height, + consensus_state: AnyConsensusState, + ) -> Self { + let consensus_state_path = ClientConsensusStatePath::new( + client_id.clone(), + height.revision_number(), + height.revision_height(), + ); + self.ibc_store + .store_consensus_state(consensus_state_path, consensus_state) + .expect("error writing to store"); + + self + } + + /// Generates a light client for the host by generating a client + /// state, as well as generating consensus states for each + /// consensus height. + pub fn generate_light_client( + &self, + mut consensus_heights: Vec, + client_params: &H::LightClientParams, + ) -> LightClientState { + let client_height = if let Some(&height) = consensus_heights.last() { + height + } else { + consensus_heights.push(self.latest_height()); + self.latest_height() + }; + + let client_state = self + .host + .generate_client_state(&client_height, client_params); + + let consensus_states = consensus_heights + .into_iter() + .map(|height| { + ( + height, + self.host_block(&height) + .expect("block exists") + .into_header() + .into_consensus_state(), + ) + }) + .collect(); + + LightClientState { + client_state, + consensus_states, + } + } + + /// Bootstrap a light client with ClientState and its ConsensusState(s) to this context. + pub fn with_light_client( + mut self, + client_id: &ClientId, + light_client: LightClientState, + ) -> Self + where + RH: TestHost, + { + self = self.with_client_state(client_id, light_client.client_state.into()); + + for (height, consensus_state) in light_client.consensus_states { + self = self.with_consensus_state(client_id, height, consensus_state.into()); + + self.ibc_store + .store_update_meta( + client_id.clone(), + height, + self.latest_timestamp(), + self.latest_height(), + ) + .expect("error writing to store"); + } + + self + } + + /// Bootstraps a IBC connection to this context. + /// + /// This does not bootstrap any light client. + pub fn with_connection( + mut self, + connection_id: ConnectionId, + connection_end: ConnectionEnd, + ) -> Self { + let connection_path = ConnectionPath::new(&connection_id); + self.ibc_store + .store_connection(&connection_path, connection_end) + .expect("error writing to store"); + self + } + + /// Bootstraps a IBC channel to this context. + /// + /// This does not bootstrap any corresponding IBC connection or light client. + pub fn with_channel( + mut self, + port_id: PortId, + chan_id: ChannelId, + channel_end: ChannelEnd, + ) -> Self { + let channel_end_path = ChannelEndPath::new(&port_id, &chan_id); + self.ibc_store + .store_channel(&channel_end_path, channel_end) + .expect("error writing to store"); + self + } + + /// Bootstraps a send sequence to this context. + /// + /// This does not bootstrap any corresponding IBC channel, connection or light client. + pub fn with_send_sequence( + mut self, + port_id: PortId, + chan_id: ChannelId, + seq_number: Sequence, + ) -> Self { + let seq_send_path = SeqSendPath::new(&port_id, &chan_id); + self.ibc_store + .store_next_sequence_send(&seq_send_path, seq_number) + .expect("error writing to store"); + self + } + + /// Bootstraps a receive sequence to this context. + /// + /// This does not bootstrap any corresponding IBC channel, connection or light client. + pub fn with_recv_sequence( + mut self, + port_id: PortId, + chan_id: ChannelId, + seq_number: Sequence, + ) -> Self { + let seq_recv_path = SeqRecvPath::new(&port_id, &chan_id); + self.ibc_store + .store_next_sequence_recv(&seq_recv_path, seq_number) + .expect("error writing to store"); + self + } + + /// Bootstraps a ack sequence to this context. + /// + /// This does not bootstrap any corresponding IBC channel, connection or light client. + pub fn with_ack_sequence( + mut self, + port_id: PortId, + chan_id: ChannelId, + seq_number: Sequence, + ) -> Self { + let seq_ack_path = SeqAckPath::new(&port_id, &chan_id); + self.ibc_store + .store_next_sequence_ack(&seq_ack_path, seq_number) + .expect("error writing to store"); + self + } + + /// Bootstraps a packet commitment to this context. + /// + /// This does not bootstrap any corresponding IBC channel, connection or light client. + pub fn with_packet_commitment( + mut self, + port_id: PortId, + chan_id: ChannelId, + seq: Sequence, + data: PacketCommitment, + ) -> Self { + let commitment_path = CommitmentPath::new(&port_id, &chan_id, seq); + self.ibc_store + .store_packet_commitment(&commitment_path, data) + .expect("error writing to store"); + self + } + + /// Calls [`validate`] function on [`MsgEnvelope`] using the context's IBC store and router. + pub fn validate(&mut self, msg: MsgEnvelope) -> Result<(), ContextError> { + validate(&self.ibc_store, &self.ibc_router, msg) + } + + /// Calls [`execute`] function on [`MsgEnvelope`] using the context's IBC store and router. + pub fn execute(&mut self, msg: MsgEnvelope) -> Result<(), ContextError> { + execute(&mut self.ibc_store, &mut self.ibc_router, msg) + } + + /// Calls [`dispatch`] function on [`MsgEnvelope`] using the context's IBC store and router. + pub fn dispatch(&mut self, msg: MsgEnvelope) -> Result<(), ContextError> { + dispatch(&mut self.ibc_store, &mut self.ibc_router, msg) + } + + /// 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: MsgEnvelope) -> Result<(), RelayerError> { + self.dispatch(msg) + .map_err(RelayerError::TransactionFailed)?; + // Create a new block. + self.advance_block_height(); + Ok(()) + } + + /// Returns all the events that have been emitted by the context's IBC store. + pub fn get_events(&self) -> Vec { + self.ibc_store.events.lock().clone() + } + + /// Returns all the logs that have been emitted by the context's IBC store. + pub fn get_logs(&self) -> Vec { + self.ibc_store.logs.lock().clone() + } +} + +#[cfg(test)] +mod tests { + use ibc::core::client::context::consensus_state::ConsensusState; + + use super::*; + use crate::hosts::{HostConsensusState, MockHost, TendermintHost}; + use crate::testapp::ibc::core::types::DefaultIbcStore; + + #[test] + fn test_mock_history_validation() { + pub struct Test + where + H: TestHost, + HostConsensusState: ConsensusState, + HostClientState: ClientStateValidation, + { + name: String, + ctx: TestContext, + } + + fn run_tests(sub_title: &str) + where + H: TestHost, + HostConsensusState: ConsensusState, + HostClientState: ClientStateValidation, + { + let cv = 0; // The version to use for all chains. + + let tests: Vec> = vec![ + Test { + name: "Empty history, small pruning window".to_string(), + ctx: TestContextConfig::builder() + .latest_height(Height::new(cv, 1).expect("Never fails")) + .build(), + }, + Test { + name: "Large pruning window".to_string(), + ctx: TestContextConfig::builder() + .latest_height(Height::new(cv, 2).expect("Never fails")) + .build(), + }, + Test { + name: "Small pruning window".to_string(), + ctx: TestContextConfig::builder() + .latest_height(Height::new(cv, 30).expect("Never fails")) + .build(), + }, + Test { + name: "Small pruning window, small starting height".to_string(), + ctx: TestContextConfig::builder() + .latest_height(Height::new(cv, 2).expect("Never fails")) + .build(), + }, + // This is disabled, as now we generate all the blocks till latest_height + // Generating 2000 Tendermint blocks is slow. + // Test { + // name: "Large pruning window, large starting height".to_string(), + // ctx: TestContextConfig::builder() + // .latest_height(Height::new(cv, 2000).expect("Never fails")) + // .build(), + // }, + ]; + + for mut test in tests { + // All tests should yield a valid context after initialization. + assert!( + test.ctx.host.validate().is_ok(), + "failed in test [{}] {} while validating context {:?}", + sub_title, + test.name, + test.ctx + ); + + let current_height = test.ctx.latest_height(); + + // After advancing the chain's height, the context should still be valid. + test.ctx.advance_block_height(); + assert!( + test.ctx.host.validate().is_ok(), + "failed in test [{}] {} while validating context {:?}", + sub_title, + test.name, + test.ctx + ); + + let next_height = current_height.increment(); + assert_eq!( + test.ctx.latest_height(), + next_height, + "failed while increasing height for context {:?}", + test.ctx + ); + + assert_eq!( + test.ctx + .host + .get_block(¤t_height) + .expect("Never fails") + .height(), + current_height, + "failed while fetching height {:?} of context {:?}", + current_height, + test.ctx + ); + } + } + + run_tests::("Mock Host"); + run_tests::("Synthetic TM Host"); + } +} diff --git a/ibc-testkit/src/fixtures/applications/transfer.rs b/ibc-testkit/src/fixtures/applications/transfer.rs index 35e71fac0..dfd16a5e0 100644 --- a/ibc-testkit/src/fixtures/applications/transfer.rs +++ b/ibc-testkit/src/fixtures/applications/transfer.rs @@ -26,7 +26,7 @@ pub struct MsgTransferConfig { impl From for MsgTransfer { fn from(config: MsgTransferConfig) -> Self { - MsgTransfer { + Self { port_id_on_a: config.port_id_on_a, chan_id_on_a: config.chan_id_on_a, packet_data: config.packet_data, @@ -67,7 +67,7 @@ pub struct PacketDataConfig { impl From for PacketData { fn from(config: PacketDataConfig) -> Self { - PacketData { + Self { token: config.token, sender: config.sender, receiver: config.receiver, diff --git a/ibc-testkit/src/fixtures/clients/tendermint.rs b/ibc-testkit/src/fixtures/clients/tendermint.rs index f96ad0e6b..47b885a52 100644 --- a/ibc-testkit/src/fixtures/clients/tendermint.rs +++ b/ibc-testkit/src/fixtures/clients/tendermint.rs @@ -1,6 +1,7 @@ use core::str::FromStr; use core::time::Duration; +use basecoin_store::avl::get_proof_spec as basecoin_proof_spec; use ibc::clients::tendermint::client_state::ClientState as TmClientState; use ibc::clients::tendermint::types::error::{Error as ClientError, Error}; use ibc::clients::tendermint::types::proto::v1::{ClientState as RawTmClientState, Fraction}; @@ -15,6 +16,7 @@ use ibc::core::commitment_types::specs::ProofSpecs; use ibc::core::host::types::identifiers::ChainId; use ibc::core::primitives::prelude::*; use tendermint::block::Header as TmHeader; +use typed_builder::TypedBuilder; /// Returns a dummy tendermint `ClientState` by given `frozen_height`, for testing purposes only! pub fn dummy_tm_client_state_from_raw(frozen_height: RawHeight) -> Result { @@ -64,9 +66,8 @@ pub fn dummy_raw_tm_client_state(frozen_height: RawHeight) -> RawTmClientState { } } -#[derive(typed_builder::TypedBuilder, Debug)] +#[derive(TypedBuilder, Debug)] pub struct ClientStateConfig { - pub chain_id: ChainId, #[builder(default = TrustThreshold::ONE_THIRD)] pub trust_level: TrustThreshold, #[builder(default = Duration::from_secs(64000))] @@ -74,9 +75,8 @@ pub struct ClientStateConfig { #[builder(default = Duration::from_secs(128_000))] pub unbonding_period: Duration, #[builder(default = Duration::from_millis(3000))] - max_clock_drift: Duration, - pub latest_height: Height, - #[builder(default = ProofSpecs::cosmos())] + pub max_clock_drift: Duration, + #[builder(default = vec![basecoin_proof_spec(); 2].try_into().expect("no error"))] pub proof_specs: ProofSpecs, #[builder(default)] pub upgrade_path: Vec, @@ -84,23 +84,30 @@ pub struct ClientStateConfig { allow_update: AllowUpdate, } -impl TryFrom for TmClientState { - type Error = ClientError; - - fn try_from(config: ClientStateConfig) -> Result { - let client_state = ClientStateType::new( - config.chain_id, - config.trust_level, - config.trusting_period, - config.unbonding_period, - config.max_clock_drift, - config.latest_height, - config.proof_specs, - config.upgrade_path, - config.allow_update, - )?; - - Ok(TmClientState::from(client_state)) +impl Default for ClientStateConfig { + fn default() -> Self { + Self::builder().build() + } +} + +impl ClientStateConfig { + pub fn into_client_state( + self, + chain_id: ChainId, + latest_height: Height, + ) -> Result { + Ok(ClientStateType::new( + chain_id, + self.trust_level, + self.trusting_period, + self.unbonding_period, + self.max_clock_drift, + latest_height, + self.proof_specs, + self.upgrade_path, + self.allow_update, + )? + .into()) } } @@ -146,7 +153,7 @@ pub fn dummy_ics07_header() -> Header { // Build a set of validators. // Below are test values inspired form `test_validator_set()` in tendermint-rs. - let v1: ValidatorInfo = ValidatorInfo::new( + let v1 = ValidatorInfo::new( PublicKey::from_raw_ed25519( &hex::decode_upper("F349539C7E5EF7C49549B09C4BFC2335318AB0FE51FBFAA2433B4F13E816F4A7") .expect("Never fails"), diff --git a/ibc-testkit/src/fixtures/core/channel/mod.rs b/ibc-testkit/src/fixtures/core/channel/mod.rs index 96834609e..5f12f7b88 100644 --- a/ibc-testkit/src/fixtures/core/channel/mod.rs +++ b/ibc-testkit/src/fixtures/core/channel/mod.rs @@ -10,22 +10,23 @@ mod recv_packet; mod timeout; mod timeout_on_close; -pub use acknowledgement::*; -pub use chan_close_confirm::*; -pub use chan_close_init::*; -pub use chan_open_ack::*; -pub use chan_open_confirm::*; -pub use chan_open_init::*; -pub use chan_open_try::*; use ibc::core::channel::types::proto::v1::{ Channel as RawChannel, Counterparty as RawCounterparty, }; use ibc::core::host::types::identifiers::{ChannelId, ConnectionId, PortId}; use ibc::primitives::prelude::*; -pub use packet::*; -pub use recv_packet::*; -pub use timeout::*; -pub use timeout_on_close::*; + +pub use self::acknowledgement::*; +pub use self::chan_close_confirm::*; +pub use self::chan_close_init::*; +pub use self::chan_open_ack::*; +pub use self::chan_open_confirm::*; +pub use self::chan_open_init::*; +pub use self::chan_open_try::*; +pub use self::packet::*; +pub use self::recv_packet::*; +pub use self::timeout::*; +pub use self::timeout_on_close::*; /// Returns a dummy `RawCounterparty`, for testing purposes only! /// Can be optionally parametrized with a specific channel identifier. diff --git a/ibc-testkit/src/fixtures/core/channel/packet.rs b/ibc-testkit/src/fixtures/core/channel/packet.rs index a4e0ca36e..9e213826f 100644 --- a/ibc-testkit/src/fixtures/core/channel/packet.rs +++ b/ibc-testkit/src/fixtures/core/channel/packet.rs @@ -31,7 +31,7 @@ pub struct PacketConfig { impl From for Packet { fn from(config: PacketConfig) -> Self { - Packet { + Self { seq_on_a: config.seq_on_a, port_id_on_a: config.port_id_on_a, chan_id_on_a: config.chan_id_on_a, diff --git a/ibc-testkit/src/fixtures/core/client/mod.rs b/ibc-testkit/src/fixtures/core/client/mod.rs index 6f2f3d841..508fe0e3b 100644 --- a/ibc-testkit/src/fixtures/core/client/mod.rs +++ b/ibc-testkit/src/fixtures/core/client/mod.rs @@ -5,10 +5,10 @@ mod msg_update_client; mod msg_upgrade_client; #[cfg(feature = "serde")] -pub use msg_create_client::*; +pub use self::msg_create_client::*; #[cfg(feature = "serde")] -pub use msg_update_client::*; -pub use msg_upgrade_client::*; +pub use self::msg_update_client::*; +pub use self::msg_upgrade_client::*; #[cfg(test)] mod tests { diff --git a/ibc-testkit/src/fixtures/core/connection/mod.rs b/ibc-testkit/src/fixtures/core/connection/mod.rs index 5b8388147..3460a3f10 100644 --- a/ibc-testkit/src/fixtures/core/connection/mod.rs +++ b/ibc-testkit/src/fixtures/core/connection/mod.rs @@ -3,16 +3,17 @@ mod conn_open_confirm; mod conn_open_init; mod conn_open_try; -pub use conn_open_ack::*; -pub use conn_open_confirm::*; -pub use conn_open_init::*; -pub use conn_open_try::*; use ibc::core::commitment_types::proto::v1::MerklePrefix; use ibc::core::connection::types::proto::v1::Counterparty as RawCounterparty; use ibc::core::host::types::identifiers::ConnectionId; use ibc::core::primitives::prelude::*; use typed_builder::TypedBuilder; +pub use self::conn_open_ack::*; +pub use self::conn_open_confirm::*; +pub use self::conn_open_init::*; +pub use self::conn_open_try::*; + #[derive(TypedBuilder, Debug)] #[builder(build_method(into = RawCounterparty))] pub struct CounterpartyConfig { diff --git a/ibc-testkit/src/fixtures/core/context.rs b/ibc-testkit/src/fixtures/core/context.rs index a886777e7..99304f0e0 100644 --- a/ibc-testkit/src/fixtures/core/context.rs +++ b/ibc-testkit/src/fixtures/core/context.rs @@ -1,127 +1,100 @@ -use alloc::sync::Arc; -use core::cmp::min; -use core::ops::{Add, Sub}; +use alloc::fmt::Debug; use core::time::Duration; +use basecoin_store::context::ProvableStore; +use ibc::core::client::context::client_state::ClientStateValidation; use ibc::core::client::types::Height; -use ibc::core::host::types::identifiers::ChainId; use ibc::core::primitives::prelude::*; use ibc::core::primitives::Timestamp; -use parking_lot::Mutex; -use tendermint_testgen::Validator as TestgenValidator; use typed_builder::TypedBuilder; -use crate::hosts::block::{HostBlock, HostType}; -use crate::testapp::ibc::core::types::{MockContext, MockIbcStore, DEFAULT_BLOCK_TIME_SECS}; +use crate::context::StoreGenericTestContext; +use crate::hosts::{HostClientState, TestBlock, TestHost}; +use crate::testapp::ibc::core::router::MockRouter; +use crate::testapp::ibc::core::types::{MockIbcStore, DEFAULT_BLOCK_TIME_SECS}; +use crate::utils::year_2023; -/// Configuration of the `MockContext` type for generating dummy contexts. +/// Configuration of the [`StoreGenericTestContext`] type for generating dummy contexts. #[derive(Debug, TypedBuilder)] -#[builder(build_method(into = MockContext))] -pub struct MockContextConfig { - #[builder(default = HostType::Mock)] - host_type: HostType, - - #[builder(default = ChainId::new("mockgaia-0").expect("Never fails"))] - host_id: ChainId, +#[builder(build_method(into))] +pub struct TestContextConfig +where + H: TestHost, +{ + #[builder(default)] + host: H, #[builder(default = Duration::from_secs(DEFAULT_BLOCK_TIME_SECS))] block_time: Duration, - // may panic if validator_set_history size is less than max_history_size + 1 - #[builder(default = 5)] - max_history_size: u64, + #[builder(default = year_2023())] + latest_timestamp: Timestamp, - #[builder(default, setter(strip_option))] - validator_set_history: Option>>, + #[builder(default)] + block_params_history: Vec, #[builder(default = Height::new(0, 5).expect("Never fails"))] latest_height: Height, - - #[builder(default = Timestamp::now())] - latest_timestamp: Timestamp, } -impl From for MockContext { - fn from(params: MockContextConfig) -> Self { - assert_ne!( - params.max_history_size, 0, - "The chain must have a non-zero max_history_size" - ); - +impl From> for StoreGenericTestContext +where + S: ProvableStore + Debug + Default, + H: TestHost, + HostClientState: ClientStateValidation>, +{ + fn from(params: TestContextConfig) -> Self { assert_ne!( params.latest_height.revision_height(), 0, "The chain must have a non-zero revision_height" ); - // Compute the number of blocks to store. - let n = min( - params.max_history_size, - params.latest_height.revision_height(), + // timestamp at height 1 + let genesis_timestamp = (params.latest_timestamp + - (params.block_time + * u32::try_from(params.latest_height.revision_height() - 1).expect("no overflow"))) + .expect("no underflow"); + + let mut context = Self { + multi_store: Default::default(), + host: params.host, + ibc_store: MockIbcStore::new( + params.latest_height.revision_number(), + Default::default(), + ), + ibc_router: MockRouter::new_with_transfer(), + }; + + // store is at height 0; no block + + context.advance_genesis_height(genesis_timestamp, &Default::default()); + + // store is at height 1; one block + + context = context.advance_block_up_to_height( + params + .latest_height + .sub(params.block_params_history.len() as u64) + .expect("no error"), ); + for block_params in params.block_params_history { + context.advance_block_height_with_params(params.block_time, &block_params); + } + assert_eq!( - params.host_id.revision_number(), - params.latest_height.revision_number(), - "The version in the chain identifier must match the version in the latest height" + context.host.latest_block().height(), + params.latest_height, + "The latest height in the host must match the latest height in the context" ); - let next_block_timestamp = params - .latest_timestamp - .add(params.block_time) - .expect("Never fails"); - - let history = if let Some(validator_set_history) = params.validator_set_history { - (0..n) - .rev() - .map(|i| { - // generate blocks with timestamps -> N, N - BT, N - 2BT, ... - // where N = now(), BT = block_time - HostBlock::generate_block_with_validators( - params.host_id.clone(), - params.host_type, - params - .latest_height - .sub(i) - .expect("Never fails") - .revision_height(), - next_block_timestamp - .sub(params.block_time * ((i + 1) as u32)) - .expect("Never fails"), - &validator_set_history[(n - i) as usize - 1], - &validator_set_history[(n - i) as usize], - ) - }) - .collect() - } else { - (0..n) - .rev() - .map(|i| { - // generate blocks with timestamps -> N, N - BT, N - 2BT, ... - // where N = now(), BT = block_time - HostBlock::generate_block( - params.host_id.clone(), - params.host_type, - params - .latest_height - .sub(i) - .expect("Never fails") - .revision_height(), - next_block_timestamp - .sub(params.block_time * ((i + 1) as u32)) - .expect("Never fails"), - ) - }) - .collect() - }; + assert_eq!( + context.host.latest_block().timestamp(), + params.latest_timestamp, + "The latest timestamp in the host must match the latest timestamp in the context" + ); - MockContext { - host_chain_type: params.host_type, - host_chain_id: params.host_id.clone(), - max_history_size: params.max_history_size, - history, - block_time: params.block_time, - ibc_store: Arc::new(Mutex::new(MockIbcStore::default())), - } + context } } diff --git a/ibc-testkit/src/fixtures/mod.rs b/ibc-testkit/src/fixtures/mod.rs index af14cadb9..f6b51ef46 100644 --- a/ibc-testkit/src/fixtures/mod.rs +++ b/ibc-testkit/src/fixtures/mod.rs @@ -6,15 +6,15 @@ use alloc::fmt::Debug; use ibc::core::handler::types::error::ContextError; use ibc::core::primitives::prelude::*; -use crate::testapp::ibc::core::types::MockContext; +use crate::testapp::ibc::core::types::DefaultIbcStore; pub enum Expect { Success, Failure(Option), } -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct Fixture { - pub ctx: MockContext, + pub ctx: DefaultIbcStore, pub msg: M, } diff --git a/ibc-testkit/src/hosts/block.rs b/ibc-testkit/src/hosts/block.rs deleted file mode 100644 index 526ca1784..000000000 --- a/ibc-testkit/src/hosts/block.rs +++ /dev/null @@ -1,260 +0,0 @@ -//! Host chain types and methods, used by context mock. - -use core::str::FromStr; - -use ibc::clients::tendermint::consensus_state::ConsensusState as TmConsensusState; -use ibc::clients::tendermint::types::proto::v1::Header as RawHeader; -use ibc::clients::tendermint::types::{Header, TENDERMINT_HEADER_TYPE_URL}; -use ibc::core::client::types::error::ClientError; -use ibc::core::client::types::Height; -use ibc::core::host::types::identifiers::ChainId; -use ibc::core::primitives::prelude::*; -use ibc::core::primitives::Timestamp; -use ibc::primitives::proto::{Any, Protobuf}; -use ibc::primitives::ToVec; -use tendermint::block::Header as TmHeader; -use tendermint::validator::Set as ValidatorSet; -use tendermint_testgen::light_block::TmLightBlock; -use tendermint_testgen::{ - Generator, Header as TestgenHeader, LightBlock as TestgenLightBlock, - Validator as TestgenValidator, -}; - -use crate::testapp::ibc::clients::mock::consensus_state::MockConsensusState; -use crate::testapp::ibc::clients::mock::header::MockHeader; -use crate::testapp::ibc::clients::AnyConsensusState; - -/// Defines the different types of host chains that a mock context can emulate. -/// The variants are as follows: -/// - `Mock` defines that the context history consists of `MockHeader` blocks. -/// - `SyntheticTendermint`: the context has synthetically-generated Tendermint (light) blocks. -/// See also the `HostBlock` enum to get more insights into the underlying block type. -#[derive(Clone, Debug, Copy)] -pub enum HostType { - Mock, - SyntheticTendermint, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SyntheticTmBlock { - pub trusted_height: Height, - pub trusted_next_validators: ValidatorSet, - pub light_block: TmLightBlock, -} - -impl SyntheticTmBlock { - pub fn header(&self) -> &TmHeader { - &self.light_block.signed_header.header - } -} - -impl From for Header { - fn from(light_block: SyntheticTmBlock) -> Self { - let SyntheticTmBlock { - trusted_height, - trusted_next_validators, - light_block, - } = light_block; - Self { - signed_header: light_block.signed_header, - validator_set: light_block.validators, - trusted_height, - trusted_next_validator_set: trusted_next_validators, - } - } -} - -/// Depending on `HostType` (the type of host chain underlying a context mock), this enum defines -/// the type of blocks composing the history of the host chain. -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum HostBlock { - Mock(Box), - 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::from_str(light_block.header().chain_id.as_str()) - .expect("Never fails") - .revision_number(), - light_block.header().height.value(), - ) - .expect("Never fails"), - } - } - - pub fn set_trusted_height(&mut self, height: Height) { - match self { - HostBlock::Mock(_) => {} - HostBlock::SyntheticTendermint(light_block) => light_block.trusted_height = height, - } - } - - pub fn set_trusted_next_validators_set(&mut self, trusted_next_validators: ValidatorSet) { - match self { - HostBlock::Mock(_) => {} - HostBlock::SyntheticTendermint(light_block) => { - light_block.trusted_next_validators = trusted_next_validators - } - } - } - - /// Returns the timestamp of a block. - pub fn timestamp(&self) -> Timestamp { - match self { - HostBlock::Mock(header) => header.timestamp, - HostBlock::SyntheticTendermint(light_block) => light_block.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, - timestamp: Timestamp, - ) -> HostBlock { - match chain_type { - HostType::Mock => HostBlock::Mock(Box::new(MockHeader { - height: Height::new(chain_id.revision_number(), height).expect("Never fails"), - timestamp, - })), - HostType::SyntheticTendermint => HostBlock::SyntheticTendermint(Box::new( - Self::generate_tm_block(chain_id, height, timestamp), - )), - } - } - - /// Generates a new block at `height` for the given chain identifier, chain type and validator sets. - pub fn generate_block_with_validators( - chain_id: ChainId, - chain_type: HostType, - height: u64, - timestamp: Timestamp, - validators: &[TestgenValidator], - next_validators: &[TestgenValidator], - ) -> HostBlock { - match chain_type { - HostType::Mock => HostBlock::Mock(Box::new(MockHeader { - height: Height::new(chain_id.revision_number(), height).expect("Never fails"), - timestamp, - })), - HostType::SyntheticTendermint => { - let light_block = TestgenLightBlock::new_default_with_header( - TestgenHeader::new(validators) - .height(height) - .chain_id(chain_id.as_str()) - .next_validators(next_validators) - .time(timestamp.into_tm_time().expect("Never fails")), - ) - .validators(validators) - .next_validators(next_validators) - .generate() - .expect("Never fails"); - - HostBlock::SyntheticTendermint(Box::new(SyntheticTmBlock { - trusted_height: Height::new(chain_id.revision_number(), 1) - .expect("Never fails"), - trusted_next_validators: light_block.next_validators.clone(), - light_block, - })) - } - } - } - - pub fn generate_tm_block( - chain_id: ChainId, - height: u64, - timestamp: Timestamp, - ) -> SyntheticTmBlock { - let validators = [ - TestgenValidator::new("1").voting_power(50), - TestgenValidator::new("2").voting_power(50), - ]; - - let header = TestgenHeader::new(&validators) - .height(height) - .chain_id(chain_id.as_str()) - .next_validators(&validators) - .time(timestamp.into_tm_time().expect("Never fails")); - - let light_block = TestgenLightBlock::new_default_with_header(header) - .generate() - .expect("Never fails"); - - SyntheticTmBlock { - trusted_height: Height::new(chain_id.revision_number(), 1).expect("Never fails"), - trusted_next_validators: light_block.next_validators.clone(), - light_block, - } - } - - pub fn try_into_tm_block(self) -> Option { - match self { - HostBlock::Mock(_) => None, - HostBlock::SyntheticTendermint(tm_block) => Some(*tm_block), - } - } -} - -impl From for AnyConsensusState { - fn from(light_block: SyntheticTmBlock) -> Self { - let cs = TmConsensusState::from(light_block.header().clone()); - cs.into() - } -} - -impl From for AnyConsensusState { - fn from(any_block: HostBlock) -> Self { - match any_block { - HostBlock::Mock(mock_header) => MockConsensusState::new(*mock_header).into(), - HostBlock::SyntheticTendermint(light_block) => { - TmConsensusState::from(light_block.header().clone()).into() - } - } - } -} - -impl Protobuf for HostBlock {} - -impl TryFrom for HostBlock { - type Error = ClientError; - - fn try_from(_raw: Any) -> Result { - todo!() - } -} - -impl From for Any { - fn from(value: HostBlock) -> Self { - fn encode_light_block(light_block: SyntheticTmBlock) -> Vec { - let SyntheticTmBlock { - trusted_height, - trusted_next_validators, - light_block, - } = light_block; - - RawHeader { - signed_header: Some(light_block.signed_header.into()), - validator_set: Some(light_block.validators.into()), - trusted_height: Some(trusted_height.into()), - trusted_validators: Some(trusted_next_validators.into()), - } - .to_vec() - } - - match value { - HostBlock::Mock(mock_header) => (*mock_header).into(), - HostBlock::SyntheticTendermint(light_block) => Self { - type_url: TENDERMINT_HEADER_TYPE_URL.to_string(), - value: encode_light_block(*light_block), - }, - } - } -} diff --git a/ibc-testkit/src/hosts/mock.rs b/ibc-testkit/src/hosts/mock.rs new file mode 100644 index 000000000..266f69f53 --- /dev/null +++ b/ibc-testkit/src/hosts/mock.rs @@ -0,0 +1,97 @@ +use alloc::vec::Vec; + +use ibc::core::client::types::Height; +use ibc::core::host::types::identifiers::ChainId; +use ibc::core::primitives::Timestamp; +use typed_builder::TypedBuilder; + +use super::{TestBlock, TestHeader, TestHost}; +use crate::testapp::ibc::clients::mock::client_state::MockClientState; +use crate::testapp::ibc::clients::mock::consensus_state::MockConsensusState; +use crate::testapp::ibc::clients::mock::header::MockHeader; + +#[derive(TypedBuilder, Debug)] +pub struct MockHost { + /// Unique identifier for the chain. + #[builder(default = ChainId::new("mock-0").expect("Never fails"))] + pub chain_id: ChainId, + /// The chain of blocks underlying this context. + #[builder(default)] + pub history: Vec, +} + +impl Default for MockHost { + fn default() -> Self { + Self::builder().build() + } +} + +impl TestHost for MockHost { + type Block = MockHeader; + type ClientState = MockClientState; + type BlockParams = (); + type LightClientParams = (); + + fn history(&self) -> &Vec { + &self.history + } + + fn push_block(&mut self, block: Self::Block) { + self.history.push(block); + } + + fn generate_block( + &self, + _commitment_root: Vec, + height: u64, + timestamp: Timestamp, + _params: &Self::BlockParams, + ) -> Self::Block { + MockHeader { + height: Height::new(self.chain_id.revision_number(), height).expect("Never fails"), + timestamp, + } + } + + fn generate_client_state( + &self, + latest_height: &Height, + _params: &Self::LightClientParams, + ) -> Self::ClientState { + MockClientState::new(self.get_block(latest_height).expect("height exists")) + } +} + +impl TestBlock for MockHeader { + type Header = Self; + + fn height(&self) -> Height { + self.height + } + + fn timestamp(&self) -> Timestamp { + self.timestamp + } + + fn into_header_with_trusted(self, _trusted_block: &Self) -> Self::Header { + self + } +} + +impl From for MockConsensusState { + fn from(block: MockHeader) -> Self { + Self::new(block) + } +} + +impl TestHeader for MockHeader { + type ConsensusState = MockConsensusState; + + fn height(&self) -> Height { + self.height + } + + fn timestamp(&self) -> Timestamp { + self.timestamp + } +} diff --git a/ibc-testkit/src/hosts/mod.rs b/ibc-testkit/src/hosts/mod.rs index a863eaad2..426b69066 100644 --- a/ibc-testkit/src/hosts/mod.rs +++ b/ibc-testkit/src/hosts/mod.rs @@ -1 +1,157 @@ -pub mod block; +pub mod mock; +pub mod tendermint; + +use core::fmt::Debug; +use core::ops::Add; +use core::time::Duration; + +use ibc::core::client::context::consensus_state::ConsensusState; +use ibc::core::client::types::Height; +use ibc::core::primitives::prelude::*; +use ibc::core::primitives::Timestamp; +use ibc::primitives::proto::Any; + +pub use self::mock::MockHost; +pub use self::tendermint::TendermintHost; +use crate::testapp::ibc::clients::{AnyClientState, AnyConsensusState}; + +pub type HostClientState = ::ClientState; +pub type HostBlock = ::Block; +pub type HostBlockParams = ::BlockParams; +pub type HostLightClientParams = ::LightClientParams; +pub type HostHeader = as TestBlock>::Header; +pub type HostConsensusState = as TestHeader>::ConsensusState; + +/// TestHost is a trait that defines the interface for a host blockchain. +pub trait TestHost: Default + Debug + Sized { + /// The type of block produced by the host. + type Block: TestBlock; + + /// The type of client state produced by the host. + type ClientState: Into + Debug; + + /// The type of block parameters to produce a block. + type BlockParams: Debug + Default; + + /// The type of light client parameters to produce a light client state. + type LightClientParams: Debug + Default; + + /// The history of blocks produced by the host chain. + fn history(&self) -> &Vec; + + /// Returns true if the host chain has no blocks. + fn is_empty(&self) -> bool { + self.history().is_empty() + } + + /// The latest height of the host chain. + fn latest_height(&self) -> Height { + self.latest_block().height() + } + + /// The latest block of the host chain. + fn latest_block(&self) -> Self::Block { + self.history().last().cloned().expect("no error") + } + + /// Get the block at the given height. + fn get_block(&self, target_height: &Height) -> Option { + self.history() + .get(target_height.revision_height() as usize - 1) + .cloned() // indexed from 1 + } + + /// Add a block to the host chain. + fn push_block(&mut self, block: Self::Block); + + /// Commit a block with commitment root to the blockchain, by extending the history of blocks. + fn commit_block( + &mut self, + commitment_root: Vec, + block_time: Duration, + params: &Self::BlockParams, + ) { + let latest_block = self.latest_block(); + + let height = TestBlock::height(&latest_block) + .increment() + .revision_height(); + let timestamp = TestBlock::timestamp(&latest_block) + .add(block_time) + .expect("Never fails"); + + let new_block = self.generate_block(commitment_root, height, timestamp, params); + + // History is not full yet. + self.push_block(new_block); + } + + /// Generate a block at the given height and timestamp, using the provided parameters. + fn generate_block( + &self, + commitment_root: Vec, + height: u64, + timestamp: Timestamp, + params: &Self::BlockParams, + ) -> Self::Block; + + /// Generate a client state using the block at the given height and the provided parameters. + fn generate_client_state( + &self, + latest_height: &Height, + params: &Self::LightClientParams, + ) -> Self::ClientState; + + fn validate(&self) -> Result<(), String> { + // Check that headers in the history are in sequential order. + let latest_height = self.latest_height(); + let mut current_height = Height::min(latest_height.revision_number()); + + while current_height <= latest_height { + if current_height != self.get_block(¤t_height).expect("no error").height() { + return Err("block height does not match".to_owned()); + } + current_height = current_height.increment(); + } + Ok(()) + } +} + +/// TestBlock is a trait that defines the interface for a block produced by a host blockchain. +pub trait TestBlock: Clone + Debug { + /// The type of header can be extracted from the block. + type Header: TestHeader; + + /// The height of the block. + fn height(&self) -> Height; + + /// The timestamp of the block. + fn timestamp(&self) -> Timestamp; + + /// Extract the IBC header using the target and trusted blocks. + fn into_header_with_trusted(self, trusted_block: &Self) -> Self::Header; + + /// Extract the IBC header only using the target block (sets the trusted + /// block to itself). + fn into_header(self) -> Self::Header { + self.clone().into_header_with_trusted(&self) + } +} + +/// TestHeader is a trait that defines the interface for a header +/// submitted by relayer from the host blockchain. +pub trait TestHeader: Clone + Debug + Into { + /// The type of consensus state can be extracted from the header. + type ConsensusState: ConsensusState + Into + From + Clone + Debug; + + /// The height of the block, as recorded in the header. + fn height(&self) -> Height; + + /// The timestamp of the block, as recorded in the header. + fn timestamp(&self) -> Timestamp; + + /// Extract the consensus state from the header. + fn into_consensus_state(self) -> Self::ConsensusState { + Self::ConsensusState::from(self) + } +} diff --git a/ibc-testkit/src/hosts/tendermint.rs b/ibc-testkit/src/hosts/tendermint.rs new file mode 100644 index 000000000..a3b8af534 --- /dev/null +++ b/ibc-testkit/src/hosts/tendermint.rs @@ -0,0 +1,240 @@ +use core::str::FromStr; + +use ibc::clients::tendermint::client_state::ClientState; +use ibc::clients::tendermint::consensus_state::ConsensusState; +use ibc::clients::tendermint::types::proto::v1::Header as RawHeader; +use ibc::clients::tendermint::types::{Header, TENDERMINT_HEADER_TYPE_URL}; +use ibc::core::client::types::Height; +use ibc::core::host::types::identifiers::ChainId; +use ibc::core::primitives::prelude::*; +use ibc::core::primitives::Timestamp; +use ibc::primitives::proto::Any; +use ibc::primitives::ToVec; +use tendermint::block::Header as TmHeader; +use tendermint::validator::Set as ValidatorSet; +use tendermint_testgen::light_block::TmLightBlock; +use tendermint_testgen::{ + Generator, Header as TestgenHeader, LightBlock as TestgenLightBlock, + Validator as TestgenValidator, +}; +use typed_builder::TypedBuilder; + +use crate::fixtures::clients::tendermint::ClientStateConfig; +use crate::hosts::{TestBlock, TestHeader, TestHost}; + +/// A host that produces Tendermint blocks and interfaces with Tendermint light clients. +#[derive(TypedBuilder, Debug)] +pub struct TendermintHost { + /// Unique identifier for the chain. + #[builder(default = ChainId::new("mock-0").expect("Never fails"))] + pub chain_id: ChainId, + /// The chain of blocks underlying this context. + #[builder(default)] + pub history: Vec, +} + +impl Default for TendermintHost { + fn default() -> Self { + Self::builder().build() + } +} + +impl TestHost for TendermintHost { + type Block = TmLightBlock; + type BlockParams = BlockParams; + type LightClientParams = ClientStateConfig; + type ClientState = ClientState; + + fn history(&self) -> &Vec { + &self.history + } + + fn push_block(&mut self, block: Self::Block) { + self.history.push(block); + } + + fn generate_block( + &self, + commitment_root: Vec, + height: u64, + timestamp: Timestamp, + params: &Self::BlockParams, + ) -> Self::Block { + TestgenLightBlock::new_default_with_header( + TestgenHeader::new(¶ms.validators) + .app_hash(commitment_root.try_into().expect("infallible")) + .height(height) + .chain_id(self.chain_id.as_str()) + .next_validators(¶ms.next_validators) + .time(timestamp.into_tm_time().expect("Never fails")), + ) + .validators(¶ms.validators) + .next_validators(¶ms.next_validators) + .generate() + .expect("Never fails") + } + + fn generate_client_state( + &self, + latest_height: &Height, + params: &Self::LightClientParams, + ) -> Self::ClientState { + let client_state = ClientStateConfig::builder() + .trusting_period(params.trusting_period) + .max_clock_drift(params.max_clock_drift) + .unbonding_period(params.unbonding_period) + .proof_specs(params.proof_specs.clone()) + .build() + .into_client_state( + self.chain_id.clone(), + self.get_block(latest_height) + .expect("block exists") + .height(), + ) + .expect("never fails"); + + client_state.inner().validate().expect("never fails"); + + client_state + } +} + +impl TestBlock for TmLightBlock { + type Header = TendermintHeader; + + fn height(&self) -> Height { + Height::new( + ChainId::from_str(self.signed_header.header.chain_id.as_str()) + .expect("Never fails") + .revision_number(), + self.signed_header.header.height.value(), + ) + .expect("Never fails") + } + + fn timestamp(&self) -> Timestamp { + self.signed_header.header.time.into() + } + + fn into_header_with_trusted(self, trusted_block: &Self) -> Self::Header { + let mut header = TendermintHeader::from(self.clone()); + header.set_trusted_height(trusted_block.height()); + header.set_trusted_next_validators_set(trusted_block.validators.clone()); + header + } +} + +#[derive(Debug, TypedBuilder)] +pub struct BlockParams { + pub validators: Vec, + pub next_validators: Vec, +} + +impl BlockParams { + pub fn from_validator_history(validator_history: Vec>) -> Vec { + validator_history + .windows(2) + .map(|vals| { + Self::builder() + .validators(vals[0].clone()) + .next_validators(vals[1].clone()) + .build() + }) + .collect() + } +} + +impl Default for BlockParams { + fn default() -> Self { + Self::builder() + .validators(vec![ + TestgenValidator::new("1").voting_power(50), + TestgenValidator::new("2").voting_power(50), + ]) + .next_validators(vec![ + TestgenValidator::new("1").voting_power(50), + TestgenValidator::new("2").voting_power(50), + ]) + .build() + } +} + +/// This wrapper type is needed to implement +/// [`From`] traits [`Header`] to foreign types. +#[derive(Debug, Clone)] +pub struct TendermintHeader(Header); + +impl TendermintHeader { + pub fn set_trusted_height(&mut self, trusted_height: Height) { + self.0.trusted_height = trusted_height + } + + pub fn set_trusted_next_validators_set(&mut self, trusted_next_validator_set: ValidatorSet) { + self.0.trusted_next_validator_set = trusted_next_validator_set + } + + pub fn header(&self) -> &TmHeader { + &self.0.signed_header.header + } +} + +impl TestHeader for TendermintHeader { + type ConsensusState = ConsensusState; + + fn height(&self) -> Height { + Height::new( + ChainId::from_str(self.0.signed_header.header.chain_id.as_str()) + .expect("Never fails") + .revision_number(), + self.0.signed_header.header.height.value(), + ) + .expect("Never fails") + } + + fn timestamp(&self) -> Timestamp { + self.0.signed_header.header.time.into() + } +} + +impl From for Header { + fn from(header: TendermintHeader) -> Self { + header.0 + } +} + +impl From for ConsensusState { + fn from(header: TendermintHeader) -> Self { + header.0.signed_header.header.into() + } +} + +impl From for TendermintHeader { + fn from(block: TmLightBlock) -> Self { + let trusted_height = block.height(); + + let TmLightBlock { + signed_header, + validators: validator_set, + .. + } = block; + + let trusted_next_validator_set = validator_set.clone(); + + // by default trust the current height and validators + Self(Header { + signed_header, + validator_set, + trusted_height, + trusted_next_validator_set, + }) + } +} + +impl From for Any { + fn from(value: TendermintHeader) -> Self { + Self { + type_url: TENDERMINT_HEADER_TYPE_URL.to_string(), + value: RawHeader::from(value.0).to_vec(), + } + } +} diff --git a/ibc-testkit/src/lib.rs b/ibc-testkit/src/lib.rs index b2f737471..f4326ba1d 100644 --- a/ibc-testkit/src/lib.rs +++ b/ibc-testkit/src/lib.rs @@ -15,7 +15,9 @@ extern crate alloc; #[cfg(feature = "std")] extern crate std; +pub mod context; pub mod fixtures; pub mod hosts; pub mod relayer; pub mod testapp; +pub mod utils; diff --git a/ibc-testkit/src/relayer/context.rs b/ibc-testkit/src/relayer/context.rs index 852f91bd3..232fb6074 100644 --- a/ibc-testkit/src/relayer/context.rs +++ b/ibc-testkit/src/relayer/context.rs @@ -1,231 +1,359 @@ -use ibc::core::client::context::ClientValidationContext; -use ibc::core::client::types::Height; -use ibc::core::handler::types::error::ContextError; -use ibc::core::host::types::identifiers::ClientId; +use ibc::core::channel::types::packet::Packet; +use ibc::core::client::context::client_state::ClientStateValidation; +use ibc::core::host::types::identifiers::{ChannelId, ClientId, ConnectionId, PortId}; +use ibc::core::host::types::path::ChannelEndPath; use ibc::core::host::ValidationContext; -use ibc::core::primitives::prelude::*; -use ibc::core::primitives::Signer; - -use crate::testapp::ibc::clients::AnyClientState; -use crate::testapp::ibc::core::types::MockContext; -/// Trait capturing all dependencies (i.e., the context) which algorithms in ICS18 require to -/// relay packets between chains. This trait comprises the dependencies towards a single chain. -/// Most of the functions in this represent wrappers over the ABCI interface. -/// This trait mimics the `Chain` trait, but at a lower level of abstraction (no networking, header -/// types, light client, RPC client, etc.) -pub trait RelayerContext { - /// Returns the latest height of the chain. - fn query_latest_height(&self) -> Result; - - /// Returns this client state for the given `client_id` on this chain. - /// Wrapper over the `/abci_query?path=..` endpoint. - fn query_client_full_state(&self, client_id: &ClientId) -> Option; - - /// Temporary solution. Similar to `CosmosSDKChain::key_and_signer()` but simpler. - fn signer(&self) -> Signer; +use ibc::primitives::Signer; + +use crate::context::TestContext; +use crate::hosts::{HostClientState, TestHost}; +use crate::relayer::utils::TypedRelayerOps; +use crate::testapp::ibc::core::types::DefaultIbcStore; + +/// A relayer context that allows interaction between two [`TestContext`] instances. +pub struct RelayerContext +where + A: TestHost, + B: TestHost, + HostClientState: ClientStateValidation, + HostClientState: ClientStateValidation, +{ + ctx_a: TestContext, + ctx_b: TestContext, } -impl RelayerContext for MockContext { - fn query_latest_height(&self) -> Result { - ValidationContext::host_height(self) +impl RelayerContext +where + A: TestHost, + B: TestHost, + HostClientState: ClientStateValidation, + HostClientState: ClientStateValidation, +{ + /// Creates a new relayer context with the given [`TestContext`] instances. + pub fn new(ctx_a: TestContext, ctx_b: TestContext) -> Self { + Self { ctx_a, ctx_b } } - fn query_client_full_state(&self, client_id: &ClientId) -> Option { - // Forward call to Ics2. - self.client_state(client_id).ok() + /// Returns immutable reference to the first context. + pub fn get_ctx_a(&self) -> &TestContext { + &self.ctx_a } - fn signer(&self) -> Signer { - "0CDA3F47EF3C4906693B170EF650EB968C5F4B2C" - .to_string() - .into() + /// Returns immutable reference to the second context. + pub fn get_ctx_b(&self) -> &TestContext { + &self.ctx_b } -} -#[cfg(test)] -mod tests { - use ibc::clients::tendermint::types::client_type as tm_client_type; - use ibc::core::client::context::client_state::ClientStateCommon; - use ibc::core::client::types::msgs::{ClientMsg, MsgUpdateClient}; - use ibc::core::client::types::Height; - use ibc::core::handler::types::msgs::MsgEnvelope; - use ibc::core::host::types::identifiers::ChainId; - use ibc::core::primitives::prelude::*; - use tracing::debug; - - use super::RelayerContext; - use crate::fixtures::core::context::MockContextConfig; - use crate::hosts::block::{HostBlock, HostType}; - use crate::relayer::context::ClientId; - use crate::relayer::error::RelayerError; - use crate::testapp::ibc::clients::mock::client_state::client_type as mock_client_type; - use crate::testapp::ibc::core::router::MockRouter; - use crate::testapp::ibc::core::types::MockClientConfig; - - /// Builds 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(crate) fn build_client_update_datagram( - dest: &Ctx, - client_id: &ClientId, - src_header: &HostBlock, - ) -> Result - where - Ctx: RelayerContext, - { - // Check if client for ibc0 on ibc1 has been updated to latest height: - // - query client state on destination chain - let dest_client_state = dest.query_client_full_state(client_id).ok_or_else(|| { - RelayerError::ClientStateNotFound { - client_id: client_id.clone(), - } - })?; - - let dest_client_latest_height = dest_client_state.latest_height(); - - if src_header.height() == dest_client_latest_height { - return Err(RelayerError::ClientAlreadyUpToDate { - client_id: client_id.clone(), - source_height: src_header.height(), - destination_height: dest_client_latest_height, - }); - }; - - if dest_client_latest_height > src_header.height() { - return Err(RelayerError::ClientAtHigherHeight { - client_id: client_id.clone(), - source_height: src_header.height(), - destination_height: dest_client_latest_height, - }); - }; - - // Client on destination chain can be updated. - Ok(ClientMsg::UpdateClient(MsgUpdateClient { - client_id: client_id.clone(), - client_message: (*src_header).clone().into(), - signer: dest.signer(), - })) - } - - #[test] - /// Serves to test both ICS-26 `dispatch` & `build_client_update_datagram` functions. - /// Implements a "ping pong" of client update messages, so that two chains repeatedly - /// process a client update message and update their height in succession. - fn client_update_ping_pong() { - let chain_a_start_height = Height::new(1, 11).unwrap(); - let chain_b_start_height = Height::new(1, 20).unwrap(); - let client_on_b_for_a_height = Height::new(1, 10).unwrap(); // Should be smaller than `chain_a_start_height` - let client_on_a_for_b_height = Height::new(1, 20).unwrap(); // Should be smaller than `chain_b_start_height` - let num_iterations = 4; - - let client_on_a_for_b = tm_client_type().build_client_id(0); - let client_on_b_for_a = mock_client_type().build_client_id(0); - - let chain_id_a = ChainId::new("mockgaiaA-1").unwrap(); - let chain_id_b = ChainId::new("mockgaiaB-1").unwrap(); - - // Create two mock contexts, one for each chain. - let mut ctx_a = MockContextConfig::builder() - .host_id(chain_id_a.clone()) - .latest_height(chain_a_start_height) - .build() - .with_client_config( - MockClientConfig::builder() - .client_chain_id(chain_id_b.clone()) - .client_id(client_on_a_for_b.clone()) - .latest_height(client_on_a_for_b_height) - .client_type(tm_client_type()) // The target host chain (B) is synthetic TM. - .build(), - ); - // dummy; not actually used in client updates - let mut router_a = MockRouter::new_with_transfer(); - - let mut ctx_b = MockContextConfig::builder() - .host_id(chain_id_b) - .host_type(HostType::SyntheticTendermint) - .latest_height(chain_b_start_height) - .build() - .with_client_config( - MockClientConfig::builder() - .client_chain_id(chain_id_a) - .client_id(client_on_b_for_a.clone()) - .latest_height(client_on_b_for_a_height) - .build(), - ); - // dummy; not actually used in client updates - let mut router_b = MockRouter::new_with_transfer(); - - for _i in 0..num_iterations { - // Update client on chain B to latest height of A. - // - create the client update message with the latest header from A - let a_latest_header = ctx_a.query_latest_header().unwrap(); - let client_msg_b_res = - build_client_update_datagram(&ctx_b, &client_on_b_for_a, &a_latest_header); - - assert!( - client_msg_b_res.is_ok(), - "create_client_update failed for context destination {ctx_b:?}, error: {client_msg_b_res:?}", - ); - - let client_msg_b = client_msg_b_res.unwrap(); - - // - send the message to B. We bypass ICS18 interface and call directly into - // MockContext `recv` method (to avoid additional serialization steps). - let dispatch_res_b = ctx_b.deliver(&mut router_b, MsgEnvelope::Client(client_msg_b)); - let validation_res = ctx_b.validate(); - assert!( - validation_res.is_ok(), - "context validation failed with error {validation_res:?} for context {ctx_b:?}", - ); - - // Check if the update succeeded. - assert!( - dispatch_res_b.is_ok(), - "Dispatch failed for host chain b with error: {dispatch_res_b:?}" - ); - let client_height_b = ctx_b - .query_client_full_state(&client_on_b_for_a) - .unwrap() - .latest_height(); - assert_eq!(client_height_b, ctx_a.query_latest_height().unwrap()); - - // Update client on chain A to latest height of B. - // - create the client update message with the latest header from B - // The test uses LightClientBlock that does not store the trusted height - let mut b_latest_header = ctx_b.query_latest_header().unwrap(); - - let th = b_latest_header.height(); - b_latest_header.set_trusted_height(th.decrement().unwrap()); - - let client_msg_a_res = - build_client_update_datagram(&ctx_a, &client_on_a_for_b, &b_latest_header); - - assert!( - client_msg_a_res.is_ok(), - "create_client_update failed for context destination {ctx_a:?}, error: {client_msg_a_res:?}", - ); - - let client_msg_a = client_msg_a_res.unwrap(); - - debug!("client_msg_a = {:?}", client_msg_a); - - // - send the message to A - let dispatch_res_a = ctx_a.deliver(&mut router_a, MsgEnvelope::Client(client_msg_a)); - let validation_res = ctx_a.validate(); - assert!( - validation_res.is_ok(), - "context validation failed with error {validation_res:?} for context {ctx_a:?}", - ); - - // Check if the update succeeded. - assert!( - dispatch_res_a.is_ok(), - "Dispatch failed for host chain a with error: {dispatch_res_a:?}" - ); - let client_height_a = ctx_a - .query_client_full_state(&client_on_a_for_b) - .unwrap() - .latest_height(); - assert_eq!(client_height_a, ctx_b.query_latest_height().unwrap()); - } + /// Returns mutable reference to the first context. + pub fn get_ctx_a_mut(&mut self) -> &mut TestContext { + &mut self.ctx_a + } + + /// Returns mutable reference to the second context. + pub fn get_ctx_b_mut(&mut self) -> &mut TestContext { + &mut self.ctx_b + } + + /// Creates a light client of second context on the first context. + /// Returns the client identifier of the created client. + pub fn create_client_on_a(&mut self, signer: Signer) -> ClientId { + TypedRelayerOps::::create_client_on_a(&mut self.ctx_a, &self.ctx_b, signer) + } + + /// Creates a light client of first context on the second context. + /// Returns the client identifier of the created client. + pub fn create_client_on_b(&mut self, signer: Signer) -> ClientId { + TypedRelayerOps::::create_client_on_a(&mut self.ctx_b, &self.ctx_a, signer) + } + + /// Updates the client on the first context with the latest header of the second context. + pub fn update_client_on_a_with_sync(&mut self, client_id_on_a: ClientId, signer: Signer) { + TypedRelayerOps::::update_client_on_a_with_sync( + &mut self.ctx_a, + &mut self.ctx_b, + client_id_on_a, + signer, + ) + } + + /// Updates the client on the second context with the latest header of the first context. + pub fn update_client_on_b_with_sync(&mut self, client_id_on_b: ClientId, signer: Signer) { + TypedRelayerOps::::update_client_on_a_with_sync( + &mut self.ctx_b, + &mut self.ctx_a, + client_id_on_b, + signer, + ) + } + + /// Creates a connection between the two contexts starting from the first context. + /// Returns the connection identifiers of the created connection ends. + pub fn create_connection_on_a( + &mut self, + client_id_on_a: ClientId, + client_id_on_b: ClientId, + signer: Signer, + ) -> (ConnectionId, ConnectionId) { + TypedRelayerOps::::create_connection_on_a( + &mut self.ctx_a, + &mut self.ctx_b, + client_id_on_a, + client_id_on_b, + signer, + ) + } + + /// Creates a connection between the two contexts starting from the second context. + /// Returns the connection identifiers of the created connection ends. + pub fn create_connection_on_b( + &mut self, + client_id_on_b: ClientId, + client_id_on_a: ClientId, + signer: Signer, + ) -> (ConnectionId, ConnectionId) { + TypedRelayerOps::::create_connection_on_a( + &mut self.ctx_b, + &mut self.ctx_a, + client_id_on_b, + client_id_on_a, + signer, + ) + } + + /// Creates a channel between the two contexts starting from the first context. + /// Returns the channel identifiers of the created channel ends. + pub fn create_channel_on_a( + &mut self, + conn_id_on_a: ConnectionId, + port_id_on_a: PortId, + conn_id_on_b: ConnectionId, + port_id_on_b: PortId, + signer: Signer, + ) -> (ChannelId, ChannelId) { + let client_id_on_a = self + .ctx_a + .ibc_store() + .connection_end(&conn_id_on_a) + .expect("connection exists") + .client_id() + .clone(); + + let client_id_on_b = self + .ctx_b + .ibc_store() + .connection_end(&conn_id_on_b) + .expect("connection exists") + .client_id() + .clone(); + + TypedRelayerOps::::create_channel_on_a( + &mut self.ctx_a, + &mut self.ctx_b, + client_id_on_a, + conn_id_on_a, + port_id_on_a, + client_id_on_b, + conn_id_on_b, + port_id_on_b, + signer, + ) + } + + /// Creates a channel between the two contexts starting from the second context. + /// Returns the channel identifiers of the created channel ends. + pub fn create_channel_on_b( + &mut self, + conn_id_on_b: ConnectionId, + port_id_on_b: PortId, + conn_id_on_a: ConnectionId, + port_id_on_a: PortId, + signer: Signer, + ) -> (ChannelId, ChannelId) { + let client_id_on_b = self + .ctx_b + .ibc_store() + .connection_end(&conn_id_on_b) + .expect("connection exists") + .client_id() + .clone(); + + let client_id_on_a = self + .ctx_a + .ibc_store() + .connection_end(&conn_id_on_a) + .expect("connection exists") + .client_id() + .clone(); + + TypedRelayerOps::::create_channel_on_a( + &mut self.ctx_b, + &mut self.ctx_a, + client_id_on_b, + conn_id_on_b, + port_id_on_b, + client_id_on_a, + conn_id_on_a, + port_id_on_a, + signer, + ) + } + + /// Closes a channel between the two contexts starting from the first context. + pub fn close_channel_on_a( + &mut self, + chan_id_on_a: ChannelId, + port_id_on_a: PortId, + chan_id_on_b: ChannelId, + port_id_on_b: PortId, + signer: Signer, + ) { + let conn_id_on_a = self + .ctx_a + .ibc_store() + .channel_end(&ChannelEndPath::new(&port_id_on_a, &chan_id_on_a)) + .expect("connection exists") + .connection_hops()[0] + .clone(); + + let conn_id_on_b = self + .ctx_b + .ibc_store() + .channel_end(&ChannelEndPath::new(&port_id_on_b, &chan_id_on_b)) + .expect("connection exists") + .connection_hops()[0] + .clone(); + + let client_id_on_a = self + .ctx_a + .ibc_store() + .connection_end(&conn_id_on_a) + .expect("connection exists") + .client_id() + .clone(); + + let client_id_on_b = self + .ctx_b + .ibc_store() + .connection_end(&conn_id_on_b) + .expect("connection exists") + .client_id() + .clone(); + + TypedRelayerOps::::close_channel_on_a( + &mut self.ctx_a, + &mut self.ctx_b, + client_id_on_a, + chan_id_on_a, + port_id_on_a, + client_id_on_b, + chan_id_on_b, + port_id_on_b, + signer, + ) + } + + /// Closes a channel between the two contexts starting from the second context. + pub fn close_channel_on_b( + &mut self, + chan_id_on_b: ChannelId, + port_id_on_b: PortId, + chan_id_on_a: ChannelId, + port_id_on_a: PortId, + signer: Signer, + ) { + let conn_id_on_b = self + .ctx_b + .ibc_store() + .channel_end(&ChannelEndPath::new(&port_id_on_b, &chan_id_on_b)) + .expect("connection exists") + .connection_hops()[0] + .clone(); + + let conn_id_on_a = self + .ctx_a + .ibc_store() + .channel_end(&ChannelEndPath::new(&port_id_on_a, &chan_id_on_a)) + .expect("connection exists") + .connection_hops()[0] + .clone(); + + let client_id_on_b = self + .ctx_b + .ibc_store() + .connection_end(&conn_id_on_b) + .expect("connection exists") + .client_id() + .clone(); + + let client_id_on_a = self + .ctx_a + .ibc_store() + .connection_end(&conn_id_on_a) + .expect("connection exists") + .client_id() + .clone(); + + TypedRelayerOps::::close_channel_on_a( + &mut self.ctx_b, + &mut self.ctx_a, + client_id_on_b, + chan_id_on_b, + port_id_on_b, + client_id_on_a, + chan_id_on_a, + port_id_on_a, + signer, + ) + } + + /// Sends a packet from the first context to the second context. + /// The IBC packet is created by an IBC application on the first context. + pub fn send_packet_on_a(&mut self, packet: Packet, signer: Signer) { + let conn_id_on_a = self + .ctx_a + .ibc_store() + .channel_end(&ChannelEndPath::new( + &packet.port_id_on_a, + &packet.chan_id_on_a, + )) + .expect("connection exists") + .connection_hops()[0] + .clone(); + + let conn_id_on_b = self + .ctx_b + .ibc_store() + .channel_end(&ChannelEndPath::new( + &packet.port_id_on_b, + &packet.chan_id_on_b, + )) + .expect("connection exists") + .connection_hops()[0] + .clone(); + + let client_id_on_a = self + .ctx_a + .ibc_store() + .connection_end(&conn_id_on_a) + .expect("connection exists") + .client_id() + .clone(); + + let client_id_on_b = self + .ctx_b + .ibc_store() + .connection_end(&conn_id_on_b) + .expect("connection exists") + .client_id() + .clone(); + + TypedRelayerOps::::send_packet_on_a( + &mut self.ctx_a, + &mut self.ctx_b, + packet, + client_id_on_a, + client_id_on_b, + signer, + ) } } diff --git a/ibc-testkit/src/relayer/integration.rs b/ibc-testkit/src/relayer/integration.rs new file mode 100644 index 000000000..85df3cd9c --- /dev/null +++ b/ibc-testkit/src/relayer/integration.rs @@ -0,0 +1,170 @@ +use core::str::FromStr; + +use ibc::apps::transfer::handler::send_transfer; +use ibc::apps::transfer::types::msgs::transfer::MsgTransfer; +use ibc::apps::transfer::types::packet::PacketData; +use ibc::apps::transfer::types::PrefixedCoin; +use ibc::core::channel::types::packet::Packet; +use ibc::core::client::context::client_state::ClientStateValidation; +use ibc::core::handler::types::events::IbcEvent; +use ibc::core::host::types::identifiers::{ChannelId, ConnectionId, PortId}; +use ibc::primitives::Timestamp; + +use crate::context::TestContext; +use crate::fixtures::core::signer::dummy_account_id; +use crate::hosts::{HostClientState, TestHost}; +use crate::relayer::context::RelayerContext; +use crate::testapp::ibc::applications::transfer::types::DummyTransferModule; +use crate::testapp::ibc::core::types::DefaultIbcStore; + +/// Integration test for IBC implementation. +/// This test creates clients, connections, channels, and sends packets between two [`TestHost`]s. +/// This uses [`DummyTransferModule`] to simulate the transfer of tokens between two contexts. +pub fn ibc_integration_test() +where + A: TestHost, + B: TestHost, + HostClientState: ClientStateValidation, + HostClientState: ClientStateValidation, +{ + let ctx_a = TestContext::::default(); + let ctx_b = TestContext::::default(); + + let signer = dummy_account_id(); + + let mut relayer = RelayerContext::new(ctx_a, ctx_b); + + // client creation + let client_id_on_a = relayer.create_client_on_a(signer.clone()); + let client_id_on_b = relayer.create_client_on_b(signer.clone()); + + // connection from A to B + let (conn_id_on_a, conn_id_on_b) = relayer.create_connection_on_a( + client_id_on_a.clone(), + client_id_on_b.clone(), + signer.clone(), + ); + + assert_eq!(conn_id_on_a, ConnectionId::new(0)); + assert_eq!(conn_id_on_b, ConnectionId::new(0)); + + // connection from B to A + let (conn_id_on_b, conn_id_on_a) = relayer.create_connection_on_b( + client_id_on_b.clone(), + client_id_on_a.clone(), + signer.clone(), + ); + + assert_eq!(conn_id_on_a, ConnectionId::new(1)); + assert_eq!(conn_id_on_b, ConnectionId::new(1)); + + // channel from A to B + let (chan_id_on_a, chan_id_on_b) = relayer.create_channel_on_a( + conn_id_on_a.clone(), + PortId::transfer(), + conn_id_on_b.clone(), + PortId::transfer(), + signer.clone(), + ); + + assert_eq!(chan_id_on_a, ChannelId::new(0)); + assert_eq!(chan_id_on_b, ChannelId::new(0)); + + // close the channel from A to B + relayer.close_channel_on_a( + chan_id_on_a.clone(), + PortId::transfer(), + chan_id_on_b.clone(), + PortId::transfer(), + signer.clone(), + ); + + // channel from B to A + let (chan_id_on_b, chan_id_on_a) = relayer.create_channel_on_b( + conn_id_on_b, + PortId::transfer(), + conn_id_on_a, + PortId::transfer(), + signer.clone(), + ); + + assert_eq!(chan_id_on_a, ChannelId::new(1)); + assert_eq!(chan_id_on_b, ChannelId::new(1)); + + // send packet from A to B + + // generate packet for DummyTransferModule + let packet_data = PacketData { + token: PrefixedCoin::from_str("1000uibc").expect("valid prefixed coin"), + sender: signer.clone(), + receiver: signer.clone(), + memo: "sample memo".into(), + }; + + // packet with ibc metadata + // either height timeout or timestamp timeout must be set + let msg = MsgTransfer { + port_id_on_a: PortId::transfer(), + chan_id_on_a: chan_id_on_a.clone(), + packet_data, + // setting timeout height to 10 blocks from B's current height. + timeout_height_on_b: relayer.get_ctx_b().latest_height().add(10).into(), + // not setting timeout timestamp. + timeout_timestamp_on_b: Timestamp::none(), + }; + + // module creates the send_packet + send_transfer( + relayer.get_ctx_a_mut().ibc_store_mut(), + &mut DummyTransferModule, + msg, + ) + .expect("successfully created send_packet"); + + // send_packet wasn't committed, hence produce a block + relayer.get_ctx_a_mut().advance_block_height(); + + // retrieve the send_packet event + let Some(IbcEvent::SendPacket(send_packet_event)) = relayer + .get_ctx_a() + .ibc_store() + .events + .lock() + .iter() + .rev() + .nth(2) + .cloned() + else { + panic!("unexpected event") + }; + + // create the IBC packet type + let packet = Packet { + port_id_on_a: send_packet_event.port_id_on_a().clone(), + chan_id_on_a: send_packet_event.chan_id_on_a().clone(), + seq_on_a: *send_packet_event.seq_on_a(), + data: send_packet_event.packet_data().to_vec(), + timeout_height_on_b: *send_packet_event.timeout_height_on_b(), + timeout_timestamp_on_b: *send_packet_event.timeout_timestamp_on_b(), + port_id_on_b: send_packet_event.port_id_on_b().clone(), + chan_id_on_b: send_packet_event.chan_id_on_b().clone(), + }; + + // continue packet relay starting from recv_packet at B + relayer.send_packet_on_a(packet, signer); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hosts::{MockHost, TendermintHost}; + + // tests among all the `TestHost` implementations + #[test] + fn ibc_integration_test_for_all_pairs() { + ibc_integration_test::(); + ibc_integration_test::(); + ibc_integration_test::(); + ibc_integration_test::(); + } +} diff --git a/ibc-testkit/src/relayer/mod.rs b/ibc-testkit/src/relayer/mod.rs index c2c641829..5c70430ef 100644 --- a/ibc-testkit/src/relayer/mod.rs +++ b/ibc-testkit/src/relayer/mod.rs @@ -1,2 +1,7 @@ pub mod context; pub mod error; +pub mod utils; + +// `ibc::apps::transfer::handler::send_transfer` requires `serde` +#[cfg(feature = "serde")] +pub mod integration; diff --git a/ibc-testkit/src/relayer/utils.rs b/ibc-testkit/src/relayer/utils.rs new file mode 100644 index 000000000..9d4c96bd6 --- /dev/null +++ b/ibc-testkit/src/relayer/utils.rs @@ -0,0 +1,1001 @@ +use alloc::string::String; +use core::marker::PhantomData; +use core::time::Duration; + +use ibc::core::channel::types::acknowledgement::Acknowledgement; +use ibc::core::channel::types::channel::Order; +use ibc::core::channel::types::msgs::{ + ChannelMsg, MsgAcknowledgement, MsgChannelCloseConfirm, MsgChannelCloseInit, MsgChannelOpenAck, + MsgChannelOpenConfirm, MsgChannelOpenInit, MsgChannelOpenTry, MsgRecvPacket, MsgTimeout, + MsgTimeoutOnClose, PacketMsg, +}; +use ibc::core::channel::types::packet::Packet; +use ibc::core::channel::types::Version as ChannelVersion; +use ibc::core::client::context::client_state::ClientStateValidation; +use ibc::core::client::context::ClientValidationContext; +use ibc::core::client::types::msgs::{ClientMsg, MsgCreateClient, MsgUpdateClient}; +use ibc::core::connection::types::msgs::{ + ConnectionMsg, MsgConnectionOpenAck, MsgConnectionOpenConfirm, MsgConnectionOpenInit, + MsgConnectionOpenTry, +}; +use ibc::core::connection::types::version::Version as ConnectionVersion; +use ibc::core::connection::types::Counterparty as ConnectionCounterParty; +use ibc::core::handler::types::events::IbcEvent; +use ibc::core::handler::types::msgs::MsgEnvelope; +use ibc::core::host::types::identifiers::{ChannelId, ClientId, ConnectionId, PortId}; +use ibc::core::host::types::path::{ + AckPath, ChannelEndPath, ClientConsensusStatePath, ClientStatePath, CommitmentPath, + ConnectionPath, ReceiptPath, +}; +use ibc::core::host::ValidationContext; +use ibc::primitives::Signer; +use ibc_query::core::context::ProvableContext; + +use crate::context::TestContext; +use crate::hosts::{HostClientState, TestBlock, TestHost}; +use crate::testapp::ibc::core::types::{DefaultIbcStore, LightClientBuilder, LightClientState}; + +/// Implements IBC relayer functions for a pair of [`TestHost`] implementations: `A` and `B`. +/// Note that, all the implementations are in one direction: from `A` to `B`. +/// This ensures that the variable namings are consistent with the IBC message fields, +/// leading to a less error-prone implementation. +/// +/// For the functions in the opposite direction, use `TypedRelayerOps::` instead of TypedRelayerOps::`. +#[derive(Debug, Default)] +pub struct TypedRelayerOps(PhantomData, PhantomData) +where + A: TestHost, + B: TestHost, + HostClientState: ClientStateValidation, + HostClientState: ClientStateValidation; + +impl TypedRelayerOps +where + A: TestHost, + B: TestHost, + HostClientState: ClientStateValidation, + HostClientState: ClientStateValidation, +{ + /// Creates a client on `A` with the state of `B`. + /// Returns the client identifier on `A`. + pub fn create_client_on_a( + ctx_a: &mut TestContext, + ctx_b: &TestContext, + signer: Signer, + ) -> ClientId { + let light_client_of_b = LightClientBuilder::init() + .context(ctx_b) + .build::>(); + + let msg_for_a = MsgEnvelope::Client(ClientMsg::CreateClient(MsgCreateClient { + client_state: light_client_of_b.client_state.into(), + consensus_state: light_client_of_b + .consensus_states + .values() + .next() + .expect("at least one") + .clone() + .into(), + signer, + })); + + ctx_a.deliver(msg_for_a).expect("success"); + + let Some(IbcEvent::CreateClient(create_client_b_event)) = + ctx_a.ibc_store().events.lock().last().cloned() + else { + panic!("unexpected event") + }; + + let client_id_on_a = create_client_b_event.client_id().clone(); + + assert_eq!( + ValidationContext::get_client_validation_context(ctx_a.ibc_store()) + .client_state(&client_id_on_a) + .expect("client state exists") + .latest_height(), + ctx_b.latest_height() + ); + + client_id_on_a + } + + /// Advances the block height on `A` until it catches up with the latest timestamp on `B`. + pub fn sync_clock_on_a(ctx_a: &mut TestContext, ctx_b: &TestContext) { + while ctx_b.latest_timestamp() > ctx_a.latest_timestamp() { + ctx_a.advance_block_height(); + } + } + + /// Updates the client on `A` with the latest header from `B`. + pub fn update_client_on_a( + ctx_a: &mut TestContext, + ctx_b: &TestContext, + client_id_on_a: ClientId, + signer: Signer, + ) { + let trusted_height_of_b = ctx_a + .ibc_store() + .get_client_validation_context() + .client_state(&client_id_on_a) + .expect("client state exists") + .latest_height(); + + let trusted_block_of_b = ctx_b + .host + .get_block(&trusted_height_of_b) + .expect("block exists"); + + let target_height_of_b = ctx_b.latest_height(); + + let target_block_of_b = ctx_b.host_block(&target_height_of_b).expect("block exists"); + + let msg_for_a = MsgEnvelope::Client(ClientMsg::UpdateClient(MsgUpdateClient { + client_id: client_id_on_a.clone(), + client_message: target_block_of_b + .into_header_with_trusted(&trusted_block_of_b) + .into(), + signer, + })); + + ctx_a.deliver(msg_for_a).expect("success"); + + let Some(IbcEvent::UpdateClient(_)) = ctx_a.ibc_store().events.lock().last().cloned() + else { + panic!("unexpected event") + }; + } + + /// Updates the client on `A` with the latest header from `B` after syncing the timestamps. + /// + /// Timestamp sync is required, as IBC doesn't allow client updates from the future beyond max clock drift. + pub fn update_client_on_a_with_sync( + ctx_a: &mut TestContext, + ctx_b: &mut TestContext, + client_id_on_a: ClientId, + signer: Signer, + ) { + TypedRelayerOps::::sync_clock_on_a(ctx_a, ctx_b); + TypedRelayerOps::::update_client_on_a(ctx_a, ctx_b, client_id_on_a, signer); + } + + /// `A` initiates a connection with the other end on `B`. + /// Returns the connection identifier on `A`. + pub fn connection_open_init_on_a( + ctx_a: &mut TestContext, + ctx_b: &TestContext, + client_id_on_a: ClientId, + client_id_on_b: ClientId, + signer: Signer, + ) -> ConnectionId { + let counterparty_b = ConnectionCounterParty::new( + client_id_on_b.clone(), + None, + ctx_b.ibc_store().commitment_prefix(), + ); + + let msg_for_a = MsgEnvelope::Connection(ConnectionMsg::OpenInit(MsgConnectionOpenInit { + client_id_on_a: client_id_on_a.clone(), + counterparty: counterparty_b, + version: None, + delay_period: Duration::from_secs(0), + signer: signer.clone(), + })); + + ctx_a.deliver(msg_for_a).expect("success"); + + let Some(IbcEvent::OpenInitConnection(open_init_connection_event)) = + ctx_a.ibc_store().events.lock().last().cloned() + else { + panic!("unexpected event") + }; + + open_init_connection_event.conn_id_on_a().clone() + } + + /// `B` receives the connection opening attempt by `A` after `A` initiates the connection. + /// Returns the connection identifier on `B`. + pub fn connection_open_try_on_b( + ctx_b: &mut TestContext, + ctx_a: &TestContext, + conn_id_on_a: ConnectionId, + client_id_on_a: ClientId, + client_id_on_b: ClientId, + signer: Signer, + ) -> ConnectionId { + let proofs_height_on_a = ctx_a.latest_height(); + + let client_state_of_b_on_a = ctx_a + .ibc_store() + .client_state(&client_id_on_a) + .expect("client state exists"); + + let consensus_height_of_b_on_a = client_state_of_b_on_a.latest_height(); + + let counterparty_a = ConnectionCounterParty::new( + client_id_on_a.clone(), + Some(conn_id_on_a.clone()), + ctx_a.ibc_store().commitment_prefix(), + ); + + let proof_conn_end_on_a = ctx_a + .ibc_store() + .get_proof( + proofs_height_on_a, + &ConnectionPath::new(&conn_id_on_a).into(), + ) + .expect("connection end exists") + .try_into() + .expect("value merkle proof"); + + let proof_client_state_of_b_on_a = ctx_a + .ibc_store() + .get_proof( + proofs_height_on_a, + &ClientStatePath::new(client_id_on_a.clone()).into(), + ) + .expect("client state exists") + .try_into() + .expect("value merkle proof"); + + let proof_consensus_state_of_b_on_a = ctx_a + .ibc_store() + .get_proof( + proofs_height_on_a, + &ClientConsensusStatePath::new( + client_id_on_a.clone(), + consensus_height_of_b_on_a.revision_number(), + consensus_height_of_b_on_a.revision_height(), + ) + .into(), + ) + .expect("consensus state exists") + .try_into() + .expect("value merkle proof"); + + #[allow(deprecated)] + let msg_for_b = MsgEnvelope::Connection(ConnectionMsg::OpenTry(MsgConnectionOpenTry { + client_id_on_b: client_id_on_b.clone(), + client_state_of_b_on_a: client_state_of_b_on_a.into(), + counterparty: counterparty_a, + versions_on_a: ConnectionVersion::compatibles(), + proof_conn_end_on_a, + proof_client_state_of_b_on_a, + proof_consensus_state_of_b_on_a, + proofs_height_on_a, + consensus_height_of_b_on_a, + delay_period: Duration::from_secs(0), + signer: signer.clone(), + proof_consensus_state_of_b: None, + // deprecated + previous_connection_id: String::new(), + })); + + ctx_b.deliver(msg_for_b).expect("success"); + + let Some(IbcEvent::OpenTryConnection(open_try_connection_event)) = + ctx_b.ibc_store().events.lock().last().cloned() + else { + panic!("unexpected event") + }; + + open_try_connection_event.conn_id_on_b().clone() + } + + /// `A` receives `B`'s acknowledgement that `B` received the connection opening attempt by `A`. + /// `A` starts processing the connection on its side. + pub fn connection_open_ack_on_a( + ctx_a: &mut TestContext, + ctx_b: &TestContext, + conn_id_on_a: ConnectionId, + conn_id_on_b: ConnectionId, + client_id_on_b: ClientId, + signer: Signer, + ) { + let proofs_height_on_b = ctx_b.latest_height(); + + let client_state_of_a_on_b = ctx_b + .ibc_store() + .client_state(&client_id_on_b) + .expect("client state exists"); + + let consensus_height_of_a_on_b = client_state_of_a_on_b.latest_height(); + + let proof_conn_end_on_b = ctx_b + .ibc_store() + .get_proof( + proofs_height_on_b, + &ConnectionPath::new(&conn_id_on_b).into(), + ) + .expect("connection end exists") + .try_into() + .expect("value merkle proof"); + + let proof_client_state_of_a_on_b = ctx_b + .ibc_store() + .get_proof( + proofs_height_on_b, + &ClientStatePath::new(client_id_on_b.clone()).into(), + ) + .expect("client state exists") + .try_into() + .expect("value merkle proof"); + + let proof_consensus_state_of_a_on_b = ctx_b + .ibc_store() + .get_proof( + proofs_height_on_b, + &ClientConsensusStatePath::new( + client_id_on_b.clone(), + consensus_height_of_a_on_b.revision_number(), + consensus_height_of_a_on_b.revision_height(), + ) + .into(), + ) + .expect("consensus state exists") + .try_into() + .expect("value merkle proof"); + + let msg_for_a = MsgEnvelope::Connection(ConnectionMsg::OpenAck(MsgConnectionOpenAck { + conn_id_on_a: conn_id_on_a.clone(), + conn_id_on_b: conn_id_on_b.clone(), + client_state_of_a_on_b: client_state_of_a_on_b.into(), + proof_conn_end_on_b, + proof_client_state_of_a_on_b, + proof_consensus_state_of_a_on_b, + proofs_height_on_b, + consensus_height_of_a_on_b, + version: ConnectionVersion::compatibles()[0].clone(), + signer: signer.clone(), + proof_consensus_state_of_a: None, + })); + + ctx_a.deliver(msg_for_a).expect("success"); + + let Some(IbcEvent::OpenAckConnection(_)) = ctx_a.ibc_store().events.lock().last().cloned() + else { + panic!("unexpected event") + }; + } + + /// `B` receives the confirmation from `A` that the connection creation was successful. + /// `B` also starts processing the connection on its side. + pub fn connection_open_confirm_on_b( + ctx_b: &mut TestContext, + ctx_a: &TestContext, + conn_id_on_a: ConnectionId, + conn_id_on_b: ConnectionId, + signer: Signer, + ) { + let proof_height_on_a = ctx_a.latest_height(); + + let proof_conn_end_on_a = ctx_a + .ibc_store() + .get_proof( + proof_height_on_a, + &ConnectionPath::new(&conn_id_on_a).into(), + ) + .expect("connection end exists") + .try_into() + .expect("value merkle proof"); + + let msg_for_b = + MsgEnvelope::Connection(ConnectionMsg::OpenConfirm(MsgConnectionOpenConfirm { + conn_id_on_b: conn_id_on_b.clone(), + proof_conn_end_on_a, + proof_height_on_a, + signer: signer.clone(), + })); + + ctx_b.deliver(msg_for_b).expect("success"); + + let Some(IbcEvent::OpenConfirmConnection(_)) = ctx_b.ibc_store().events.lock().last() + else { + panic!("unexpected event") + }; + } + + /// A connection is created by `A` towards `B` using the IBC connection handshake protocol. + /// Returns the connection identifiers of `A` and `B`. + pub fn create_connection_on_a( + ctx_a: &mut TestContext, + ctx_b: &mut TestContext, + client_id_on_a: ClientId, + client_id_on_b: ClientId, + signer: Signer, + ) -> (ConnectionId, ConnectionId) { + let conn_id_on_a = TypedRelayerOps::::connection_open_init_on_a( + ctx_a, + ctx_b, + client_id_on_a.clone(), + client_id_on_b.clone(), + signer.clone(), + ); + + TypedRelayerOps::::update_client_on_a_with_sync( + ctx_b, + ctx_a, + client_id_on_b.clone(), + signer.clone(), + ); + + let conn_id_on_b = TypedRelayerOps::::connection_open_try_on_b( + ctx_b, + ctx_a, + conn_id_on_a.clone(), + client_id_on_a.clone(), + client_id_on_b.clone(), + signer.clone(), + ); + + TypedRelayerOps::::update_client_on_a_with_sync( + ctx_a, + ctx_b, + client_id_on_a.clone(), + signer.clone(), + ); + + TypedRelayerOps::::connection_open_ack_on_a( + ctx_a, + ctx_b, + conn_id_on_a.clone(), + conn_id_on_b.clone(), + client_id_on_b.clone(), + signer.clone(), + ); + + TypedRelayerOps::::update_client_on_a_with_sync( + ctx_b, + ctx_a, + client_id_on_b.clone(), + signer.clone(), + ); + + TypedRelayerOps::::connection_open_confirm_on_b( + ctx_b, + ctx_a, + conn_id_on_b.clone(), + conn_id_on_a.clone(), + signer.clone(), + ); + + TypedRelayerOps::::update_client_on_a_with_sync(ctx_a, ctx_b, client_id_on_a, signer); + + (conn_id_on_a, conn_id_on_b) + } + + /// `A` initiates a channel with port identifier with the other end on `B`. + /// Returns the channel identifier of `A`. + pub fn channel_open_init_on_a( + ctx_a: &mut TestContext, + conn_id_on_a: ConnectionId, + port_id_on_a: PortId, + port_id_on_b: PortId, + signer: Signer, + ) -> ChannelId { + let msg_for_a = MsgEnvelope::Channel(ChannelMsg::OpenInit(MsgChannelOpenInit { + port_id_on_a, + connection_hops_on_a: [conn_id_on_a].to_vec(), + port_id_on_b, + ordering: Order::Unordered, + signer, + version_proposal: ChannelVersion::empty(), + })); + + ctx_a.deliver(msg_for_a).expect("success"); + + let Some(IbcEvent::OpenInitChannel(open_init_channel_event)) = + ctx_a.ibc_store().events.lock().last().cloned() + else { + panic!("unexpected event") + }; + + open_init_channel_event.chan_id_on_a().clone() + } + + /// `B` receives the channel opening attempt by `A` after `A` initiates the channel. + /// Returns the channel identifier of `B`. + pub fn channel_open_try_on_b( + ctx_b: &mut TestContext, + ctx_a: &TestContext, + conn_id_on_b: ConnectionId, + chan_id_on_a: ChannelId, + port_id_on_a: PortId, + signer: Signer, + ) -> ChannelId { + let proof_height_on_a = ctx_a.latest_height(); + + let proof_chan_end_on_a = ctx_a + .ibc_store() + .get_proof( + proof_height_on_a, + &ChannelEndPath::new(&port_id_on_a, &chan_id_on_a).into(), + ) + .expect("connection end exists") + .try_into() + .expect("value merkle proof"); + + #[allow(deprecated)] + let msg_for_b = MsgEnvelope::Channel(ChannelMsg::OpenTry(MsgChannelOpenTry { + port_id_on_b: PortId::transfer(), + connection_hops_on_b: [conn_id_on_b].to_vec(), + port_id_on_a: PortId::transfer(), + chan_id_on_a, + version_supported_on_a: ChannelVersion::empty(), + proof_chan_end_on_a, + proof_height_on_a, + ordering: Order::Unordered, + signer, + + version_proposal: ChannelVersion::empty(), + })); + + ctx_b.deliver(msg_for_b).expect("success"); + + let Some(IbcEvent::OpenTryChannel(open_try_channel_event)) = + ctx_b.ibc_store().events.lock().last().cloned() + else { + panic!("unexpected event") + }; + + open_try_channel_event.chan_id_on_b().clone() + } + + /// `A` receives `B`'s acknowledgement that `B` received the channel opening attempt by `A`. + /// `A` starts processing the channel on its side. + pub fn channel_open_ack_on_a( + ctx_a: &mut TestContext, + ctx_b: &TestContext, + chan_id_on_a: ChannelId, + port_id_on_a: PortId, + chan_id_on_b: ChannelId, + port_id_on_b: PortId, + signer: Signer, + ) { + let proof_height_on_b = ctx_b.latest_height(); + + let proof_chan_end_on_b = ctx_b + .ibc_store() + .get_proof( + proof_height_on_b, + &ChannelEndPath::new(&port_id_on_b, &chan_id_on_b).into(), + ) + .expect("connection end exists") + .try_into() + .expect("value merkle proof"); + + let msg_for_a = MsgEnvelope::Channel(ChannelMsg::OpenAck(MsgChannelOpenAck { + port_id_on_a, + chan_id_on_a, + chan_id_on_b, + version_on_b: ChannelVersion::empty(), + proof_chan_end_on_b, + proof_height_on_b, + signer, + })); + + ctx_a.deliver(msg_for_a).expect("success"); + + let Some(IbcEvent::OpenAckChannel(_)) = ctx_a.ibc_store().events.lock().last().cloned() + else { + panic!("unexpected event") + }; + } + + /// `B` receives the confirmation from `A` that the channel creation was successful. + /// `B` also starts processing the channel on its side. + pub fn channel_open_confirm_on_b( + ctx_b: &mut TestContext, + ctx_a: &TestContext, + chan_id_on_a: ChannelId, + chan_id_on_b: ChannelId, + port_id_on_b: PortId, + signer: Signer, + ) { + let proof_height_on_a = ctx_a.latest_height(); + + let proof_chan_end_on_a = ctx_a + .ibc_store() + .get_proof( + proof_height_on_a, + &ChannelEndPath::new(&PortId::transfer(), &chan_id_on_a).into(), + ) + .expect("connection end exists") + .try_into() + .expect("value merkle proof"); + + let msg_for_b = MsgEnvelope::Channel(ChannelMsg::OpenConfirm(MsgChannelOpenConfirm { + port_id_on_b, + chan_id_on_b, + proof_chan_end_on_a, + proof_height_on_a, + signer, + })); + + ctx_b.deliver(msg_for_b).expect("success"); + + let Some(IbcEvent::OpenConfirmChannel(_)) = ctx_b.ibc_store().events.lock().last().cloned() + else { + panic!("unexpected event") + }; + } + + /// `A` initiates the channel closing with the other end on `B`. + /// `A` stops processing the channel. + pub fn channel_close_init_on_a( + ctx_a: &mut TestContext, + chan_id_on_a: ChannelId, + port_id_on_a: PortId, + signer: Signer, + ) { + let msg_for_a = MsgEnvelope::Channel(ChannelMsg::CloseInit(MsgChannelCloseInit { + port_id_on_a, + chan_id_on_a, + signer, + })); + + ctx_a.deliver(msg_for_a).expect("success"); + + let Some(IbcEvent::CloseInitChannel(_)) = ctx_a.ibc_store().events.lock().last().cloned() + else { + panic!("unexpected event") + }; + } + + /// `B` receives the channel closing attempt by `A` after `A` initiates the channel closing. + /// `B` also stops processing the channel. + pub fn channel_close_confirm_on_b( + ctx_b: &mut TestContext, + ctx_a: &TestContext, + chan_id_on_b: ChannelId, + port_id_on_b: PortId, + signer: Signer, + ) { + let proof_height_on_a = ctx_a.latest_height(); + + let proof_chan_end_on_a = ctx_a + .ibc_store() + .get_proof( + proof_height_on_a, + &ChannelEndPath::new(&PortId::transfer(), &chan_id_on_b).into(), + ) + .expect("connection end exists") + .try_into() + .expect("value merkle proof"); + + let msg_for_b = MsgEnvelope::Channel(ChannelMsg::CloseConfirm(MsgChannelCloseConfirm { + port_id_on_b, + chan_id_on_b, + proof_chan_end_on_a, + proof_height_on_a, + signer, + })); + + ctx_b.deliver(msg_for_b).expect("success"); + + let Some(IbcEvent::CloseConfirmChannel(_)) = + ctx_b.ibc_store().events.lock().last().cloned() + else { + panic!("unexpected event") + }; + } + + /// A channel is created by `A` towards `B` using the IBC channel handshake protocol. + /// Returns the channel identifiers of `A` and `B`. + #[allow(clippy::too_many_arguments)] + pub fn create_channel_on_a( + ctx_a: &mut TestContext, + ctx_b: &mut TestContext, + client_id_on_a: ClientId, + conn_id_on_a: ConnectionId, + port_id_on_a: PortId, + client_id_on_b: ClientId, + conn_id_on_b: ConnectionId, + port_id_on_b: PortId, + signer: Signer, + ) -> (ChannelId, ChannelId) { + let chan_id_on_a = TypedRelayerOps::::channel_open_init_on_a( + ctx_a, + conn_id_on_a.clone(), + port_id_on_a.clone(), + port_id_on_b.clone(), + signer.clone(), + ); + + TypedRelayerOps::::update_client_on_a_with_sync( + ctx_b, + ctx_a, + client_id_on_b.clone(), + signer.clone(), + ); + + let chan_id_on_b = TypedRelayerOps::::channel_open_try_on_b( + ctx_b, + ctx_a, + conn_id_on_b.clone(), + chan_id_on_a.clone(), + port_id_on_a.clone(), + signer.clone(), + ); + + TypedRelayerOps::::update_client_on_a_with_sync( + ctx_a, + ctx_b, + client_id_on_a.clone(), + signer.clone(), + ); + + TypedRelayerOps::::channel_open_ack_on_a( + ctx_a, + ctx_b, + chan_id_on_a.clone(), + port_id_on_a.clone(), + chan_id_on_b.clone(), + port_id_on_b.clone(), + signer.clone(), + ); + + TypedRelayerOps::::update_client_on_a_with_sync( + ctx_b, + ctx_a, + client_id_on_b.clone(), + signer.clone(), + ); + + TypedRelayerOps::::channel_open_confirm_on_b( + ctx_b, + ctx_a, + chan_id_on_a.clone(), + chan_id_on_b.clone(), + port_id_on_b, + signer.clone(), + ); + + TypedRelayerOps::::update_client_on_a_with_sync(ctx_a, ctx_b, client_id_on_a, signer); + + (chan_id_on_a, chan_id_on_b) + } + + /// A channel is closed by `A` towards `B` using the IBC channel handshake protocol. + #[allow(clippy::too_many_arguments)] + pub fn close_channel_on_a( + ctx_a: &mut TestContext, + ctx_b: &mut TestContext, + client_id_on_a: ClientId, + chan_id_on_a: ChannelId, + port_id_on_a: PortId, + client_id_on_b: ClientId, + chan_id_on_b: ChannelId, + port_id_on_b: PortId, + signer: Signer, + ) { + TypedRelayerOps::::channel_close_init_on_a( + ctx_a, + chan_id_on_a.clone(), + port_id_on_a.clone(), + signer.clone(), + ); + + TypedRelayerOps::::update_client_on_a_with_sync( + ctx_b, + ctx_a, + client_id_on_b, + signer.clone(), + ); + + TypedRelayerOps::::channel_close_confirm_on_b( + ctx_b, + ctx_a, + chan_id_on_b, + port_id_on_b, + signer.clone(), + ); + + TypedRelayerOps::::update_client_on_a_with_sync(ctx_a, ctx_b, client_id_on_a, signer); + } + + /// `B` receives a packet from an IBC module on `A`. + /// Returns `B`'s acknowledgement of receipt. + pub fn packet_recv_on_b( + ctx_b: &mut TestContext, + ctx_a: &TestContext, + packet: Packet, + signer: Signer, + ) -> Acknowledgement { + let proof_height_on_a = ctx_a.latest_height(); + + let proof_commitment_on_a = ctx_a + .ibc_store() + .get_proof( + proof_height_on_a, + &CommitmentPath::new(&packet.port_id_on_a, &packet.chan_id_on_a, packet.seq_on_a) + .into(), + ) + .expect("commitment proof exists") + .try_into() + .expect("value merkle proof"); + + let msg_for_b = MsgEnvelope::Packet(PacketMsg::Recv(MsgRecvPacket { + packet, + proof_commitment_on_a, + proof_height_on_a, + signer, + })); + + ctx_b.deliver(msg_for_b).expect("success"); + + let Some(IbcEvent::WriteAcknowledgement(write_ack_event)) = + ctx_b.ibc_store().events.lock().last().cloned() + else { + panic!("unexpected event") + }; + + write_ack_event.acknowledgement().clone() + } + + /// `A` receives the acknowledgement from `B` that `B` received the packet from `A`. + pub fn packet_ack_on_a( + ctx_a: &mut TestContext, + ctx_b: &TestContext, + packet: Packet, + acknowledgement: Acknowledgement, + signer: Signer, + ) { + let proof_height_on_b = ctx_b.latest_height(); + + let proof_acked_on_b = ctx_b + .ibc_store() + .get_proof( + proof_height_on_b, + &AckPath::new(&packet.port_id_on_b, &packet.chan_id_on_b, packet.seq_on_a).into(), + ) + .expect("acknowledgement proof exists") + .try_into() + .expect("value merkle proof"); + + let msg_for_a = MsgEnvelope::Packet(PacketMsg::Ack(MsgAcknowledgement { + packet, + acknowledgement, + proof_acked_on_b, + proof_height_on_b, + signer, + })); + + ctx_a.deliver(msg_for_a).expect("success"); + + let Some(IbcEvent::AcknowledgePacket(_)) = ctx_a.ibc_store().events.lock().last().cloned() + else { + panic!("unexpected event") + }; + } + + /// `A` receives the timeout packet from `B`. + /// That is, `B` has not received the packet from `A` within the timeout period. + pub fn packet_timeout_on_a( + ctx_a: &mut TestContext, + ctx_b: &TestContext, + packet: Packet, + signer: Signer, + ) { + let proof_height_on_b = ctx_b.latest_height(); + + let proof_unreceived_on_b = ctx_b + .ibc_store() + .get_proof( + proof_height_on_b, + &ReceiptPath::new(&packet.port_id_on_b, &packet.chan_id_on_b, packet.seq_on_a) + .into(), + ) + .expect("non-membership receipt proof exists") + .try_into() + .expect("value merkle proof"); + + let msg_for_a = MsgEnvelope::Packet(PacketMsg::Timeout(MsgTimeout { + next_seq_recv_on_b: packet.seq_on_a, + packet, + proof_unreceived_on_b, + proof_height_on_b, + signer, + })); + + ctx_a.deliver(msg_for_a).expect("success"); + + let Some(IbcEvent::TimeoutPacket(_)) = ctx_a.ibc_store().events.lock().last().cloned() + else { + panic!("unexpected event") + }; + } + + /// `A` receives the timeout packet from `B` after closing the channel. + /// That is, `B` has not received the packet from `A` because the channel is closed. + pub fn packet_timeout_on_close_on_a( + ctx_a: &mut TestContext, + ctx_b: &TestContext, + packet: Packet, + chan_id_on_b: ChannelId, + port_id_on_b: PortId, + signer: Signer, + ) { + let proof_height_on_b = ctx_b.latest_height(); + + let proof_unreceived_on_b = ctx_b + .ibc_store() + .get_proof( + proof_height_on_b, + &ReceiptPath::new(&port_id_on_b, &chan_id_on_b, packet.seq_on_a).into(), + ) + .expect("non-membership receipt proof") + .try_into() + .expect("value merkle proof"); + + let proof_close_on_b = ctx_b + .ibc_store() + .get_proof( + proof_height_on_b, + &ChannelEndPath::new(&port_id_on_b, &chan_id_on_b).into(), + ) + .expect("channel end data exists") + .try_into() + .expect("value merkle proof"); + + let msg_for_a = MsgEnvelope::Packet(PacketMsg::TimeoutOnClose(MsgTimeoutOnClose { + next_seq_recv_on_b: packet.seq_on_a, + packet, + proof_unreceived_on_b, + proof_close_on_b, + proof_height_on_b, + signer, + })); + + ctx_a.deliver(msg_for_a).expect("success"); + + let Some(IbcEvent::ChannelClosed(_)) = ctx_a.ibc_store().events.lock().last().cloned() + else { + panic!("unexpected event") + }; + } + + /// Sends a packet from an IBC application on `A` to `B` using the IBC packet relay protocol. + pub fn send_packet_on_a( + ctx_a: &mut TestContext, + ctx_b: &mut TestContext, + packet: Packet, + client_id_on_a: ClientId, + client_id_on_b: ClientId, + signer: Signer, + ) { + // packet is passed from module + + TypedRelayerOps::::update_client_on_a_with_sync( + ctx_b, + ctx_a, + client_id_on_b.clone(), + signer.clone(), + ); + + let acknowledgement = + TypedRelayerOps::::packet_recv_on_b(ctx_b, ctx_a, packet.clone(), signer.clone()); + + TypedRelayerOps::::update_client_on_a_with_sync( + ctx_a, + ctx_b, + client_id_on_a, + signer.clone(), + ); + + TypedRelayerOps::::packet_ack_on_a( + ctx_a, + ctx_b, + packet, + acknowledgement, + signer.clone(), + ); + + TypedRelayerOps::::update_client_on_a_with_sync( + ctx_b, + ctx_a, + client_id_on_b, + signer.clone(), + ); + } +} diff --git a/ibc-testkit/src/testapp/ibc/clients/mock/client_state.rs b/ibc-testkit/src/testapp/ibc/clients/mock/client_state.rs index c46c81343..4352ac22f 100644 --- a/ibc-testkit/src/testapp/ibc/clients/mock/client_state.rs +++ b/ibc-testkit/src/testapp/ibc/clients/mock/client_state.rs @@ -44,7 +44,7 @@ impl MockClientState { pub fn new(header: MockHeader) -> Self { Self { header, - trusting_period: Duration::from_secs(10), + trusting_period: Duration::from_secs(64000), frozen: false, } } @@ -108,13 +108,13 @@ impl TryFrom for MockClientState { impl From for RawMockClientState { fn from(value: MockClientState) -> Self { - RawMockClientState { + Self { header: Some(value.header.into()), trusting_period: value .trusting_period .as_nanos() .try_into() - .expect("no error"), + .expect("no overflow"), frozen: value.frozen, } } @@ -144,23 +144,13 @@ impl TryFrom for MockClientState { impl From for Any { fn from(client_state: MockClientState) -> Self { - Any { + Self { type_url: MOCK_CLIENT_STATE_TYPE_URL.to_string(), value: Protobuf::::encode_vec(client_state), } } } -pub trait ConsensusStateConverter: - TryInto + From -{ -} - -impl ConsensusStateConverter for C where - C: TryInto + From -{ -} - pub trait MockClientContext { /// Returns the current timestamp of the local chain. fn host_timestamp(&self) -> Result; @@ -202,7 +192,7 @@ impl ClientStateCommon for MockClientState { _proof_upgrade_consensus_state: CommitmentProofBytes, _root: &CommitmentRoot, ) -> Result<(), ClientError> { - let upgraded_mock_client_state = MockClientState::try_from(upgraded_client_state)?; + let upgraded_mock_client_state = Self::try_from(upgraded_client_state)?; MockConsensusState::try_from(upgraded_consensus_state)?; if self.latest_height() >= upgraded_mock_client_state.latest_height() { return Err(UpgradeClientError::LowUpgradeHeight { @@ -238,7 +228,8 @@ impl ClientStateCommon for MockClientState { impl ClientStateValidation for MockClientState where V: ClientValidationContext + MockClientContext, - V::ConsensusStateRef: ConsensusStateConverter, + MockConsensusState: Convertible, + >::Error: Into, { fn verify_client_message( &self, @@ -288,13 +279,13 @@ where return Ok(Status::Frozen); } - let latest_consensus_state = { + let latest_consensus_state: MockConsensusState = { match ctx.consensus_state(&ClientConsensusStatePath::new( client_id.clone(), self.latest_height().revision_number(), self.latest_height().revision_height(), )) { - Ok(cs) => cs.try_into()?, + Ok(cs) => cs.try_into().map_err(Into::into)?, // if the client state does not have an associated consensus state for its latest height // then it must be expired Err(_) => return Ok(Status::Expired), @@ -323,8 +314,9 @@ where impl ClientStateExecution for MockClientState where E: ClientExecutionContext + MockClientContext, - E::ClientStateRef: From, - E::ConsensusStateRef: ConsensusStateConverter, + E::ClientStateRef: From, + MockConsensusState: Convertible, + >::Error: Into, { fn initialise( &self, @@ -332,7 +324,7 @@ where client_id: &ClientId, consensus_state: Any, ) -> Result<(), ClientError> { - let mock_consensus_state = MockConsensusState::try_from(consensus_state)?; + let mock_consensus_state: MockConsensusState = consensus_state.try_into()?; ctx.store_client_state(ClientStatePath::new(client_id.clone()), (*self).into())?; ctx.store_consensus_state( @@ -362,7 +354,7 @@ where let header = MockHeader::try_from(header)?; let header_height = header.height; - let new_client_state = MockClientState::new(header); + let new_client_state = Self::new(header); let new_consensus_state = MockConsensusState::new(header); ctx.store_consensus_state( @@ -410,8 +402,8 @@ where upgraded_client_state: Any, upgraded_consensus_state: Any, ) -> Result { - let new_client_state = MockClientState::try_from(upgraded_client_state)?; - let new_consensus_state = MockConsensusState::try_from(upgraded_consensus_state)?; + let new_client_state = Self::try_from(upgraded_client_state)?; + let new_consensus_state: MockConsensusState = upgraded_consensus_state.try_into()?; let latest_height = new_client_state.latest_height(); @@ -460,7 +452,7 @@ where let host_timestamp = ctx.host_timestamp()?; let host_height = ctx.host_height()?; - let mock_consensus_state = MockConsensusState::try_from(substitute_consensus_state)?; + let mock_consensus_state: MockConsensusState = substitute_consensus_state.try_into()?; ctx.store_consensus_state( ClientConsensusStatePath::new( @@ -503,7 +495,7 @@ mod test { use super::{MockClientState, MockHeader}; let client_state = MockClientState::new(MockHeader::default()); - let expected = r#"{"typeUrl":"/ibc.mock.ClientState","value":"CgQKAhABEIDIr6Al"}"#; + let expected = r#"{"typeUrl":"/ibc.mock.ClientState","value":"CgQKAhABEICAkMrSxg4="}"#; let json = serde_json::to_string(&Any::from(client_state)).unwrap(); assert_eq!(json, expected); diff --git a/ibc-testkit/src/testapp/ibc/clients/mock/consensus_state.rs b/ibc-testkit/src/testapp/ibc/clients/mock/consensus_state.rs index dec5e9431..c637d26e5 100644 --- a/ibc-testkit/src/testapp/ibc/clients/mock/consensus_state.rs +++ b/ibc-testkit/src/testapp/ibc/clients/mock/consensus_state.rs @@ -6,9 +6,7 @@ use ibc::core::primitives::Timestamp; use ibc::primitives::proto::{Any, Protobuf}; use crate::testapp::ibc::clients::mock::header::MockHeader; -use crate::testapp::ibc::clients::mock::proto::{ - ConsensusState as RawMockConsensusState, Header as RawMockHeader, -}; +use crate::testapp::ibc::clients::mock::proto::ConsensusState as RawMockConsensusState; pub const MOCK_CONSENSUS_STATE_TYPE_URL: &str = "/ibc.mock.ConsensusState"; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -20,7 +18,7 @@ pub struct MockConsensusState { impl MockConsensusState { pub fn new(header: MockHeader) -> Self { - MockConsensusState { + Self { header, root: CommitmentRoot::from(vec![0]), } @@ -40,7 +38,7 @@ impl TryFrom for MockConsensusState { let raw_header = raw.header.ok_or(ClientError::MissingRawConsensusState)?; Ok(Self { - header: MockHeader::try_from(raw_header)?, + header: raw_header.try_into()?, root: CommitmentRoot::from(vec![0]), }) } @@ -48,11 +46,8 @@ impl TryFrom for MockConsensusState { impl From for RawMockConsensusState { fn from(value: MockConsensusState) -> Self { - RawMockConsensusState { - header: Some(RawMockHeader { - height: Some(value.header.height().into()), - timestamp: value.header.timestamp.nanoseconds(), - }), + Self { + header: Some(value.header.into()), } } } @@ -83,7 +78,7 @@ impl TryFrom for MockConsensusState { impl From for Any { fn from(consensus_state: MockConsensusState) -> Self { - Any { + Self { type_url: MOCK_CONSENSUS_STATE_TYPE_URL.to_string(), value: Protobuf::::encode_vec(consensus_state), } diff --git a/ibc-testkit/src/testapp/ibc/clients/mock/header.rs b/ibc-testkit/src/testapp/ibc/clients/mock/header.rs index a861ac311..804db7ab0 100644 --- a/ibc-testkit/src/testapp/ibc/clients/mock/header.rs +++ b/ibc-testkit/src/testapp/ibc/clients/mock/header.rs @@ -42,21 +42,25 @@ impl TryFrom for MockHeader { type Error = ClientError; fn try_from(raw: RawMockHeader) -> Result { - Ok(MockHeader { + Ok(Self { height: raw .height - .and_then(|raw_height| raw_height.try_into().ok()) - .ok_or(ClientError::MissingClientMessage)?, - - timestamp: Timestamp::from_nanoseconds(raw.timestamp) - .map_err(ClientError::InvalidPacketTimestamp)?, + .ok_or(ClientError::Other { + description: "missing height".into(), + })? + .try_into()?, + timestamp: Timestamp::from_nanoseconds(raw.timestamp).map_err(|err| { + ClientError::Other { + description: err.to_string(), + } + })?, }) } } impl From for RawMockHeader { fn from(value: MockHeader) -> Self { - RawMockHeader { + Self { height: Some(value.height.into()), timestamp: value.timestamp.nanoseconds(), } @@ -108,7 +112,7 @@ impl TryFrom for MockHeader { impl From for Any { fn from(header: MockHeader) -> Self { - Any { + Self { type_url: MOCK_HEADER_TYPE_URL.to_string(), value: Protobuf::::encode_vec(header), } diff --git a/ibc-testkit/src/testapp/ibc/clients/mock/misbehaviour.rs b/ibc-testkit/src/testapp/ibc/clients/mock/misbehaviour.rs index 15ea17a40..ff4664da4 100644 --- a/ibc-testkit/src/testapp/ibc/clients/mock/misbehaviour.rs +++ b/ibc-testkit/src/testapp/ibc/clients/mock/misbehaviour.rs @@ -38,7 +38,7 @@ impl TryFrom for Misbehaviour { impl From for RawMisbehaviour { fn from(value: Misbehaviour) -> Self { - RawMisbehaviour { + Self { client_id: value.client_id.to_string(), header1: Some(value.header1.into()), header2: Some(value.header2.into()), @@ -70,7 +70,7 @@ impl TryFrom for Misbehaviour { impl From for Any { fn from(misbehaviour: Misbehaviour) -> Self { - Any { + Self { type_url: MOCK_MISBEHAVIOUR_TYPE_URL.to_string(), value: Protobuf::::encode_vec(misbehaviour), } diff --git a/ibc-testkit/src/testapp/ibc/clients/mod.rs b/ibc-testkit/src/testapp/ibc/clients/mod.rs index 1f7e364c6..05fe539da 100644 --- a/ibc-testkit/src/testapp/ibc/clients/mod.rs +++ b/ibc-testkit/src/testapp/ibc/clients/mod.rs @@ -1,5 +1,8 @@ pub mod mock; +use alloc::fmt::Debug; + +use basecoin_store::context::ProvableStore; use derive_more::From; use ibc::clients::tendermint::client_state::ClientState as TmClientState; use ibc::clients::tendermint::consensus_state::ConsensusState as TmConsensusState; @@ -8,26 +11,43 @@ use ibc::clients::tendermint::types::{ TENDERMINT_CLIENT_STATE_TYPE_URL, TENDERMINT_CONSENSUS_STATE_TYPE_URL, }; use ibc::core::client::types::error::ClientError; +use ibc::core::client::types::Height; use ibc::core::primitives::prelude::*; use ibc::derive::{ClientState, ConsensusState}; use ibc::primitives::proto::{Any, Protobuf}; +use super::core::types::MockIbcStore; use crate::testapp::ibc::clients::mock::client_state::{ MockClientState, MOCK_CLIENT_STATE_TYPE_URL, }; use crate::testapp::ibc::clients::mock::consensus_state::{ MockConsensusState, MOCK_CONSENSUS_STATE_TYPE_URL, }; -use crate::testapp::ibc::core::types::MockContext; #[derive(Debug, Clone, From, PartialEq, ClientState)] -#[validation(MockContext)] -#[execution(MockContext)] +#[validation(MockIbcStore)] +#[execution(MockIbcStore)] pub enum AnyClientState { Tendermint(TmClientState), Mock(MockClientState), } +impl AnyClientState { + pub fn latest_height(&self) -> Height { + match self { + Self::Tendermint(cs) => cs.inner().latest_height, + Self::Mock(cs) => cs.latest_height(), + } + } + + pub fn is_frozen(&self) -> bool { + match self { + Self::Tendermint(cs) => cs.inner().is_frozen(), + Self::Mock(cs) => cs.is_frozen(), + } + } +} + impl Protobuf for AnyClientState {} impl TryFrom for AnyClientState { @@ -57,13 +77,13 @@ impl From for Any { impl From for AnyClientState { fn from(client_state: ClientStateType) -> Self { - AnyClientState::Tendermint(client_state.into()) + Self::Tendermint(client_state.into()) } } impl From for AnyConsensusState { fn from(consensus_state: ConsensusStateType) -> Self { - AnyConsensusState::Tendermint(consensus_state.into()) + Self::Tendermint(consensus_state.into()) } } @@ -73,8 +93,6 @@ pub enum AnyConsensusState { Mock(MockConsensusState), } -impl Protobuf for AnyConsensusState {} - impl TryFrom for AnyConsensusState { type Error = ClientError; diff --git a/ibc-testkit/src/testapp/ibc/core/client_ctx.rs b/ibc-testkit/src/testapp/ibc/core/client_ctx.rs index cc7c29b30..8c7bd8dd2 100644 --- a/ibc-testkit/src/testapp/ibc/core/client_ctx.rs +++ b/ibc-testkit/src/testapp/ibc/core/client_ctx.rs @@ -1,3 +1,7 @@ +use core::fmt::Debug; + +use basecoin_store::context::{ProvableStore, Store}; +use basecoin_store::types::Height as StoreHeight; use ibc::core::client::context::{ ClientExecutionContext, ClientValidationContext, ExtClientValidationContext, }; @@ -5,14 +9,16 @@ use ibc::core::client::types::error::ClientError; use ibc::core::client::types::Height; use ibc::core::handler::types::error::ContextError; use ibc::core::host::types::identifiers::{ChannelId, ClientId, PortId}; -use ibc::core::host::types::path::{ClientConsensusStatePath, ClientStatePath}; +use ibc::core::host::types::path::{ + ClientConsensusStatePath, ClientStatePath, ClientUpdateHeightPath, ClientUpdateTimePath, Path, +}; use ibc::core::host::ValidationContext; use ibc::core::primitives::Timestamp; use ibc::primitives::prelude::*; +use super::types::MockIbcStore; use crate::testapp::ibc::clients::mock::client_state::MockClientContext; use crate::testapp::ibc::clients::{AnyClientState, AnyConsensusState}; -use crate::testapp::ibc::core::types::MockContext; pub type PortChannelIdMap = BTreeMap>; @@ -27,7 +33,10 @@ pub struct MockClientRecord { pub consensus_states: BTreeMap, } -impl MockClientContext for MockContext { +impl MockClientContext for MockIbcStore +where + S: ProvableStore + Debug, +{ fn host_timestamp(&self) -> Result { ValidationContext::host_timestamp(self) } @@ -37,7 +46,10 @@ impl MockClientContext for MockContext { } } -impl ExtClientValidationContext for MockContext { +impl ExtClientValidationContext for MockIbcStore +where + S: ProvableStore + Debug, +{ fn host_timestamp(&self) -> Result { ValidationContext::host_timestamp(self) } @@ -46,19 +58,31 @@ impl ExtClientValidationContext for MockContext { ValidationContext::host_height(self) } + /// Returns the list of heights at which the consensus state of the given client was updated. fn consensus_state_heights(&self, client_id: &ClientId) -> Result, ContextError> { - let ibc_store = self.ibc_store.lock(); - let client_record = - ibc_store - .clients - .get(client_id) - .ok_or_else(|| ClientError::ClientStateNotFound { - client_id: client_id.clone(), - })?; - - let heights = client_record.consensus_states.keys().copied().collect(); - - Ok(heights) + let path = format!("clients/{}/consensusStates", client_id) + .try_into() + .map_err(|_| ClientError::Other { + description: "Invalid consensus state path".into(), + })?; + + self.consensus_state_store + .get_keys(&path) + .into_iter() + .filter_map(|path| { + if let Ok(Path::ClientConsensusState(consensus_path)) = path.try_into() { + Some(consensus_path) + } else { + None + } + }) + .map(|consensus_path| { + Ok(Height::new( + consensus_path.revision_number, + consensus_path.revision_height, + )?) + }) + .collect::, _>>() } fn next_consensus_state( @@ -66,33 +90,32 @@ impl ExtClientValidationContext for MockContext { client_id: &ClientId, height: &Height, ) -> Result, ContextError> { - let ibc_store = self.ibc_store.lock(); - let client_record = - ibc_store - .clients - .get(client_id) - .ok_or_else(|| ClientError::ClientStateNotFound { - client_id: client_id.clone(), - })?; - - // Get the consensus state heights and sort them in ascending order. - let mut heights: Vec = client_record.consensus_states.keys().copied().collect(); - heights.sort(); - - // Search for next state. - for h in heights { - if h > *height { - // unwrap should never happen, as the consensus state for h must exist - return Ok(Some( - client_record - .consensus_states - .get(&h) - .expect("Never fails") - .clone(), - )); + let path = format!("clients/{client_id}/consensusStates").into(); + + let keys = self.store.get_keys(&path); + let found_path = keys.into_iter().find_map(|path| { + if let Ok(Path::ClientConsensusState(path)) = path.try_into() { + if height + < &Height::new(path.revision_number, path.revision_height).expect("no error") + { + return Some(path); + } } - } - Ok(None) + None + }); + + let consensus_state = found_path + .map(|path| { + self.consensus_state_store + .get(StoreHeight::Pending, &path) + .ok_or_else(|| ClientError::ConsensusStateNotFound { + client_id: client_id.clone(), + height: *height, + }) + }) + .transpose()?; + + Ok(consensus_state) } fn prev_consensus_state( @@ -100,150 +123,139 @@ impl ExtClientValidationContext for MockContext { client_id: &ClientId, height: &Height, ) -> Result, ContextError> { - let ibc_store = self.ibc_store.lock(); - let client_record = - ibc_store - .clients - .get(client_id) - .ok_or_else(|| ClientError::ClientStateNotFound { - client_id: client_id.clone(), - })?; - - // Get the consensus state heights and sort them in descending order. - let mut heights: Vec = client_record.consensus_states.keys().copied().collect(); - heights.sort_by(|a, b| b.cmp(a)); - - // Search for previous state. - for h in heights { - if h < *height { - // unwrap should never happen, as the consensus state for h must exist - return Ok(Some( - client_record - .consensus_states - .get(&h) - .expect("Never fails") - .clone(), - )); + let path = format!("clients/{client_id}/consensusStates").into(); + + let keys = self.store.get_keys(&path); + let found_path = keys.into_iter().rev().find_map(|path| { + if let Ok(Path::ClientConsensusState(path)) = path.try_into() { + if height + > &Height::new(path.revision_number, path.revision_height).expect("no error") + { + return Some(path); + } } - } - Ok(None) + None + }); + + let consensus_state = found_path + .map(|path| { + self.consensus_state_store + .get(StoreHeight::Pending, &path) + .ok_or_else(|| ClientError::ConsensusStateNotFound { + client_id: client_id.clone(), + height: *height, + }) + }) + .transpose()?; + + Ok(consensus_state) } } -impl ClientValidationContext for MockContext { +impl ClientValidationContext for MockIbcStore +where + S: ProvableStore + Debug, +{ type ClientStateRef = AnyClientState; type ConsensusStateRef = AnyConsensusState; fn client_state(&self, client_id: &ClientId) -> Result { - match self.ibc_store.lock().clients.get(client_id) { - Some(client_record) => { - client_record - .client_state - .clone() - .ok_or_else(|| ClientError::ClientStateNotFound { - client_id: client_id.clone(), - }) - } - None => Err(ClientError::ClientStateNotFound { + Ok(self + .client_state_store + .get(StoreHeight::Pending, &ClientStatePath(client_id.clone())) + .ok_or(ClientError::ClientStateNotFound { client_id: client_id.clone(), - }), - } - .map_err(ContextError::ClientError) + })?) } fn consensus_state( &self, client_cons_state_path: &ClientConsensusStatePath, ) -> Result { - let client_id = &client_cons_state_path.client_id; let height = Height::new( client_cons_state_path.revision_number, client_cons_state_path.revision_height, - )?; - match self.ibc_store.lock().clients.get(client_id) { - Some(client_record) => match client_record.consensus_states.get(&height) { - Some(consensus_state) => Ok(consensus_state.clone()), - None => Err(ClientError::ConsensusStateNotFound { - client_id: client_id.clone(), - height, - }), - }, - None => Err(ClientError::ConsensusStateNotFound { - client_id: client_id.clone(), + ) + .map_err(|_| ClientError::InvalidHeight)?; + let consensus_state = self + .consensus_state_store + .get(StoreHeight::Pending, client_cons_state_path) + .ok_or(ClientError::ConsensusStateNotFound { + client_id: client_cons_state_path.client_id.clone(), height, - }), - } - .map_err(ContextError::ClientError) + })?; + + Ok(consensus_state) } + /// Returns the time and height when the client state for the given + /// [`ClientId`] was updated with a header for the given [`Height`] fn client_update_meta( &self, client_id: &ClientId, height: &Height, ) -> Result<(Timestamp, Height), ContextError> { - let key = (client_id.clone(), *height); - (|| { - let ibc_store = self.ibc_store.lock(); - let time = ibc_store.client_processed_times.get(&key)?; - let height = ibc_store.client_processed_heights.get(&key)?; - Some((*time, *height)) - })() - .ok_or(ClientError::UpdateMetaDataNotFound { - client_id: key.0, - height: key.1, - }) - .map_err(ContextError::from) + let client_update_time_path = ClientUpdateTimePath::new( + client_id.clone(), + height.revision_number(), + height.revision_height(), + ); + let processed_timestamp = self + .client_processed_times + .get(StoreHeight::Pending, &client_update_time_path) + .ok_or(ClientError::UpdateMetaDataNotFound { + client_id: client_id.clone(), + height: *height, + })?; + let client_update_height_path = ClientUpdateHeightPath::new( + client_id.clone(), + height.revision_number(), + height.revision_height(), + ); + let processed_height = self + .client_processed_heights + .get(StoreHeight::Pending, &client_update_height_path) + .ok_or(ClientError::UpdateMetaDataNotFound { + client_id: client_id.clone(), + height: *height, + })?; + + Ok((processed_timestamp, processed_height)) } } -impl ClientExecutionContext for MockContext { +impl ClientExecutionContext for MockIbcStore +where + S: ProvableStore + Debug, +{ type ClientStateMut = AnyClientState; + /// Called upon successful client creation and update fn store_client_state( &mut self, client_state_path: ClientStatePath, client_state: Self::ClientStateRef, ) -> Result<(), ContextError> { - let mut ibc_store = self.ibc_store.lock(); - - let client_id = client_state_path.0; - let client_record = ibc_store - .clients - .entry(client_id) - .or_insert(MockClientRecord { - consensus_states: Default::default(), - client_state: Default::default(), - }); - - client_record.client_state = Some(client_state); + self.client_state_store + .set(client_state_path, client_state) + .map_err(|_| ClientError::Other { + description: "Client state store error".to_string(), + })?; Ok(()) } + /// Called upon successful client creation and update fn store_consensus_state( &mut self, consensus_state_path: ClientConsensusStatePath, consensus_state: Self::ConsensusStateRef, ) -> Result<(), ContextError> { - let mut ibc_store = self.ibc_store.lock(); - - let client_record = ibc_store - .clients - .entry(consensus_state_path.client_id) - .or_insert(MockClientRecord { - consensus_states: Default::default(), - client_state: Default::default(), - }); - - let height = Height::new( - consensus_state_path.revision_number, - consensus_state_path.revision_height, - ) - .expect("Never fails"); - client_record - .consensus_states - .insert(height, consensus_state); - + self.consensus_state_store + .set(consensus_state_path, consensus_state) + .map_err(|_| ClientError::Other { + description: "Consensus state store error".to_string(), + })?; Ok(()) } @@ -251,39 +263,36 @@ impl ClientExecutionContext for MockContext { &mut self, consensus_state_path: ClientConsensusStatePath, ) -> Result<(), ContextError> { - let mut ibc_store = self.ibc_store.lock(); - - let client_record = ibc_store - .clients - .entry(consensus_state_path.client_id) - .or_insert(MockClientRecord { - consensus_states: Default::default(), - client_state: Default::default(), - }); - - let height = Height::new( - consensus_state_path.revision_number, - consensus_state_path.revision_height, - ) - .expect("Never fails"); - - client_record.consensus_states.remove(&height); - + self.consensus_state_store.delete(consensus_state_path); Ok(()) } + /// Delete the update metadata associated with the client at the specified + /// height. fn delete_update_meta( &mut self, client_id: ClientId, height: Height, ) -> Result<(), ContextError> { - let key = (client_id, height); - let mut ibc_store = self.ibc_store.lock(); - ibc_store.client_processed_times.remove(&key); - ibc_store.client_processed_heights.remove(&key); + let client_update_time_path = ClientUpdateTimePath::new( + client_id.clone(), + height.revision_number(), + height.revision_height(), + ); + self.client_processed_times.delete(client_update_time_path); + let client_update_height_path = ClientUpdateHeightPath::new( + client_id, + height.revision_number(), + height.revision_height(), + ); + self.client_processed_heights + .delete(client_update_height_path); Ok(()) } + /// Called upon successful client update. Implementations are expected to + /// use this to record the time and height at which this update (or header) + /// was processed. fn store_update_meta( &mut self, client_id: ClientId, @@ -291,13 +300,26 @@ impl ClientExecutionContext for MockContext { host_timestamp: Timestamp, host_height: Height, ) -> Result<(), ContextError> { - let mut ibc_store = self.ibc_store.lock(); - ibc_store - .client_processed_times - .insert((client_id.clone(), height), host_timestamp); - ibc_store - .client_processed_heights - .insert((client_id, height), host_height); + let client_update_time_path = ClientUpdateTimePath::new( + client_id.clone(), + height.revision_number(), + height.revision_height(), + ); + self.client_processed_times + .set(client_update_time_path, host_timestamp) + .map_err(|_| ClientError::Other { + description: "store update error".into(), + })?; + let client_update_height_path = ClientUpdateHeightPath::new( + client_id, + height.revision_number(), + height.revision_height(), + ); + self.client_processed_heights + .set(client_update_height_path, host_height) + .map_err(|_| ClientError::Other { + description: "store update error".into(), + })?; Ok(()) } } diff --git a/ibc-testkit/src/testapp/ibc/core/core_ctx.rs b/ibc-testkit/src/testapp/ibc/core/core_ctx.rs index 4901b2d2c..386a83efb 100644 --- a/ibc-testkit/src/testapp/ibc/core/core_ctx.rs +++ b/ibc-testkit/src/testapp/ibc/core/core_ctx.rs @@ -1,70 +1,78 @@ //! Implementation of a global context mock. Used in testing handlers of all IBC modules. -use core::ops::Add; +use core::fmt::Debug; use core::time::Duration; -use ibc::core::channel::types::channel::ChannelEnd; +use basecoin_store::context::{ProvableStore, Store}; +use basecoin_store::types::Height as StoreHeight; +use ibc::core::channel::types::channel::{ChannelEnd, IdentifiedChannelEnd}; use ibc::core::channel::types::commitment::{AcknowledgementCommitment, PacketCommitment}; use ibc::core::channel::types::error::{ChannelError, PacketError}; -use ibc::core::channel::types::packet::Receipt; +use ibc::core::channel::types::packet::{PacketState, Receipt}; +use ibc::core::client::context::consensus_state::ConsensusState; use ibc::core::client::types::error::ClientError; use ibc::core::client::types::Height; use ibc::core::commitment_types::commitment::CommitmentPrefix; +use ibc::core::commitment_types::merkle::MerkleProof; use ibc::core::connection::types::error::ConnectionError; -use ibc::core::connection::types::ConnectionEnd; +use ibc::core::connection::types::{ConnectionEnd, IdentifiedConnectionEnd}; use ibc::core::handler::types::error::ContextError; use ibc::core::handler::types::events::IbcEvent; -use ibc::core::host::types::identifiers::{ConnectionId, Sequence}; +use ibc::core::host::types::identifiers::{ClientId, ConnectionId, Sequence}; use ibc::core::host::types::path::{ - AckPath, ChannelEndPath, ClientConnectionPath, CommitmentPath, ConnectionPath, ReceiptPath, + AckPath, ChannelEndPath, ClientConnectionPath, CommitmentPath, ConnectionPath, + NextChannelSequencePath, NextClientSequencePath, NextConnectionSequencePath, Path, ReceiptPath, SeqAckPath, SeqRecvPath, SeqSendPath, }; -use ibc::core::host::{ExecutionContext, ValidationContext}; +use ibc::core::host::{ClientStateRef, ConsensusStateRef, ExecutionContext, ValidationContext}; use ibc::core::primitives::prelude::*; use ibc::core::primitives::{Signer, Timestamp}; +use ibc::primitives::ToVec; +use ibc_proto::ibc::core::commitment::v1::MerkleProof as RawMerkleProof; +use ibc_query::core::context::{ProvableContext, QueryContext}; -use super::types::MockContext; -use crate::testapp::ibc::clients::mock::client_state::MockClientState; -use crate::testapp::ibc::clients::mock::consensus_state::MockConsensusState; -use crate::testapp::ibc::clients::AnyConsensusState; +use super::types::{MockIbcStore, DEFAULT_BLOCK_TIME_SECS}; +use crate::testapp::ibc::clients::{AnyClientState, AnyConsensusState}; -impl ValidationContext for MockContext { +impl ValidationContext for MockIbcStore +where + S: ProvableStore + Debug, +{ type V = Self; - type HostClientState = MockClientState; - type HostConsensusState = MockConsensusState; + type HostClientState = AnyClientState; + type HostConsensusState = AnyConsensusState; fn host_height(&self) -> Result { - Ok(self.latest_height()) + Ok(Height::new( + *self.revision_number.lock(), + self.store.current_height(), + )?) } fn host_timestamp(&self) -> Result { - Ok(self - .history - .last() - .expect("history cannot be empty") - .timestamp() - .add(self.block_time) - .expect("Never fails")) + let host_height = self.host_height()?; + let host_cons_state = self.host_consensus_state(&host_height)?; + Ok(host_cons_state.timestamp()) } fn client_counter(&self) -> Result { - Ok(self.ibc_store.lock().client_ids_counter) + Ok(self + .client_counter + .get(StoreHeight::Pending, &NextClientSequencePath) + .ok_or(ClientError::Other { + description: "client counter not found".into(), + })?) } - fn host_consensus_state(&self, height: &Height) -> Result { - let cs: AnyConsensusState = match self.host_block(height) { - Some(block_ref) => Ok(block_ref.clone().into()), - None => Err(ClientError::MissingLocalConsensusState { height: *height }), - } - .map_err(ContextError::ClientError)?; - - match cs { - AnyConsensusState::Mock(cs) => Ok(cs), - _ => Err(ClientError::Other { - description: "unexpected consensus state type".to_string(), - } - .into()), - } + fn host_consensus_state( + &self, + height: &Height, + ) -> Result { + let consensus_states_binding = self.host_consensus_states.lock(); + Ok(consensus_states_binding + .get(&height.revision_height()) + .cloned() + .ok_or(ClientError::MissingLocalConsensusState { height: *height })?) } fn validate_self_client( @@ -78,8 +86,9 @@ impl ValidationContext for MockContext { .into()); } - let self_chain_id = &self.host_chain_id; - let self_revision_number = self_chain_id.revision_number(); + let latest_height = self.host_height()?; + + let self_revision_number = latest_height.revision_number(); if self_revision_number != client_state_of_host_on_counterparty .latest_height() @@ -98,7 +107,7 @@ impl ValidationContext for MockContext { )); } - let host_current_height = self.latest_height().increment(); + let host_current_height = latest_height.increment(); if client_state_of_host_on_counterparty.latest_height() >= host_current_height { return Err(ContextError::ConnectionError( ConnectionError::InvalidClientState { @@ -114,179 +123,140 @@ impl ValidationContext for MockContext { Ok(()) } - fn connection_end(&self, cid: &ConnectionId) -> Result { - match self.ibc_store.lock().connections.get(cid) { - Some(connection_end) => Ok(connection_end.clone()), - None => Err(ConnectionError::ConnectionNotFound { - connection_id: cid.clone(), - }), - } - .map_err(ContextError::ConnectionError) + fn connection_end(&self, conn_id: &ConnectionId) -> Result { + Ok(self + .connection_end_store + .get(StoreHeight::Pending, &ConnectionPath::new(conn_id)) + .ok_or(ConnectionError::ConnectionNotFound { + connection_id: conn_id.clone(), + })?) } fn commitment_prefix(&self) -> CommitmentPrefix { + // this is prefix of ibc store + // using a dummy prefix as in our mock context CommitmentPrefix::try_from(b"mock".to_vec()).expect("Never fails") } fn connection_counter(&self) -> Result { - Ok(self.ibc_store.lock().connection_ids_counter) + Ok(self + .conn_counter + .get(StoreHeight::Pending, &NextConnectionSequencePath) + .ok_or(ConnectionError::Other { + description: "connection counter not found".into(), + })?) } - fn channel_end(&self, chan_end_path: &ChannelEndPath) -> Result { - let port_id = &chan_end_path.0; - let channel_id = &chan_end_path.1; - - match self - .ibc_store - .lock() - .channels - .get(port_id) - .and_then(|map| map.get(channel_id)) - { - Some(channel_end) => Ok(channel_end.clone()), - None => Err(ChannelError::ChannelNotFound { - port_id: port_id.clone(), - channel_id: channel_id.clone(), - }), - } - .map_err(ContextError::ChannelError) + fn channel_end(&self, channel_end_path: &ChannelEndPath) -> Result { + Ok(self + .channel_end_store + .get( + StoreHeight::Pending, + &ChannelEndPath::new(&channel_end_path.0, &channel_end_path.1), + ) + .ok_or(ChannelError::MissingChannel)?) } fn get_next_sequence_send( &self, seq_send_path: &SeqSendPath, ) -> Result { - let port_id = &seq_send_path.0; - let channel_id = &seq_send_path.1; - - match self - .ibc_store - .lock() - .next_sequence_send - .get(port_id) - .and_then(|map| map.get(channel_id)) - { - Some(sequence) => Ok(*sequence), - None => Err(PacketError::MissingNextSendSeq { - port_id: port_id.clone(), - channel_id: channel_id.clone(), - }), - } - .map_err(ContextError::PacketError) + Ok(self + .send_sequence_store + .get( + StoreHeight::Pending, + &SeqSendPath::new(&seq_send_path.0, &seq_send_path.1), + ) + .ok_or(PacketError::ImplementationSpecific)?) } fn get_next_sequence_recv( &self, seq_recv_path: &SeqRecvPath, ) -> Result { - let port_id = &seq_recv_path.0; - let channel_id = &seq_recv_path.1; - - match self - .ibc_store - .lock() - .next_sequence_recv - .get(port_id) - .and_then(|map| map.get(channel_id)) - { - Some(sequence) => Ok(*sequence), - None => Err(PacketError::MissingNextRecvSeq { - port_id: port_id.clone(), - channel_id: channel_id.clone(), - }), - } - .map_err(ContextError::PacketError) + Ok(self + .recv_sequence_store + .get( + StoreHeight::Pending, + &SeqRecvPath::new(&seq_recv_path.0, &seq_recv_path.1), + ) + .ok_or(PacketError::ImplementationSpecific)?) } fn get_next_sequence_ack(&self, seq_ack_path: &SeqAckPath) -> Result { - let port_id = &seq_ack_path.0; - let channel_id = &seq_ack_path.1; - - match self - .ibc_store - .lock() - .next_sequence_ack - .get(port_id) - .and_then(|map| map.get(channel_id)) - { - Some(sequence) => Ok(*sequence), - None => Err(PacketError::MissingNextAckSeq { - port_id: port_id.clone(), - channel_id: channel_id.clone(), - }), - } - .map_err(ContextError::PacketError) + Ok(self + .ack_sequence_store + .get( + StoreHeight::Pending, + &SeqAckPath::new(&seq_ack_path.0, &seq_ack_path.1), + ) + .ok_or(PacketError::ImplementationSpecific)?) } fn get_packet_commitment( &self, commitment_path: &CommitmentPath, ) -> Result { - let port_id = &commitment_path.port_id; - let channel_id = &commitment_path.channel_id; - let seq = &commitment_path.sequence; - - match self - .ibc_store - .lock() - .packet_commitment - .get(port_id) - .and_then(|map| map.get(channel_id)) - .and_then(|map| map.get(seq)) - { - Some(commitment) => Ok(commitment.clone()), - None => Err(PacketError::PacketCommitmentNotFound { sequence: *seq }), - } - .map_err(ContextError::PacketError) + Ok(self + .packet_commitment_store + .get( + StoreHeight::Pending, + &CommitmentPath::new( + &commitment_path.port_id, + &commitment_path.channel_id, + commitment_path.sequence, + ), + ) + .ok_or(PacketError::ImplementationSpecific)?) } fn get_packet_receipt(&self, receipt_path: &ReceiptPath) -> Result { - let port_id = &receipt_path.port_id; - let channel_id = &receipt_path.channel_id; - let seq = &receipt_path.sequence; - - match self - .ibc_store - .lock() - .packet_receipt - .get(port_id) - .and_then(|map| map.get(channel_id)) - .and_then(|map| map.get(seq)) - { - Some(receipt) => Ok(receipt.clone()), - None => Err(PacketError::PacketReceiptNotFound { sequence: *seq }), - } - .map_err(ContextError::PacketError) + Ok(self + .packet_receipt_store + .is_path_set( + StoreHeight::Pending, + &ReceiptPath::new( + &receipt_path.port_id, + &receipt_path.channel_id, + receipt_path.sequence, + ), + ) + .then_some(Receipt::Ok) + .ok_or(PacketError::PacketReceiptNotFound { + sequence: receipt_path.sequence, + })?) } fn get_packet_acknowledgement( &self, ack_path: &AckPath, ) -> Result { - let port_id = &ack_path.port_id; - let channel_id = &ack_path.channel_id; - let seq = &ack_path.sequence; - - match self - .ibc_store - .lock() - .packet_acknowledgement - .get(port_id) - .and_then(|map| map.get(channel_id)) - .and_then(|map| map.get(seq)) - { - Some(ack) => Ok(ack.clone()), - None => Err(PacketError::PacketAcknowledgementNotFound { sequence: *seq }), - } - .map_err(ContextError::PacketError) - } - + Ok(self + .packet_ack_store + .get( + StoreHeight::Pending, + &AckPath::new(&ack_path.port_id, &ack_path.channel_id, ack_path.sequence), + ) + .ok_or(PacketError::PacketAcknowledgementNotFound { + sequence: ack_path.sequence, + })?) + } + + /// Returns a counter of the number of channel ids that have been created thus far. + /// The value of this counter should increase only via the + /// `ChannelKeeper::increase_channel_counter` method. fn channel_counter(&self) -> Result { - Ok(self.ibc_store.lock().channel_ids_counter) + Ok(self + .channel_counter + .get(StoreHeight::Pending, &NextChannelSequencePath) + .ok_or(ChannelError::Other { + description: "channel counter not found".into(), + })?) } + /// Returns the maximum expected time per block fn max_expected_time_per_block(&self) -> Duration { - self.block_time + Duration::from_secs(DEFAULT_BLOCK_TIME_SECS) } fn validate_message_signer(&self, _signer: &Signer) -> Result<(), ContextError> { @@ -298,46 +268,452 @@ impl ValidationContext for MockContext { } } -impl ExecutionContext for MockContext { +/// Trait to provide proofs in gRPC service blanket implementations. +impl ProvableContext for MockIbcStore +where + S: ProvableStore + Debug, +{ + /// Returns the proof for the given [`Height`] and [`Path`] + fn get_proof(&self, height: Height, path: &Path) -> Option> { + self.store + .get_proof(height.revision_height().into(), &path.to_string().into()) + .map(|path_proof| { + let ibc_commitment_proof = self + .ibc_commiment_proofs + .lock() + .get(&height.revision_height()) + .expect("proof exists") + .clone(); + + RawMerkleProof::from(MerkleProof { + proofs: vec![path_proof, ibc_commitment_proof], + }) + }) + .map(|p| p.to_vec()) + } +} + +/// Trait to complete the gRPC service blanket implementations. +impl QueryContext for MockIbcStore +where + S: ProvableStore + Debug, +{ + /// Returns the list of all client states. + fn client_states(&self) -> Result)>, ContextError> { + let path = "clients".to_owned().into(); + + self.client_state_store + .get_keys(&path) + .into_iter() + .filter_map(|path| { + if let Ok(Path::ClientState(client_path)) = path.try_into() { + Some(client_path) + } else { + None + } + }) + .map(|client_state_path| { + let client_state = self + .client_state_store + .get(StoreHeight::Pending, &client_state_path) + .ok_or_else(|| ClientError::ClientStateNotFound { + client_id: client_state_path.0.clone(), + })?; + Ok((client_state_path.0, client_state)) + }) + .collect() + } + + /// Returns the list of all consensus states of the given client. + fn consensus_states( + &self, + client_id: &ClientId, + ) -> Result)>, ContextError> { + let path = format!("clients/{}/consensusStates", client_id) + .try_into() + .map_err(|_| ClientError::Other { + description: "Invalid consensus state path".into(), + })?; + + self.consensus_state_store + .get_keys(&path) + .into_iter() + .filter_map(|path| { + if let Ok(Path::ClientConsensusState(consensus_path)) = path.try_into() { + Some(consensus_path) + } else { + None + } + }) + .map(|consensus_path| { + let height = Height::new( + consensus_path.revision_number, + consensus_path.revision_height, + )?; + let client_state = self + .consensus_state_store + .get(StoreHeight::Pending, &consensus_path) + .ok_or({ + ClientError::ConsensusStateNotFound { + client_id: consensus_path.client_id, + height, + } + })?; + Ok((height, client_state)) + }) + .collect() + } + + /// Returns the list of heights at which the consensus state of the given client was updated. + fn consensus_state_heights(&self, client_id: &ClientId) -> Result, ContextError> { + let path = format!("clients/{}/consensusStates", client_id) + .try_into() + .map_err(|_| ClientError::Other { + description: "Invalid consensus state path".into(), + })?; + + self.consensus_state_store + .get_keys(&path) + .into_iter() + .filter_map(|path| { + if let Ok(Path::ClientConsensusState(consensus_path)) = path.try_into() { + Some(consensus_path) + } else { + None + } + }) + .map(|consensus_path| { + Ok(Height::new( + consensus_path.revision_number, + consensus_path.revision_height, + )?) + }) + .collect::, _>>() + } + + /// Returns all the IBC connection ends of a chain. + fn connection_ends(&self) -> Result, ContextError> { + let path = "connections".to_owned().into(); + + self.connection_end_store + .get_keys(&path) + .into_iter() + .filter_map(|path| { + if let Ok(Path::Connection(connection_path)) = path.try_into() { + Some(connection_path) + } else { + None + } + }) + .map(|connection_path| { + let connection_end = self + .connection_end_store + .get(StoreHeight::Pending, &connection_path) + .ok_or_else(|| ConnectionError::ConnectionNotFound { + connection_id: connection_path.0.clone(), + })?; + Ok(IdentifiedConnectionEnd { + connection_id: connection_path.0, + connection_end, + }) + }) + .collect() + } + + /// Returns all the IBC connection ends associated with a client. + fn client_connection_ends( + &self, + client_id: &ClientId, + ) -> Result, ContextError> { + let client_connection_path = ClientConnectionPath::new(client_id.clone()); + + Ok(self + .connection_ids_store + .get(StoreHeight::Pending, &client_connection_path) + .unwrap_or_default()) + } + + /// Returns all the IBC channel ends of a chain. + fn channel_ends(&self) -> Result, ContextError> { + let path = "channelEnds".to_owned().into(); + + self.channel_end_store + .get_keys(&path) + .into_iter() + .filter_map(|path| { + if let Ok(Path::ChannelEnd(channel_path)) = path.try_into() { + Some(channel_path) + } else { + None + } + }) + .map(|channel_path| { + let channel_end = self + .channel_end_store + .get(StoreHeight::Pending, &channel_path) + .ok_or_else(|| ChannelError::ChannelNotFound { + port_id: channel_path.0.clone(), + channel_id: channel_path.1.clone(), + })?; + Ok(IdentifiedChannelEnd { + port_id: channel_path.0, + channel_id: channel_path.1, + channel_end, + }) + }) + .collect() + } + + /// Returns all the packet commitments associated with a channel. + fn packet_commitments( + &self, + channel_end_path: &ChannelEndPath, + ) -> Result, ContextError> { + let path = format!( + "commitments/ports/{}/channels/{}/sequences", + channel_end_path.0, channel_end_path.1 + ) + .try_into() + .map_err(|_| PacketError::Other { + description: "Invalid commitment path".into(), + })?; + + self.packet_commitment_store + .get_keys(&path) + .into_iter() + .filter_map(|path| { + if let Ok(Path::Commitment(commitment_path)) = path.try_into() { + Some(commitment_path) + } else { + None + } + }) + .filter(|commitment_path| { + self.packet_commitment_store + .get(StoreHeight::Pending, commitment_path) + .is_some() + }) + .map(|commitment_path| { + self.get_packet_commitment(&commitment_path) + .map(|packet| PacketState { + seq: commitment_path.sequence, + port_id: commitment_path.port_id, + chan_id: commitment_path.channel_id, + data: packet.as_ref().into(), + }) + }) + .collect::, _>>() + } + + /// Returns the acknowledged packets associated with a channel. + /// + /// Takes a sequence list as an argument. + /// If the list set is empty, it returns all acknowledged packets. + fn packet_acknowledgements( + &self, + channel_end_path: &ChannelEndPath, + sequences: impl ExactSizeIterator, + ) -> Result, ContextError> { + let collected_paths: Vec<_> = if sequences.len() == 0 { + // if sequences is empty, return all the acks + let ack_path_prefix = format!( + "acks/ports/{}/channels/{}/sequences", + channel_end_path.0, channel_end_path.1 + ) + .try_into() + .map_err(|_| PacketError::Other { + description: "Invalid ack path".into(), + })?; + + self.packet_ack_store + .get_keys(&ack_path_prefix) + .into_iter() + .filter_map(|path| { + if let Ok(Path::Ack(ack_path)) = path.try_into() { + Some(ack_path) + } else { + None + } + }) + .collect() + } else { + sequences + .into_iter() + .map(|seq| AckPath::new(&channel_end_path.0, &channel_end_path.1, seq)) + .collect() + }; + + collected_paths + .into_iter() + .filter(|ack_path| { + self.packet_ack_store + .get(StoreHeight::Pending, ack_path) + .is_some() + }) + .map(|ack_path| { + self.get_packet_acknowledgement(&ack_path) + .map(|packet| PacketState { + seq: ack_path.sequence, + port_id: ack_path.port_id, + chan_id: ack_path.channel_id, + data: packet.as_ref().into(), + }) + }) + .collect::, _>>() + } + + /// Returns the unreceived IBC packets associated with a channel and sequences. + /// + /// Takes a sequence list as an argument. + fn unreceived_packets( + &self, + channel_end_path: &ChannelEndPath, + sequences: impl ExactSizeIterator, + ) -> Result, ContextError> { + // QUESTION. Currently only works for unordered channels; ordered channels + // don't use receipts. However, ibc-go does it this way. Investigate if + // this query only ever makes sense on unordered channels. + + Ok(sequences + .into_iter() + .map(|seq| ReceiptPath::new(&channel_end_path.0, &channel_end_path.1, seq)) + .filter(|receipt_path| { + self.packet_receipt_store + .get(StoreHeight::Pending, receipt_path) + .is_none() + }) + .map(|receipts_path| receipts_path.sequence) + .collect()) + } + + /// Returns all the unreceived IBC acknowledgements associated with a channel and sequences. + /// + /// Takes a sequence list as an argument. + /// If the list is empty, it Returns all the unreceived acks. + fn unreceived_acks( + &self, + channel_end_path: &ChannelEndPath, + sequences: impl ExactSizeIterator, + ) -> Result, ContextError> { + let collected_paths: Vec<_> = if sequences.len() == 0 { + // if sequences is empty, return all the acks + let commitment_path_prefix = format!( + "commitments/ports/{}/channels/{}/sequences", + channel_end_path.0, channel_end_path.1 + ) + .try_into() + .map_err(|_| PacketError::Other { + description: "Invalid commitment path".into(), + })?; + + self.packet_commitment_store + .get_keys(&commitment_path_prefix) + .into_iter() + .filter_map(|path| { + if let Ok(Path::Commitment(commitment_path)) = path.try_into() { + Some(commitment_path) + } else { + None + } + }) + .collect() + } else { + sequences + .into_iter() + .map(|seq| CommitmentPath::new(&channel_end_path.0, &channel_end_path.1, seq)) + .collect() + }; + + Ok(collected_paths + .into_iter() + .filter(|commitment_path: &CommitmentPath| -> bool { + self.packet_commitment_store + .get(StoreHeight::Pending, commitment_path) + .is_some() + }) + .map(|commitment_path| commitment_path.sequence) + .collect()) + } +} + +impl ExecutionContext for MockIbcStore +where + S: ProvableStore + Debug, +{ type E = Self; fn get_client_execution_context(&mut self) -> &mut Self::E { self } + /// Called upon client creation. + /// Increases the counter which keeps track of how many clients have been created. fn increase_client_counter(&mut self) -> Result<(), ContextError> { - self.ibc_store.lock().client_ids_counter += 1; + let current_sequence = self + .client_counter + .get(StoreHeight::Pending, &NextClientSequencePath) + .ok_or(ClientError::Other { + description: "client counter not found".into(), + })?; + + self.client_counter + .set(NextClientSequencePath, current_sequence + 1) + .map_err(|e| ClientError::Other { + description: format!("client counter update failed: {e:?}"), + })?; + Ok(()) } + /// Stores the given connection_end at path fn store_connection( &mut self, connection_path: &ConnectionPath, connection_end: ConnectionEnd, ) -> Result<(), ContextError> { - let connection_id = connection_path.0.clone(); - self.ibc_store - .lock() - .connections - .insert(connection_id, connection_end); + self.connection_end_store + .set(connection_path.clone(), connection_end) + .map_err(|_| ConnectionError::Other { + description: "Connection end store error".to_string(), + })?; Ok(()) } + /// Stores the given connection_id at a path associated with the client_id. fn store_connection_to_client( &mut self, client_connection_path: &ClientConnectionPath, conn_id: ConnectionId, ) -> Result<(), ContextError> { - let client_id = client_connection_path.0.clone(); - self.ibc_store - .lock() - .client_connections - .insert(client_id, conn_id); + let mut conn_ids: Vec = self + .connection_ids_store + .get(StoreHeight::Pending, client_connection_path) + .unwrap_or_default(); + conn_ids.push(conn_id); + self.connection_ids_store + .set(client_connection_path.clone(), conn_ids) + .map_err(|_| ConnectionError::Other { + description: "Connection ids store error".to_string(), + })?; Ok(()) } + /// Called upon connection identifier creation (Init or Try process). + /// Increases the counter which keeps track of how many connections have been created. fn increase_connection_counter(&mut self) -> Result<(), ContextError> { - self.ibc_store.lock().connection_ids_counter += 1; + let current_sequence = self + .conn_counter + .get(StoreHeight::Pending, &NextConnectionSequencePath) + .ok_or(ConnectionError::Other { + description: "connection counter not found".into(), + })?; + + self.conn_counter + .set(NextConnectionSequencePath, current_sequence + 1) + .map_err(|e| ConnectionError::Other { + description: format!("connection counter update failed: {e:?}"), + })?; + Ok(()) } @@ -346,14 +722,9 @@ impl ExecutionContext for MockContext { commitment_path: &CommitmentPath, commitment: PacketCommitment, ) -> Result<(), ContextError> { - self.ibc_store - .lock() - .packet_commitment - .entry(commitment_path.port_id.clone()) - .or_default() - .entry(commitment_path.channel_id.clone()) - .or_default() - .insert(commitment_path.sequence, commitment); + self.packet_commitment_store + .set(commitment_path.clone(), commitment) + .map_err(|_| PacketError::ImplementationSpecific)?; Ok(()) } @@ -361,28 +732,18 @@ impl ExecutionContext for MockContext { &mut self, commitment_path: &CommitmentPath, ) -> Result<(), ContextError> { - self.ibc_store - .lock() - .packet_commitment - .get_mut(&commitment_path.port_id) - .and_then(|map| map.get_mut(&commitment_path.channel_id)) - .and_then(|map| map.remove(&commitment_path.sequence)); + self.packet_commitment_store.delete(commitment_path.clone()); Ok(()) } fn store_packet_receipt( &mut self, - path: &ReceiptPath, - receipt: Receipt, + receipt_path: &ReceiptPath, + _receipt: Receipt, ) -> Result<(), ContextError> { - self.ibc_store - .lock() - .packet_receipt - .entry(path.port_id.clone()) - .or_default() - .entry(path.channel_id.clone()) - .or_default() - .insert(path.sequence, receipt); + self.packet_receipt_store + .set_path(receipt_path.clone()) + .map_err(|_| PacketError::ImplementationSpecific)?; Ok(()) } @@ -391,32 +752,14 @@ impl ExecutionContext for MockContext { ack_path: &AckPath, ack_commitment: AcknowledgementCommitment, ) -> Result<(), ContextError> { - let port_id = ack_path.port_id.clone(); - let channel_id = ack_path.channel_id.clone(); - let seq = ack_path.sequence; - - self.ibc_store - .lock() - .packet_acknowledgement - .entry(port_id) - .or_default() - .entry(channel_id) - .or_default() - .insert(seq, ack_commitment); + self.packet_ack_store + .set(ack_path.clone(), ack_commitment) + .map_err(|_| PacketError::ImplementationSpecific)?; Ok(()) } fn delete_packet_acknowledgement(&mut self, ack_path: &AckPath) -> Result<(), ContextError> { - let port_id = ack_path.port_id.clone(); - let channel_id = ack_path.channel_id.clone(); - let sequence = ack_path.sequence; - - self.ibc_store - .lock() - .packet_acknowledgement - .get_mut(&port_id) - .and_then(|map| map.get_mut(&channel_id)) - .and_then(|map| map.remove(&sequence)); + self.packet_ack_store.delete(ack_path.clone()); Ok(()) } @@ -425,15 +768,11 @@ impl ExecutionContext for MockContext { channel_end_path: &ChannelEndPath, channel_end: ChannelEnd, ) -> Result<(), ContextError> { - let port_id = channel_end_path.0.clone(); - let channel_id = channel_end_path.1.clone(); - - self.ibc_store - .lock() - .channels - .entry(port_id) - .or_default() - .insert(channel_id, channel_end); + self.channel_end_store + .set(channel_end_path.clone(), channel_end) + .map_err(|_| ChannelError::Other { + description: "Channel end store error".to_string(), + })?; Ok(()) } @@ -442,15 +781,9 @@ impl ExecutionContext for MockContext { seq_send_path: &SeqSendPath, seq: Sequence, ) -> Result<(), ContextError> { - let port_id = seq_send_path.0.clone(); - let channel_id = seq_send_path.1.clone(); - - self.ibc_store - .lock() - .next_sequence_send - .entry(port_id) - .or_default() - .insert(channel_id, seq); + self.send_sequence_store + .set(seq_send_path.clone(), seq) + .map_err(|_| PacketError::ImplementationSpecific)?; Ok(()) } @@ -459,15 +792,9 @@ impl ExecutionContext for MockContext { seq_recv_path: &SeqRecvPath, seq: Sequence, ) -> Result<(), ContextError> { - let port_id = seq_recv_path.0.clone(); - let channel_id = seq_recv_path.1.clone(); - - self.ibc_store - .lock() - .next_sequence_recv - .entry(port_id) - .or_default() - .insert(channel_id, seq); + self.recv_sequence_store + .set(seq_recv_path.clone(), seq) + .map_err(|_| PacketError::ImplementationSpecific)?; Ok(()) } @@ -476,30 +803,36 @@ impl ExecutionContext for MockContext { seq_ack_path: &SeqAckPath, seq: Sequence, ) -> Result<(), ContextError> { - let port_id = seq_ack_path.0.clone(); - let channel_id = seq_ack_path.1.clone(); - - self.ibc_store - .lock() - .next_sequence_ack - .entry(port_id) - .or_default() - .insert(channel_id, seq); + self.ack_sequence_store + .set(seq_ack_path.clone(), seq) + .map_err(|_| PacketError::ImplementationSpecific)?; Ok(()) } fn increase_channel_counter(&mut self) -> Result<(), ContextError> { - self.ibc_store.lock().channel_ids_counter += 1; + let current_sequence = self + .channel_counter + .get(StoreHeight::Pending, &NextChannelSequencePath) + .ok_or(ChannelError::Other { + description: "channel counter not found".into(), + })?; + + self.channel_counter + .set(NextChannelSequencePath, current_sequence + 1) + .map_err(|e| ChannelError::Other { + description: format!("channel counter update failed: {e:?}"), + })?; + Ok(()) } fn emit_ibc_event(&mut self, event: IbcEvent) -> Result<(), ContextError> { - self.ibc_store.lock().events.push(event); + self.events.lock().push(event); Ok(()) } fn log_message(&mut self, message: String) -> Result<(), ContextError> { - self.ibc_store.lock().logs.push(message); + self.logs.lock().push(message); Ok(()) } } diff --git a/ibc-testkit/src/testapp/ibc/core/router/mod.rs b/ibc-testkit/src/testapp/ibc/core/router/mod.rs index 6629c0e08..faaf47994 100644 --- a/ibc-testkit/src/testapp/ibc/core/router/mod.rs +++ b/ibc-testkit/src/testapp/ibc/core/router/mod.rs @@ -1,4 +1,4 @@ mod context; mod types; -pub use types::*; +pub use self::types::*; diff --git a/ibc-testkit/src/testapp/ibc/core/router/types.rs b/ibc-testkit/src/testapp/ibc/core/router/types.rs index 00aff346f..b48cd7fbd 100644 --- a/ibc-testkit/src/testapp/ibc/core/router/types.rs +++ b/ibc-testkit/src/testapp/ibc/core/router/types.rs @@ -8,7 +8,7 @@ use ibc::core::router::types::module::ModuleId; use crate::testapp::ibc::applications::transfer::types::DummyTransferModule; -#[derive(Default)] +#[derive(Debug, Default)] pub struct MockRouter { pub router: BTreeMap>, diff --git a/ibc-testkit/src/testapp/ibc/core/types.rs b/ibc-testkit/src/testapp/ibc/core/types.rs index 7e365c49c..a1296cf6a 100644 --- a/ibc-testkit/src/testapp/ibc/core/types.rs +++ b/ibc-testkit/src/testapp/ibc/core/types.rs @@ -1,779 +1,200 @@ //! Implementation of a global context mock. Used in testing handlers of all IBC modules. use alloc::sync::Arc; -use core::cmp::min; use core::fmt::Debug; -use core::ops::{Add, Sub}; -use core::time::Duration; -use ibc::clients::tendermint::client_state::ClientState as TmClientState; -use ibc::clients::tendermint::types::TENDERMINT_CLIENT_TYPE; +use basecoin_store::context::{ProvableStore, Store}; +use basecoin_store::impls::SharedStore; +use basecoin_store::types::{BinStore, JsonStore, ProtobufStore, TypedSet, TypedStore}; use ibc::core::channel::types::channel::ChannelEnd; use ibc::core::channel::types::commitment::{AcknowledgementCommitment, PacketCommitment}; -use ibc::core::channel::types::packet::Receipt; +use ibc::core::client::context::client_state::ClientStateValidation; use ibc::core::client::types::Height; use ibc::core::connection::types::ConnectionEnd; -use ibc::core::entrypoint::dispatch; use ibc::core::handler::types::events::IbcEvent; -use ibc::core::handler::types::msgs::MsgEnvelope; -use ibc::core::host::types::identifiers::{ - ChainId, ChannelId, ClientId, ClientType, ConnectionId, PortId, Sequence, +use ibc::core::host::types::identifiers::{ConnectionId, Sequence}; +use ibc::core::host::types::path::{ + AckPath, ChannelEndPath, ClientConnectionPath, ClientConsensusStatePath, ClientStatePath, + ClientUpdateHeightPath, ClientUpdateTimePath, CommitmentPath, ConnectionPath, + NextChannelSequencePath, NextClientSequencePath, NextConnectionSequencePath, ReceiptPath, + SeqAckPath, SeqRecvPath, SeqSendPath, }; -use ibc::core::host::ValidationContext; use ibc::core::primitives::prelude::*; use ibc::core::primitives::Timestamp; -use ibc::core::router::router::Router; +use ibc_proto::google::protobuf::Any; +use ibc_proto::ibc::core::channel::v1::Channel as RawChannelEnd; +use ibc_proto::ibc::core::client::v1::Height as RawHeight; +use ibc_proto::ibc::core::connection::v1::ConnectionEnd as RawConnectionEnd; +use ibc_proto::ics23::CommitmentProof; use parking_lot::Mutex; -use tendermint_testgen::Validator as TestgenValidator; use typed_builder::TypedBuilder; -use super::client_ctx::{MockClientRecord, PortChannelIdMap}; -use crate::fixtures::clients::tendermint::ClientStateConfig as TmClientStateConfig; -use crate::fixtures::core::context::MockContextConfig; -use crate::hosts::block::{HostBlock, HostType}; -use crate::relayer::error::RelayerError; -use crate::testapp::ibc::clients::mock::client_state::{ - client_type as mock_client_type, MockClientState, MOCK_CLIENT_TYPE, -}; -use crate::testapp::ibc::clients::mock::consensus_state::MockConsensusState; +use crate::context::{MockStore, TestContext}; +use crate::fixtures::core::context::TestContextConfig; +use crate::hosts::{HostClientState, TestBlock, TestHeader, TestHost}; use crate::testapp::ibc::clients::mock::header::MockHeader; use crate::testapp::ibc::clients::{AnyClientState, AnyConsensusState}; pub const DEFAULT_BLOCK_TIME_SECS: u64 = 3; -/// An object that stores all IBC related data. -#[derive(Clone, Debug, Default)] -pub struct MockIbcStore { - /// The set of all clients, indexed by their id. - pub clients: BTreeMap, - - /// Tracks the processed time for clients header updates - pub client_processed_times: BTreeMap<(ClientId, Height), Timestamp>, - - /// Tracks the processed height for the clients - pub client_processed_heights: BTreeMap<(ClientId, Height), Height>, - - /// Counter for the client identifiers, necessary for `increase_client_counter` and the - /// `client_counter` methods. - pub client_ids_counter: u64, - - /// Association between client ids and connection ids. - pub client_connections: BTreeMap, - - /// All the connections in the store. - pub connections: BTreeMap, - - /// Counter for connection identifiers (see `increase_connection_counter`). - pub connection_ids_counter: u64, - - /// Association between connection ids and channel ids. - pub connection_channels: BTreeMap>, - - /// Counter for channel identifiers (see `increase_channel_counter`). - pub channel_ids_counter: u64, - - /// All the channels in the store. TODO Make new key PortId X ChannelId - pub channels: PortChannelIdMap, +pub type DefaultIbcStore = MockIbcStore; - /// Tracks the sequence number for the next packet to be sent. - pub next_sequence_send: PortChannelIdMap, - - /// Tracks the sequence number for the next packet to be received. - pub next_sequence_recv: PortChannelIdMap, - - /// Tracks the sequence number for the next packet to be acknowledged. - pub next_sequence_ack: PortChannelIdMap, - - pub packet_acknowledgement: PortChannelIdMap>, - - /// Constant-size commitments to packets data fields - pub packet_commitment: PortChannelIdMap>, - - /// Used by unordered channel - pub packet_receipt: PortChannelIdMap>, - - /// Emitted IBC events in order - pub events: Vec, - - /// Logs of the IBC module - pub logs: Vec, -} - -/// A context implementing the dependencies necessary for testing any IBC module. +/// An object that stores all IBC related data. #[derive(Debug)] -pub struct MockContext { - /// The type of host chain underlying this mock context. - pub host_chain_type: HostType, - - /// Host chain identifier. - pub host_chain_id: ChainId, - - /// Maximum size for the history of the host chain. Any block older than this is pruned. - pub max_history_size: u64, - - /// 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). - pub history: Vec, - - /// Average time duration between blocks - pub block_time: Duration, - - /// An object that stores all IBC related data. - pub ibc_store: Arc>, -} - -#[derive(Debug, TypedBuilder)] -pub struct MockClientConfig { - #[builder(default = ChainId::new("mockZ-1").expect("no error"))] - client_chain_id: ChainId, - #[builder(default = ClientId::new("07-tendermint", 0).expect("no error"))] - client_id: ClientId, - #[builder(default = mock_client_type())] - client_type: ClientType, - latest_height: Height, - #[builder(default)] - consensus_state_heights: Vec, - #[builder(default = Timestamp::now())] - latest_timestamp: Timestamp, - - #[builder(default = Duration::from_secs(64000))] - trusting_period: Duration, - #[builder(default = Duration::from_millis(3000))] - max_clock_drift: Duration, - #[builder(default = Duration::from_secs(128_000))] - unbonding_period: Duration, -} - -/// Returns a MockContext with bare minimum initialization: no clients, no connections and no channels are -/// present, and the chain has Height(5). This should be used sparingly, mostly for testing the -/// creation of new domain objects. -impl Default for MockContext { - fn default() -> Self { - MockContextConfig::builder().build() - } -} - -/// A manual clone impl is provided because the tests are oblivious to the fact that the `ibc_store` -/// is a shared ptr. -impl Clone for MockContext { - fn clone(&self) -> Self { - let ibc_store = { - let ibc_store = self.ibc_store.lock().clone(); - Arc::new(Mutex::new(ibc_store)) - }; - - Self { - host_chain_type: self.host_chain_type, - host_chain_id: self.host_chain_id.clone(), - max_history_size: self.max_history_size, - history: self.history.clone(), - block_time: self.block_time, - ibc_store, - } - } +pub struct MockIbcStore +where + S: ProvableStore + Debug, +{ + /// chain revision number, + pub revision_number: Arc>, + + /// Handle to store instance. + /// The module is guaranteed exclusive access to all paths in the store key-space. + pub store: SharedStore, + /// A typed-store for next client counter sequence + pub client_counter: JsonStore, NextClientSequencePath, u64>, + /// A typed-store for next connection counter sequence + pub conn_counter: JsonStore, NextConnectionSequencePath, u64>, + /// A typed-store for next channel counter sequence + pub channel_counter: JsonStore, NextChannelSequencePath, u64>, + /// Tracks the processed time for client updates + pub client_processed_times: JsonStore, ClientUpdateTimePath, Timestamp>, + /// A typed-store to track the processed height for client updates + pub client_processed_heights: + ProtobufStore, ClientUpdateHeightPath, Height, RawHeight>, + /// A typed-store for AnyClientState + pub client_state_store: ProtobufStore, ClientStatePath, AnyClientState, Any>, + /// A typed-store for AnyConsensusState + pub consensus_state_store: + ProtobufStore, ClientConsensusStatePath, AnyConsensusState, Any>, + /// A typed-store for ConnectionEnd + pub connection_end_store: + ProtobufStore, ConnectionPath, ConnectionEnd, RawConnectionEnd>, + /// A typed-store for ConnectionIds + pub connection_ids_store: JsonStore, ClientConnectionPath, Vec>, + /// A typed-store for ChannelEnd + pub channel_end_store: ProtobufStore, ChannelEndPath, ChannelEnd, RawChannelEnd>, + /// A typed-store for send sequences + pub send_sequence_store: JsonStore, SeqSendPath, Sequence>, + /// A typed-store for receive sequences + pub recv_sequence_store: JsonStore, SeqRecvPath, Sequence>, + /// A typed-store for ack sequences + pub ack_sequence_store: JsonStore, SeqAckPath, Sequence>, + /// A typed-store for packet commitments + pub packet_commitment_store: BinStore, CommitmentPath, PacketCommitment>, + /// A typed-store for packet receipts + pub packet_receipt_store: TypedSet, ReceiptPath>, + /// A typed-store for packet ack + pub packet_ack_store: BinStore, AckPath, AcknowledgementCommitment>, + /// Map of host consensus states + pub host_consensus_states: Arc>>, + /// Map of older ibc commitment proofs + pub ibc_commiment_proofs: Arc>>, + /// IBC Events + pub events: Arc>>, + /// message logs + pub logs: Arc>>, } -/// Implementation of internal interface for use in testing. The methods in this interface should -/// _not_ be accessible to any Ics handler. -impl MockContext { - /// Creates a mock context. Parameter `max_history_size` determines how many blocks will - /// the chain maintain in its history, which also determines the pruning window. Parameter - /// `latest_height` determines the current height of the chain. This context - /// has support to emulate two type of underlying chains: Mock or SyntheticTendermint. - #[deprecated( - since = "0.50.0", - note = "Please use `MockContextConfig::builder().build()` instead" - )] - pub fn new( - host_id: ChainId, - host_type: HostType, - max_history_size: u64, - latest_height: Height, - ) -> Self { - assert_ne!( - max_history_size, 0, - "The chain must have a non-zero max_history_size" - ); - - assert_ne!( - latest_height.revision_height(), - 0, - "The chain must have a non-zero revision_height" - ); +impl MockIbcStore +where + S: ProvableStore + Debug, +{ + pub fn new(revision_number: u64, store: S) -> Self { + let shared_store = SharedStore::new(store); - // Compute the number of blocks to store. - let n = min(max_history_size, latest_height.revision_height()); + let mut client_counter = TypedStore::new(shared_store.clone()); + let mut conn_counter = TypedStore::new(shared_store.clone()); + let mut channel_counter = TypedStore::new(shared_store.clone()); - assert_eq!( - host_id.revision_number(), - latest_height.revision_number(), - "The version in the chain identifier must match the version in the latest height" - ); + client_counter + .set(NextClientSequencePath, 0) + .expect("no error"); - let block_time = Duration::from_secs(DEFAULT_BLOCK_TIME_SECS); - let next_block_timestamp = Timestamp::now().add(block_time).expect("Never fails"); - MockContext { - host_chain_type: host_type, - host_chain_id: host_id.clone(), - max_history_size, - 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).expect("Never fails").revision_height(), - next_block_timestamp - .sub(Duration::from_secs(DEFAULT_BLOCK_TIME_SECS * (i + 1))) - .expect("Never fails"), - ) - }) - .collect(), - block_time, - ibc_store: Arc::new(Mutex::new(MockIbcStore::default())), - } - } + conn_counter + .set(NextConnectionSequencePath, 0) + .expect("no error"); - /// Same as [Self::new] but with custom validator sets for each block. - /// Note: the validator history is used accordingly for current validator set and next validator set. - /// `validator_history[i]` and `validator_history[i+1]` is i'th block's current and next validator set. - /// The number of blocks will be `validator_history.len() - 1` due to the above. - #[deprecated( - since = "0.50.0", - note = "Please use `MockContextConfig::builder().build()` instead" - )] - pub fn new_with_validator_history( - host_id: ChainId, - host_type: HostType, - validator_history: &[Vec], - latest_height: Height, - ) -> Self { - let max_history_size = validator_history.len() as u64 - 1; + channel_counter + .set(NextChannelSequencePath, 0) + .expect("no error"); - assert_ne!( - max_history_size, 0, - "The chain must have a non-zero max_history_size" - ); - - assert_ne!( - latest_height.revision_height(), - 0, - "The chain must have a non-zero revision_height" - ); - - assert!( - max_history_size <= latest_height.revision_height(), - "The number of blocks must be greater than the number of validator set histories" - ); - - assert_eq!( - host_id.revision_number(), - latest_height.revision_number(), - "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).expect("Never fails"); - - let history = (0..max_history_size) - .rev() - .map(|i| { - // generate blocks with timestamps -> N, N - BT, N - 2BT, ... - // where N = now(), BT = block_time - HostBlock::generate_block_with_validators( - host_id.clone(), - host_type, - latest_height.sub(i).expect("Never fails").revision_height(), - next_block_timestamp - .sub(Duration::from_secs(DEFAULT_BLOCK_TIME_SECS * (i + 1))) - .expect("Never fails"), - &validator_history[(max_history_size - i) as usize - 1], - &validator_history[(max_history_size - i) as usize], - ) - }) - .collect(); - - MockContext { - host_chain_type: host_type, - host_chain_id: host_id, - max_history_size, - history, - block_time, - ibc_store: Arc::new(Mutex::new(MockIbcStore::default())), + Self { + revision_number: Arc::new(Mutex::new(revision_number)), + client_counter, + conn_counter, + channel_counter, + client_processed_times: TypedStore::new(shared_store.clone()), + client_processed_heights: TypedStore::new(shared_store.clone()), + host_consensus_states: Arc::new(Mutex::new(Default::default())), + ibc_commiment_proofs: Arc::new(Mutex::new(Default::default())), + client_state_store: TypedStore::new(shared_store.clone()), + consensus_state_store: TypedStore::new(shared_store.clone()), + connection_end_store: TypedStore::new(shared_store.clone()), + connection_ids_store: TypedStore::new(shared_store.clone()), + channel_end_store: TypedStore::new(shared_store.clone()), + send_sequence_store: TypedStore::new(shared_store.clone()), + recv_sequence_store: TypedStore::new(shared_store.clone()), + ack_sequence_store: TypedStore::new(shared_store.clone()), + packet_commitment_store: TypedStore::new(shared_store.clone()), + packet_receipt_store: TypedStore::new(shared_store.clone()), + packet_ack_store: TypedStore::new(shared_store.clone()), + events: Arc::new(Mutex::new(Vec::new())), + logs: Arc::new(Mutex::new(Vec::new())), + store: shared_store, } } - /// Associates a client record to this context. - /// Given a client id and a height, registers a new client in the context and also associates - /// to this client a mock client state and a mock consensus state for height `height`. The type - /// of this client is implicitly assumed to be Mock. - #[deprecated( - since = "0.50.0", - note = "Please use `MockClientConfig::builder().build()` instead" - )] - pub fn with_client(self, client_id: &ClientId, height: Height) -> Self { - // NOTE: this is wrong; the client chain ID is supposed to represent - // the chain ID of the counterparty chain. But at this point this is - // too ingrained in our tests; `with_client()` is called everywhere, - // which delegates to this. - let client_chain_id = self.host_chain_id.clone(); - - self.with_client_config( - MockClientConfig::builder() - .client_chain_id(client_chain_id) - .client_id(client_id.clone()) - .latest_height(height) - .build(), - ) - } - - /// Similar to `with_client`, this function associates a client record to this context, but - /// additionally permits to parametrize two details of the client. If `client_type` is None, - /// then the client will have type Mock, otherwise the specified type. If - /// `consensus_state_height` is None, then the client will be initialized with a consensus - /// state matching the same height as the client state (`client_state_height`). - #[deprecated( - since = "0.50.0", - note = "Please use `MockClientConfig::builder().build()` instead" - )] - pub fn with_client_parametrized( - self, - client_id: &ClientId, - client_state_height: Height, - client_type: Option, - consensus_state_height: Option, - ) -> Self { - // NOTE: this is wrong; the client chain ID is supposed to represent - // the chain ID of the counterparty chain. But at this point this is - // too ingrained in our tests; `with_client()` is called everywhere, - // which delegates to this. - let client_chain_id = self.host_chain_id.clone(); - - self.with_client_config( - MockClientConfig::builder() - .client_chain_id(client_chain_id) - .client_id(client_id.clone()) - .latest_height(client_state_height) - .client_type(client_type.unwrap_or_else(mock_client_type)) - .consensus_state_heights( - vec![consensus_state_height.unwrap_or(client_state_height)], - ) - .build(), - ) - } - - #[deprecated( - since = "0.50.0", - note = "Please use `MockClientConfig::builder().build()` instead" - )] - pub fn with_client_parametrized_with_chain_id( - self, - client_chain_id: ChainId, - client_id: &ClientId, - client_state_height: Height, - client_type: Option, - consensus_state_height: Option, - ) -> Self { - self.with_client_config( - MockClientConfig::builder() - .client_chain_id(client_chain_id) - .client_id(client_id.clone()) - .latest_height(client_state_height) - .client_type(client_type.unwrap_or_else(mock_client_type)) - .consensus_state_heights( - vec![consensus_state_height.unwrap_or(client_state_height)], - ) - .build(), - ) - } - - #[deprecated( - since = "0.50.0", - note = "Please use `MockClientConfig::builder().build()` instead" - )] - pub fn with_client_parametrized_history( - self, - client_id: &ClientId, - client_state_height: Height, - client_type: Option, - consensus_state_height: Option, - ) -> Self { - let client_chain_id = self.host_chain_id.clone(); - let current_consensus_height = consensus_state_height.unwrap_or(client_state_height); - let prev_consensus_height = current_consensus_height - .sub(1) - .unwrap_or(client_state_height); - self.with_client_config( - MockClientConfig::builder() - .client_chain_id(client_chain_id) - .client_id(client_id.clone()) - .latest_height(client_state_height) - .client_type(client_type.unwrap_or_else(mock_client_type)) - .consensus_state_heights(vec![prev_consensus_height, current_consensus_height]) - .build(), - ) - } - - #[deprecated( - since = "0.50.0", - note = "Please use `MockClientConfig::builder().build()` instead" - )] - pub fn with_client_parametrized_history_with_chain_id( - self, - client_chain_id: ChainId, - client_id: &ClientId, - client_state_height: Height, - client_type: Option, - consensus_state_height: Option, - ) -> Self { - let current_consensus_height = consensus_state_height.unwrap_or(client_state_height); - let prev_consensus_height = current_consensus_height - .sub(1) - .unwrap_or(client_state_height); - self.with_client_config( - MockClientConfig::builder() - .client_chain_id(client_chain_id) - .client_id(client_id.clone()) - .latest_height(client_state_height) - .client_type(client_type.unwrap_or_else(mock_client_type)) - .consensus_state_heights(vec![prev_consensus_height, current_consensus_height]) - .build(), - ) - } - - pub fn with_client_config(self, client: MockClientConfig) -> Self { - let cs_heights = if client.consensus_state_heights.is_empty() { - vec![client.latest_height] - } else { - client.consensus_state_heights - }; - - fn blocks_since(a: Height, b: Height) -> Option { - (a.revision_number() == b.revision_number() - && a.revision_height() >= b.revision_height()) - .then(|| a.revision_height() - b.revision_height()) - } - - let (client_state, consensus_states) = match client.client_type.as_str() { - MOCK_CLIENT_TYPE => { - let blocks: Vec<_> = cs_heights - .into_iter() - .map(|cs_height| { - let n_blocks = blocks_since(client.latest_height, cs_height) - .expect("less or equal height"); - ( - cs_height, - MockHeader::new(cs_height).with_timestamp( - client - .latest_timestamp - .sub(self.block_time * (n_blocks as u32)) - .expect("never fails"), - ), - ) - }) - .collect(); - - let client_state = MockClientState::new( - MockHeader::new(client.latest_height).with_timestamp(client.latest_timestamp), - ); - - let cs_states = blocks - .into_iter() - .map(|(height, block)| (height, MockConsensusState::new(block).into())) - .collect(); - - (client_state.into(), cs_states) - } - TENDERMINT_CLIENT_TYPE => { - let blocks: Vec<_> = cs_heights - .into_iter() - .map(|cs_height| { - let n_blocks = blocks_since(client.latest_height, cs_height) - .expect("less or equal height"); - ( - cs_height, - HostBlock::generate_tm_block( - client.client_chain_id.clone(), - cs_height.revision_height(), - client - .latest_timestamp - .sub(self.block_time * (n_blocks as u32)) - .expect("never fails"), - ), - ) - }) - .collect(); - - let client_state: TmClientState = TmClientStateConfig::builder() - .chain_id(client.client_chain_id) - .latest_height(client.latest_height) - .trusting_period(client.trusting_period) - .max_clock_drift(client.max_clock_drift) - .unbonding_period(client.unbonding_period) - .build() - .try_into() - .expect("never fails"); - - client_state.inner().validate().expect("never fails"); - - let cs_states = blocks - .into_iter() - .map(|(height, block)| (height, block.into())) - .collect(); - - (client_state.into(), cs_states) - } - _ => panic!("unknown client type"), - }; - - let client_record = MockClientRecord { - client_state: Some(client_state), - consensus_states, - }; - - self.ibc_store - .lock() - .clients - .insert(client.client_id, client_record); - self - } - - /// Associates a connection to this context. - pub fn with_connection( - self, - connection_id: ConnectionId, - connection_end: ConnectionEnd, - ) -> Self { - self.ibc_store + fn store_host_consensus_state(&mut self, height: u64, consensus_state: AnyConsensusState) { + self.host_consensus_states .lock() - .connections - .insert(connection_id, connection_end); - self + .insert(height, consensus_state); } - /// Associates a channel (in an arbitrary state) to this context. - pub fn with_channel( - self, - port_id: PortId, - chan_id: ChannelId, - channel_end: ChannelEnd, - ) -> Self { - let mut channels = self.ibc_store.lock().channels.clone(); - channels - .entry(port_id) - .or_default() - .insert(chan_id, channel_end); - self.ibc_store.lock().channels = channels; - self - } - - pub fn with_send_sequence( - self, - port_id: PortId, - chan_id: ChannelId, - seq_number: Sequence, - ) -> Self { - let mut next_sequence_send = self.ibc_store.lock().next_sequence_send.clone(); - next_sequence_send - .entry(port_id) - .or_default() - .insert(chan_id, seq_number); - self.ibc_store.lock().next_sequence_send = next_sequence_send; - self - } - - pub fn with_recv_sequence( - self, - port_id: PortId, - chan_id: ChannelId, - seq_number: Sequence, - ) -> Self { - let mut next_sequence_recv = self.ibc_store.lock().next_sequence_recv.clone(); - next_sequence_recv - .entry(port_id) - .or_default() - .insert(chan_id, seq_number); - self.ibc_store.lock().next_sequence_recv = next_sequence_recv; - self - } - - pub fn with_ack_sequence( - self, - port_id: PortId, - chan_id: ChannelId, - seq_number: Sequence, - ) -> Self { - let mut next_sequence_ack = self.ibc_store.lock().next_sequence_send.clone(); - next_sequence_ack - .entry(port_id) - .or_default() - .insert(chan_id, seq_number); - self.ibc_store.lock().next_sequence_ack = next_sequence_ack; - self - } - - pub fn with_height(self, target_height: Height) -> Self { - let latest_height = self.latest_height(); - if target_height.revision_number() > latest_height.revision_number() { - unimplemented!() - } 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() < latest_height.revision_height() { - panic!("Cannot rewind history of the chain to a smaller 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 = self; - while ctx.latest_height().revision_height() < target_height.revision_height() { - ctx.advance_host_chain_height() - } - ctx - } else { - // Both the revision number and height match - self - } + fn store_ibc_commitment_proof(&mut self, height: u64, proof: CommitmentProof) { + self.ibc_commiment_proofs.lock().insert(height, proof); } - pub fn with_packet_commitment( - self, - port_id: PortId, - chan_id: ChannelId, - seq: Sequence, - data: PacketCommitment, - ) -> Self { - let mut packet_commitment = self.ibc_store.lock().packet_commitment.clone(); - packet_commitment - .entry(port_id) - .or_default() - .entry(chan_id) - .or_default() - .insert(seq, data); - self.ibc_store.lock().packet_commitment = packet_commitment; - self - } - - /// Accessor for a block of the local (host) chain from this context. - /// 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(); - let latest = self.latest_height().revision_height(); - - // Check that the block is not too advanced, nor has it been pruned. - if (target > latest) || (target <= latest - self.history.len() as u64) { - None // Block for requested height does not exist in history. - } else { - Some(&self.history[self.history.len() + target as usize - latest as usize - 1]) - } - } - - /// 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, - latest_block.height().increment().revision_height(), - latest_block - .timestamp() - .add(self.block_time) - .expect("Never fails"), - ); - - // Append the new header at the tip of the history. - if self.history.len() as u64 >= 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 as usize - 1] = new_block; - } else { - // History is not full yet. - self.history.push(new_block); - } - } - - /// 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( + pub fn begin_block( &mut self, - router: &mut impl Router, - msg: MsgEnvelope, - ) -> Result<(), RelayerError> { - dispatch(self, router, msg).map_err(RelayerError::TransactionFailed)?; - // Create a new block. - self.advance_host_chain_height(); - Ok(()) - } - - /// Validates this context. Should be called after the context is mutated by a test. - pub fn validate(&self) -> Result<(), String> { - // Check that the number of entries is not higher than window size. - if self.history.len() as u64 > self.max_history_size { - return Err("too many entries".to_string()); - } - - // Check the content of the history. - if !self.history.is_empty() { - // 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() { - return Err("latest height is not updated".to_string()); - } - } - - // 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]; - if ph.height().increment() != h.height() { - return Err("headers in history not sequential".to_string()); - } - } - Ok(()) + height: u64, + consensus_state: AnyConsensusState, + proof: CommitmentProof, + ) { + assert_eq!(self.store.current_height(), height); + self.store_host_consensus_state(height, consensus_state); + self.store_ibc_commitment_proof(height, proof); } - pub fn latest_client_states(&self, client_id: &ClientId) -> AnyClientState { - self.ibc_store.lock().clients[client_id] - .client_state - .as_ref() - .expect("Never fails") - .clone() + pub fn end_block(&mut self) -> Result, as Store>::Error> { + self.store.commit() } - pub fn latest_consensus_states( - &self, - client_id: &ClientId, - height: &Height, - ) -> AnyConsensusState { - self.ibc_store.lock().clients[client_id] - .consensus_states - .get(height) - .expect("Never fails") - .clone() - } - - pub fn latest_height(&self) -> Height { - self.history - .last() - .expect("history cannot be empty") - .height() - } - - pub fn ibc_store_share(&self) -> Arc> { - self.ibc_store.clone() - } - - pub fn query_latest_header(&self) -> Option { - let block_ref = self.host_block(&self.host_height().expect("Never fails")); - block_ref.cloned() - } - - pub fn get_events(&self) -> Vec { - self.ibc_store.lock().events.clone() + pub fn prune_host_consensus_states_till(&self, height: &Height) { + assert!(height.revision_number() == *self.revision_number.lock()); + let mut history = self.host_consensus_states.lock(); + history.retain(|h, _| h > &height.revision_height()); + let mut commitment_proofs = self.ibc_commiment_proofs.lock(); + commitment_proofs.retain(|h, _| h > &height.revision_height()); } +} - pub fn get_logs(&self) -> Vec { - self.ibc_store.lock().logs.clone() +impl Default for MockIbcStore +where + S: ProvableStore + Debug + Default, +{ + fn default() -> Self { + // Note: this creates a MockIbcStore which has MockConsensusState as Host ConsensusState + let mut ibc_store = Self::new(0, S::default()); + ibc_store.store.commit().expect("no error"); + ibc_store.store_host_consensus_state( + ibc_store.store.current_height(), + MockHeader::default().into_consensus_state().into(), + ); + ibc_store.store_ibc_commitment_proof( + ibc_store.store.current_height(), + CommitmentProof::default(), + ); + ibc_store } } @@ -784,8 +205,10 @@ mod tests { use ibc::core::channel::types::error::{ChannelError, PacketError}; use ibc::core::channel::types::packet::Packet; use ibc::core::channel::types::Version; + use ibc::core::host::types::identifiers::{ChannelId, PortId}; use ibc::core::primitives::Signer; use ibc::core::router::module::Module; + use ibc::core::router::router::Router; use ibc::core::router::types::module::{ModuleExtras, ModuleId}; use super::*; @@ -793,145 +216,6 @@ mod tests { use crate::fixtures::core::signer::dummy_bech32_account; use crate::testapp::ibc::core::router::MockRouter; - #[test] - fn test_history_manipulation() { - pub struct Test { - name: String, - ctx: MockContext, - } - let cv = 1; // The version to use for all chains. - - let mock_chain_id = ChainId::new(&format!("mockgaia-{cv}")).unwrap(); - - let tests: Vec = vec![ - Test { - name: "Empty history, small pruning window".to_string(), - ctx: MockContextConfig::builder() - .host_id(mock_chain_id.clone()) - .max_history_size(2) - .latest_height(Height::new(cv, 1).expect("Never fails")) - .build(), - }, - Test { - name: "[Synthetic TM host] Empty history, small pruning window".to_string(), - ctx: MockContextConfig::builder() - .host_id(mock_chain_id.clone()) - .host_type(HostType::SyntheticTendermint) - .max_history_size(2) - .latest_height(Height::new(cv, 1).expect("Never fails")) - .build(), - }, - Test { - name: "Large pruning window".to_string(), - ctx: MockContextConfig::builder() - .host_id(mock_chain_id.clone()) - .max_history_size(30) - .latest_height(Height::new(cv, 2).expect("Never fails")) - .build(), - }, - Test { - name: "[Synthetic TM host] Large pruning window".to_string(), - ctx: MockContextConfig::builder() - .host_id(mock_chain_id.clone()) - .host_type(HostType::SyntheticTendermint) - .max_history_size(30) - .latest_height(Height::new(cv, 2).expect("Never fails")) - .build(), - }, - Test { - name: "Small pruning window".to_string(), - ctx: MockContextConfig::builder() - .host_id(mock_chain_id.clone()) - .max_history_size(3) - .latest_height(Height::new(cv, 30).expect("Never fails")) - .build(), - }, - Test { - name: "[Synthetic TM host] Small pruning window".to_string(), - ctx: MockContextConfig::builder() - .host_id(mock_chain_id.clone()) - .host_type(HostType::SyntheticTendermint) - .max_history_size(3) - .latest_height(Height::new(cv, 30).expect("Never fails")) - .build(), - }, - Test { - name: "Small pruning window, small starting height".to_string(), - ctx: MockContextConfig::builder() - .host_id(mock_chain_id.clone()) - .max_history_size(3) - .latest_height(Height::new(cv, 2).expect("Never fails")) - .build(), - }, - Test { - name: "[Synthetic TM host] Small pruning window, small starting height".to_string(), - ctx: MockContextConfig::builder() - .host_id(mock_chain_id.clone()) - .host_type(HostType::SyntheticTendermint) - .max_history_size(3) - .latest_height(Height::new(cv, 2).expect("Never fails")) - .build(), - }, - Test { - name: "Large pruning window, large starting height".to_string(), - ctx: MockContextConfig::builder() - .host_id(mock_chain_id.clone()) - .max_history_size(50) - .latest_height(Height::new(cv, 2000).expect("Never fails")) - .build(), - }, - Test { - name: "[Synthetic TM host] Large pruning window, large starting height".to_string(), - ctx: MockContextConfig::builder() - .host_id(mock_chain_id) - .host_type(HostType::SyntheticTendermint) - .max_history_size(50) - .latest_height(Height::new(cv, 2000).expect("Never fails")) - .build(), - }, - ]; - - for mut test in tests { - // All tests should yield a valid context after initialization. - assert!( - test.ctx.validate().is_ok(), - "failed in test {} while validating context {:?}", - test.name, - test.ctx - ); - - 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(); - assert!( - test.ctx.validate().is_ok(), - "failed in test {} while validating context {:?}", - test.name, - test.ctx - ); - - let next_height = current_height.increment(); - assert_eq!( - test.ctx.latest_height(), - next_height, - "failed while increasing height for context {:?}", - test.ctx - ); - - assert_eq!( - test.ctx - .host_block(¤t_height) - .expect("Never fails") - .height(), - current_height, - "failed while fetching height {:?} of context {:?}", - current_height, - test.ctx - ); - } - } - #[test] fn test_router() { #[derive(Debug, Default)] @@ -1158,3 +442,63 @@ mod tests { ]; } } + +pub struct LightClientState { + pub client_state: H::ClientState, + pub consensus_states: + BTreeMap::Header as TestHeader>::ConsensusState>, +} + +impl Default for LightClientState +where + H: TestHost, + HostClientState: ClientStateValidation, +{ + fn default() -> Self { + let context = TestContext::::default(); + LightClientBuilder::init().context(&context).build() + } +} + +impl LightClientState +where + H: TestHost, + HostClientState: ClientStateValidation, +{ + pub fn with_latest_height(height: Height) -> Self { + let context = TestContextConfig::builder() + .latest_height(height) + .build::>(); + LightClientBuilder::init().context(&context).build() + } +} + +#[derive(TypedBuilder)] +#[builder(builder_method(name = init), build_method(into))] +pub struct LightClientBuilder<'a, H> +where + H: TestHost, + HostClientState: ClientStateValidation, +{ + context: &'a TestContext, + #[builder(default, setter(into))] + consensus_heights: Vec, + #[builder(default)] + params: H::LightClientParams, +} + +impl<'a, H> From> for LightClientState +where + H: TestHost, + HostClientState: ClientStateValidation, +{ + fn from(builder: LightClientBuilder<'a, H>) -> Self { + let LightClientBuilder { + context, + consensus_heights, + params, + } = builder; + + context.generate_light_client(consensus_heights, ¶ms) + } +} diff --git a/ibc-testkit/src/utils/mod.rs b/ibc-testkit/src/utils/mod.rs new file mode 100644 index 000000000..80eb0bad4 --- /dev/null +++ b/ibc-testkit/src/utils/mod.rs @@ -0,0 +1,17 @@ +use ibc::primitives::Timestamp; +use tendermint::Time; + +/// Returns a `Timestamp` representation of beginning of year 2023. +/// +/// This is introduced to initialize [`StoreGenericTestContext`](crate::context::StoreGenericTestContext)s +/// with the same latest timestamp by default. +/// If two [`StoreGenericTestContext`](crate::context::StoreGenericTestContext) +/// are initialized using [`Time::now()`], second one will have a greater timestamp than the first one. +/// So, the latest header of the second context can not be submitted to first one. +/// We can still set a custom timestamp via [`TestContextConfig`](crate::fixtures::core::context::TestContextConfig). +pub fn year_2023() -> Timestamp { + // Sun Jan 01 2023 00:00:00 GMT+0000 + Time::from_unix_timestamp(1_672_531_200, 0) + .expect("should be a valid time") + .into() +} diff --git a/ibc-testkit/tests/core/ics02_client/create_client.rs b/ibc-testkit/tests/core/ics02_client/create_client.rs index 447af33fd..3f8bd4bc8 100644 --- a/ibc-testkit/tests/core/ics02_client/create_client.rs +++ b/ibc-testkit/tests/core/ics02_client/create_client.rs @@ -1,3 +1,4 @@ +use basecoin_store::impls::InMemoryStore; use ibc::clients::tendermint::types::{ client_type as tm_client_type, ConsensusState as TmConsensusState, }; @@ -6,26 +7,33 @@ use ibc::core::client::context::ClientValidationContext; use ibc::core::client::types::error::ClientError; use ibc::core::client::types::msgs::{ClientMsg, MsgCreateClient}; use ibc::core::client::types::Height; +use ibc::core::commitment_types::error::CommitmentError; use ibc::core::entrypoint::{execute, validate}; use ibc::core::handler::types::error::ContextError; use ibc::core::handler::types::msgs::MsgEnvelope; +use ibc::core::host::types::identifiers::ClientId; +use ibc::core::host::types::path::{ClientConsensusStatePath, NextClientSequencePath}; use ibc::core::host::{ClientStateRef, ValidationContext}; +use ibc_query::core::context::ProvableContext; +use ibc_testkit::context::{MockContext, TendermintContext}; use ibc_testkit::fixtures::clients::tendermint::{ dummy_tendermint_header, dummy_tm_client_state_from_header, }; +use ibc_testkit::fixtures::core::context::TestContextConfig; use ibc_testkit::fixtures::core::signer::dummy_account_id; use ibc_testkit::testapp::ibc::clients::mock::client_state::{ client_type as mock_client_type, MockClientState, }; use ibc_testkit::testapp::ibc::clients::mock::consensus_state::MockConsensusState; use ibc_testkit::testapp::ibc::clients::mock::header::MockHeader; +use ibc_testkit::testapp::ibc::clients::{AnyClientState, AnyConsensusState}; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::MockContext; +use ibc_testkit::testapp::ibc::core::types::{DefaultIbcStore, LightClientBuilder, MockIbcStore}; use test_log::test; #[test] fn test_create_client_ok() { - let mut ctx = MockContext::default(); + let mut ctx = DefaultIbcStore::default(); let mut router = MockRouter::new_with_transfer(); let signer = dummy_account_id(); let height = Height::new(0, 42).unwrap(); @@ -49,7 +57,8 @@ fn test_create_client_ok() { assert!(res.is_ok(), "execution happy path"); - let expected_client_state = ClientStateRef::::try_from(msg.client_state).unwrap(); + let expected_client_state = + ClientStateRef::::try_from(msg.client_state).unwrap(); assert_eq!(expected_client_state.client_type(), client_type); assert_eq!(ctx.client_state(&client_id).unwrap(), expected_client_state); } @@ -58,7 +67,7 @@ fn test_create_client_ok() { fn test_tm_create_client_ok() { let signer = dummy_account_id(); - let mut ctx = MockContext::default(); + let mut ctx = DefaultIbcStore::default(); let mut router = MockRouter::new_with_transfer(); @@ -85,7 +94,8 @@ fn test_tm_create_client_ok() { assert!(res.is_ok(), "tendermint client execution happy path"); - let expected_client_state = ClientStateRef::::try_from(msg.client_state).unwrap(); + let expected_client_state = + ClientStateRef::>::try_from(msg.client_state).unwrap(); assert_eq!(expected_client_state.client_type(), client_type); assert_eq!(ctx.client_state(&client_id).unwrap(), expected_client_state); } @@ -94,7 +104,7 @@ fn test_tm_create_client_ok() { fn test_invalid_frozen_tm_client_creation() { let signer = dummy_account_id(); - let ctx = MockContext::default(); + let ctx = DefaultIbcStore::default(); let router = MockRouter::new_with_transfer(); @@ -111,12 +121,94 @@ fn test_invalid_frozen_tm_client_creation() { signer, ); - let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg.clone())); + let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg)); - let res = validate(&ctx, &router, msg_envelope.clone()); + let res = validate(&ctx, &router, msg_envelope); assert!(matches!( res, Err(ContextError::ClientError(ClientError::ClientFrozen { .. })) )) } + +#[test] +fn test_tm_create_client_proof_verification_ok() { + let client_id = ClientId::new("07-tendermint", 0).expect("no error"); + let client_height = Height::new(0, 10).expect("no error"); + + let ctx_tm = TestContextConfig::builder() + .latest_height(client_height) + .build::(); + + let ctx_mk = MockContext::default().with_light_client( + &client_id, + LightClientBuilder::init().context(&ctx_tm).build(), + ); + + let client_validation_ctx_mk = ctx_mk.ibc_store().get_client_validation_context(); + + let (AnyClientState::Tendermint(tm_client_state),) = (client_validation_ctx_mk + .client_state(&client_id) + .expect("client state exists"),) + else { + panic!("client state is not valid") + }; + + let latest_client_height = tm_client_state.latest_height(); + let consensus_state_path = ClientConsensusStatePath::new( + client_id.clone(), + latest_client_height.revision_number(), + latest_client_height.revision_height(), + ); + + let AnyConsensusState::Tendermint(tm_consensus_state) = client_validation_ctx_mk + .consensus_state(&consensus_state_path) + .expect("consensus_state exists") + else { + panic!("consensus state is not valid") + }; + + let next_client_seq_path = NextClientSequencePath; + let next_client_seq_value = client_validation_ctx_mk + .client_counter() + .expect("counter exists"); + + assert_eq!( + next_client_seq_value, 0, + "client counter is not incremented" + ); + + let proof = ctx_tm + .ibc_store() + .get_proof(ctx_tm.latest_height(), &next_client_seq_path.clone().into()) + .expect("proof exists") + .try_into() + .expect("value merkle proof"); + + let root = tm_consensus_state.inner().root(); + + // correct value verification + tm_client_state + .verify_membership( + &ctx_tm.ibc_store().commitment_prefix(), + &proof, + &root, + next_client_seq_path.clone().into(), + serde_json::to_vec(&next_client_seq_value).expect("valid json serialization"), + ) + .expect("successful proof verification"); + + // incorrect value verification + assert!(matches!( + tm_client_state + .verify_membership( + &ctx_tm.ibc_store().commitment_prefix(), + &proof, + &root, + next_client_seq_path.into(), + serde_json::to_vec(&(next_client_seq_value + 1)).expect("valid json serialization"), + ) + .expect_err("proof verification fails"), + ClientError::Ics23Verification(CommitmentError::VerificationFailure) + )); +} diff --git a/ibc-testkit/tests/core/ics02_client/recover_client.rs b/ibc-testkit/tests/core/ics02_client/recover_client.rs index e99098d65..28f1de076 100644 --- a/ibc-testkit/tests/core/ics02_client/recover_client.rs +++ b/ibc-testkit/tests/core/ics02_client/recover_client.rs @@ -1,27 +1,28 @@ use core::time::Duration; +use ibc::core::client::context::client_state::ClientStateValidation; use ibc::core::client::context::ClientValidationContext; use ibc::core::client::handler::recover_client; use ibc::core::client::types::msgs::{ClientMsg, MsgCreateClient, MsgRecoverClient}; -use ibc::core::client::types::Height; -use ibc::core::entrypoint::{execute, validate}; +use ibc::core::client::types::{Height, Status as ClientStatus}; use ibc::core::handler::types::msgs::MsgEnvelope; use ibc::core::host::types::identifiers::ClientId; use ibc::core::host::types::path::ClientConsensusStatePath; use ibc::core::host::ValidationContext; -use ibc::core::primitives::{Signer, Timestamp}; +use ibc::core::primitives::Signer; +use ibc_testkit::context::{MockContext, TendermintContext}; +use ibc_testkit::fixtures::core::context::TestContextConfig; use ibc_testkit::fixtures::core::signer::dummy_account_id; +use ibc_testkit::hosts::{TestBlock, TestHost}; use ibc_testkit::testapp::ibc::clients::mock::client_state::{ client_type as mock_client_type, MockClientState, }; use ibc_testkit::testapp::ibc::clients::mock::consensus_state::MockConsensusState; -use ibc_testkit::testapp::ibc::clients::mock::header::MockHeader; -use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::MockContext; +use ibc_testkit::testapp::ibc::core::types::DEFAULT_BLOCK_TIME_SECS; use rstest::*; struct Fixture { - ctx: MockContext, + ctx: TendermintContext, subject_client_id: ClientId, substitute_client_id: ClientId, signer: Signer, @@ -41,50 +42,70 @@ fn setup_client_recovery_fixture( substitute_trusting_period: Duration, substitute_height: Height, ) -> Fixture { - let mut ctx = MockContext::default(); - let mut router = MockRouter::new_with_transfer(); - let signer = dummy_account_id(); + let mut ctx = TendermintContext::default(); + + // create a ctx_b + let ctx_b: MockContext = TestContextConfig::builder() + .latest_height(substitute_height) + .build(); - let subject_timestamp = (Timestamp::now() - subject_trusting_period).unwrap(); + let signer = dummy_account_id(); - // Create the subject client state such that it will be in an expired state by initializing it with - // a timestamp that is of duration `subject_trusting_period` in the past - let subject_client_state = - MockClientState::new(MockHeader::new(subject_height).with_timestamp(subject_timestamp)) - .with_trusting_period(subject_trusting_period); + let subject_client_header = ctx_b + .host + .get_block(&subject_height) + .expect("block exists") + .into_header(); - // Create the subject client let msg = MsgCreateClient::new( - subject_client_state.into(), - MockConsensusState::new(MockHeader::new(subject_height).with_current_timestamp()).into(), + MockClientState::new(subject_client_header) + .with_trusting_period(subject_trusting_period) + .into(), + MockConsensusState::new(subject_client_header).into(), signer.clone(), ); - let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg)); + let subject_msg_envelope = MsgEnvelope::from(ClientMsg::from(msg)); let client_type = mock_client_type(); - let subject_client_id = client_type.build_client_id(ctx.client_counter().unwrap()); + let subject_client_id = client_type.build_client_id(ctx.ibc_store().client_counter().unwrap()); - validate(&ctx, &router, msg_envelope.clone()).expect("create subject client validation"); - execute(&mut ctx, &mut router, msg_envelope).expect("create subject client execution"); + ctx.dispatch(subject_msg_envelope) + .expect("create subject client execution"); - // Create the substitute client - let substitute_client_state = - MockClientState::new(MockHeader::new(substitute_height).with_current_timestamp()) - .with_trusting_period(substitute_trusting_period); + let substitute_client_header = ctx_b + .host + .get_block(&substitute_height) + .expect("block exists") + .into_header(); let msg = MsgCreateClient::new( - substitute_client_state.into(), - MockConsensusState::new(MockHeader::new(substitute_height).with_current_timestamp()).into(), + MockClientState::new(substitute_client_header) + .with_trusting_period(substitute_trusting_period) + .into(), + MockConsensusState::new(substitute_client_header).into(), signer.clone(), ); - let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg)); + let substitute_msg_envelope = MsgEnvelope::from(ClientMsg::from(msg)); + + let substitute_client_id = + client_type.build_client_id(ctx.ibc_store().client_counter().unwrap()); - let substitute_client_id = client_type.build_client_id(ctx.client_counter().unwrap()); + ctx.dispatch(substitute_msg_envelope) + .expect("create substitute client execution"); - validate(&ctx, &router, msg_envelope.clone()).expect("create substitute client validation"); - execute(&mut ctx, &mut router, msg_envelope).expect("create substitute client execution"); + let subject_client_trusted_timestamp = + (subject_client_header.timestamp() + subject_trusting_period).expect("no error"); + + // Let the subject client state expire. + while ctx.latest_timestamp() <= subject_client_trusted_timestamp { + ctx.advance_block_height(); + } + + // at this point, the subject client should be expired. + // and the substitute client should be active or expired according + // to the fixture arguments Fixture { ctx, @@ -96,8 +117,8 @@ fn setup_client_recovery_fixture( #[rstest] fn test_recover_client_ok() { - let subject_trusting_period = Duration::from_nanos(100); - let substitute_trusting_period = Duration::from_secs(3); + let subject_trusting_period = Duration::from_secs(DEFAULT_BLOCK_TIME_SECS); + let substitute_trusting_period = Duration::from_secs(DEFAULT_BLOCK_TIME_SECS) * 10; let subject_height = Height::new(0, 42).unwrap(); let substitute_height = Height::new(0, 43).unwrap(); @@ -113,47 +134,70 @@ fn test_recover_client_ok() { substitute_height, ); + assert_eq!( + ctx.ibc_store() + .client_state(&subject_client_id) + .expect("substitute client state exists") + .status(ctx.ibc_store(), &subject_client_id) + .expect("no error"), + ClientStatus::Expired + ); + + assert_eq!( + ctx.ibc_store() + .client_state(&substitute_client_id) + .expect("substitute client state exists") + .status(ctx.ibc_store(), &substitute_client_id) + .expect("no error"), + ClientStatus::Active + ); + let msg = MsgRecoverClient { subject_client_id, substitute_client_id, signer, }; - let res = recover_client::validate(&ctx, msg.clone()); - - assert!(res.is_ok(), "client recovery validation happy path"); - - let res = recover_client::execute(&mut ctx, msg.clone()); + recover_client::validate(ctx.ibc_store_mut(), msg.clone()) + .expect("client recovery execution happy path"); - assert!(res.is_ok(), "client recovery execution happy path"); + recover_client::execute(ctx.ibc_store_mut(), msg.clone()) + .expect("client recovery execution happy path"); // client state is copied. assert_eq!( - ctx.client_state(&msg.subject_client_id).unwrap(), - ctx.client_state(&msg.substitute_client_id).unwrap(), + ctx.ibc_store() + .client_state(&msg.subject_client_id) + .unwrap(), + ctx.ibc_store() + .client_state(&msg.substitute_client_id) + .unwrap(), ); // latest consensus state is copied. assert_eq!( - ctx.consensus_state(&ClientConsensusStatePath::new( - msg.subject_client_id, - substitute_height.revision_number(), - substitute_height.revision_height(), - )) - .unwrap(), - ctx.consensus_state(&ClientConsensusStatePath::new( - msg.substitute_client_id, - substitute_height.revision_number(), - substitute_height.revision_height(), - )) - .unwrap(), + ctx.ibc_store() + .consensus_state(&ClientConsensusStatePath::new( + msg.subject_client_id, + substitute_height.revision_number(), + substitute_height.revision_height(), + )) + .unwrap(), + ctx.ibc_store() + .consensus_state(&ClientConsensusStatePath::new( + msg.substitute_client_id, + substitute_height.revision_number(), + substitute_height.revision_height(), + )) + .unwrap(), ); } #[rstest] fn test_recover_client_with_expired_substitute() { - let subject_trusting_period = Duration::from_nanos(100); - let substitute_trusting_period = Duration::from_nanos(100); + // twice of DEFAULT_BLOCK_TIME_SECS to make sure the substitute client is expired as well + let subject_trusting_period = Duration::from_secs(DEFAULT_BLOCK_TIME_SECS) * 2; + let substitute_trusting_period = Duration::from_secs(DEFAULT_BLOCK_TIME_SECS); let subject_height = Height::new(0, 42).unwrap(); let substitute_height = Height::new(0, 43).unwrap(); @@ -169,21 +213,38 @@ fn test_recover_client_with_expired_substitute() { substitute_height, ); + assert_eq!( + ctx.ibc_store() + .client_state(&subject_client_id) + .expect("substitute client state exists") + .status(ctx.ibc_store(), &subject_client_id) + .expect("no error"), + ClientStatus::Expired + ); + + assert_eq!( + ctx.ibc_store() + .client_state(&substitute_client_id) + .expect("substitute client state exists") + .status(ctx.ibc_store(), &substitute_client_id) + .expect("no error"), + ClientStatus::Expired + ); + let msg = MsgRecoverClient { subject_client_id, substitute_client_id, signer, }; - let res = recover_client::validate(&ctx, msg); - - assert!(res.is_err(), "expected client recovery validation to fail"); + recover_client::validate(ctx.ibc_store(), msg) + .expect_err("expected client recovery validation to fail"); } #[rstest] fn test_recover_client_with_matching_heights() { - let subject_trusting_period = Duration::from_nanos(100); - let substitute_trusting_period = Duration::from_secs(3); + let subject_trusting_period = Duration::from_secs(DEFAULT_BLOCK_TIME_SECS); + let substitute_trusting_period = Duration::from_secs(DEFAULT_BLOCK_TIME_SECS) * 10; let subject_height = Height::new(0, 42).unwrap(); let substitute_height = Height::new(0, 42).unwrap(); @@ -199,13 +260,30 @@ fn test_recover_client_with_matching_heights() { substitute_height, ); + assert_eq!( + ctx.ibc_store() + .client_state(&subject_client_id) + .expect("substitute client state exists") + .status(ctx.ibc_store(), &subject_client_id) + .expect("no error"), + ClientStatus::Expired + ); + + assert_eq!( + ctx.ibc_store() + .client_state(&substitute_client_id) + .expect("substitute client state exists") + .status(ctx.ibc_store(), &substitute_client_id) + .expect("no error"), + ClientStatus::Active + ); + let msg = MsgRecoverClient { subject_client_id, substitute_client_id, signer, }; - let res = recover_client::validate(&ctx, msg); - - assert!(res.is_err(), "expected client recovery validation to fail"); + dbg!(recover_client::validate(ctx.ibc_store(), msg)) + .expect_err("expected client recovery validation to fail"); } diff --git a/ibc-testkit/tests/core/ics02_client/update_client.rs b/ibc-testkit/tests/core/ics02_client/update_client.rs index 20828b529..9dbc49030 100644 --- a/ibc-testkit/tests/core/ics02_client/update_client.rs +++ b/ibc-testkit/tests/core/ics02_client/update_client.rs @@ -1,13 +1,15 @@ +use core::fmt::Debug; use core::str::FromStr; use core::time::Duration; +use basecoin_store::context::ProvableStore; use ibc::clients::tendermint::client_state::ClientState; use ibc::clients::tendermint::types::proto::v1::{ClientState as RawTmClientState, Fraction}; use ibc::clients::tendermint::types::{ client_type as tm_client_type, ClientState as TmClientState, Header as TmHeader, Misbehaviour as TmMisbehaviour, }; -use ibc::core::client::context::client_state::{ClientStateCommon, ClientStateValidation}; +use ibc::core::client::context::client_state::ClientStateValidation; use ibc::core::client::context::ClientValidationContext; use ibc::core::client::types::msgs::{ClientMsg, MsgUpdateClient}; use ibc::core::client::types::proto::v1::Height as RawHeight; @@ -22,9 +24,15 @@ use ibc::core::host::ValidationContext; use ibc::core::primitives::Timestamp; use ibc::primitives::proto::Any; use ibc::primitives::ToVec; -use ibc_testkit::fixtures::core::context::MockContextConfig; +use ibc_testkit::context::{MockContext, TendermintContext, TestContext}; +use ibc_testkit::fixtures::clients::tendermint::ClientStateConfig; +use ibc_testkit::fixtures::core::context::TestContextConfig; use ibc_testkit::fixtures::core::signer::dummy_account_id; -use ibc_testkit::hosts::block::{HostBlock, HostType}; +use ibc_testkit::hosts::tendermint::BlockParams; +use ibc_testkit::hosts::{ + HostClientState, MockHost, TendermintHost, TestBlock, TestHeader, TestHost, +}; +use ibc_testkit::relayer::error::RelayerError; use ibc_testkit::testapp::ibc::clients::mock::client_state::{ client_type as mock_client_type, MockClientState, }; @@ -32,9 +40,12 @@ use ibc_testkit::testapp::ibc::clients::mock::header::MockHeader; use ibc_testkit::testapp::ibc::clients::mock::misbehaviour::Misbehaviour as MockMisbehaviour; use ibc_testkit::testapp::ibc::clients::AnyConsensusState; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::{MockClientConfig, MockContext}; +use ibc_testkit::testapp::ibc::core::types::{ + DefaultIbcStore, LightClientBuilder, LightClientState, MockIbcStore, +}; use rstest::*; use tendermint_testgen::Validator as TestgenValidator; +use tracing::debug; struct Fixture { ctx: MockContext, @@ -45,11 +56,9 @@ struct Fixture { fn fixture() -> Fixture { let client_id = ClientId::new("07-tendermint", 0).expect("no error"); - let ctx = MockContext::default().with_client_config( - MockClientConfig::builder() - .client_id(client_id.clone()) - .latest_height(Height::new(0, 42).unwrap()) - .build(), + let ctx = MockContext::default().with_light_client( + &client_id, + LightClientState::::with_latest_height(Height::new(0, 42).unwrap()), ); let router = MockRouter::new_with_transfer(); @@ -95,16 +104,16 @@ fn test_update_client_ok(fixture: Fixture) { let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg.clone())); - let res = validate(&ctx, &router, msg_envelope.clone()); + let res = validate(&ctx.ibc_store, &router, msg_envelope.clone()); assert!(res.is_ok(), "validation happy path"); - let res = execute(&mut ctx, &mut router, msg_envelope); + let res = execute(&mut ctx.ibc_store, &mut router, msg_envelope); assert!(res.is_ok(), "execution happy path"); assert_eq!( - ctx.client_state(&msg.client_id).unwrap(), + ctx.ibc_store.client_state(&msg.client_id).unwrap(), MockClientState::new(MockHeader::new(height).with_timestamp(timestamp)).into() ); } @@ -120,14 +129,22 @@ fn test_update_client_with_prev_header() { let height_1 = Height::new(0, 43).unwrap(); let height_2 = Height::new(0, 44).unwrap(); - let mut ctx = MockContext::default().with_client_config( - MockClientConfig::builder() - .client_chain_id(chain_id_b.clone()) - .client_type(tm_client_type()) - .client_id(client_id.clone()) - .latest_height(latest_height) - .build(), - ); + let ctx_b = TestContextConfig::builder() + .host( + TendermintHost::builder() + .chain_id(chain_id_b.clone()) + .build(), + ) + .latest_height(latest_height) + .build::(); + + let mut ctx = MockContext::default() + .with_light_client( + &client_id, + LightClientBuilder::init().context(&ctx_b).build(), + ) + .ibc_store; + let mut router = MockRouter::new_with_transfer(); fn build_msg_from_header( @@ -136,13 +153,18 @@ fn test_update_client_with_prev_header() { target_height: Height, trusted_height: Height, ) -> MsgEnvelope { - let mut tm_block = HostBlock::generate_tm_block( - chain_id, - target_height.revision_height(), - Timestamp::now(), - ); - - tm_block.trusted_height = trusted_height; + let mut tm_block = TendermintHost::builder() + .chain_id(chain_id) + .build() + .generate_block( + Vec::new(), + target_height.revision_height(), + Timestamp::now(), + &Default::default(), + ) + .into_header(); + + tm_block.set_trusted_height(trusted_height); let msg = MsgUpdateClient { client_id, @@ -187,8 +209,8 @@ fn test_update_client_with_prev_header() { /// Tests that the Tendermint client consensus state pruning logic /// functions correctly. /// -/// This test sets up a MockContext with host height 1 and a trusting -/// period of 3 seconds. It then advances the state of the MockContext +/// This test sets up a `TendermintContext` with host height 1 and a trusting +/// period of 3 seconds. It then advances the state of the `TendermintContext` /// by 2 heights, and thus 6 seconds, due to the DEFAULT_BLOCK_TIME_SECS /// constant being set to 3 seconds. At this point, the chain is at height /// 3. Any consensus states associated with a block more than 3 seconds @@ -204,26 +226,31 @@ fn test_consensus_state_pruning() { let client_id = tm_client_type().build_client_id(0); - let mut ctx = MockContextConfig::builder() - .host_id(chain_id.clone()) - .host_type(HostType::SyntheticTendermint) + let ctx_b = TestContextConfig::builder() + .host(TendermintHost::builder().chain_id(chain_id.clone()).build()) + .latest_height(client_height) + .build::(); + + let mut ctx = TestContextConfig::builder() + .host(TendermintHost::builder().chain_id(chain_id).build()) .latest_height(client_height) .latest_timestamp(Timestamp::now()) - .max_history_size(u64::MAX) - .build() - .with_client_config( - MockClientConfig::builder() - .client_chain_id(chain_id.clone()) - .client_id(client_id.clone()) - .latest_height(client_height) - .client_type(tm_client_type()) - .trusting_period(Duration::from_secs(3)) + .build::() + .with_light_client( + &client_id, + LightClientBuilder::init() + .context(&ctx_b) + .params( + ClientStateConfig::builder() + .trusting_period(Duration::from_secs(3)) + .build(), + ) .build(), ); let mut router = MockRouter::new_with_transfer(); - let start_host_timestamp = ctx.host_timestamp().unwrap(); + let start_host_timestamp = ctx.ibc_store.host_timestamp().unwrap(); // Move the chain forward by 2 blocks to pass the trusting period. for _ in 1..=2 { @@ -231,9 +258,10 @@ fn test_consensus_state_pruning() { let update_height = ctx.latest_height(); - ctx.advance_host_chain_height(); + ctx.advance_block_height(); - let mut block = ctx.host_block(&update_height).unwrap().clone(); + let block = ctx.host_block(&update_height).unwrap().clone(); + let mut block = block.into_header(); block.set_trusted_height(client_height); @@ -245,8 +273,8 @@ fn test_consensus_state_pruning() { let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg)); - let _ = validate(&ctx, &router, msg_envelope.clone()); - let _ = execute(&mut ctx, &mut router, msg_envelope); + let _ = validate(&ctx.ibc_store, &router, msg_envelope.clone()); + let _ = execute(&mut ctx.ibc_store, &mut router, msg_envelope); } // Check that latest expired consensus state is pruned. @@ -256,8 +284,14 @@ fn test_consensus_state_pruning() { expired_height.revision_number(), expired_height.revision_height(), ); - assert!(ctx.client_update_meta(&client_id, &expired_height).is_err()); - assert!(ctx.consensus_state(&client_cons_state_path).is_err()); + assert!(ctx + .ibc_store + .client_update_meta(&client_id, &expired_height) + .is_err()); + assert!(ctx + .ibc_store + .consensus_state(&client_cons_state_path) + .is_err()); // Check that latest valid consensus state exists. let earliest_valid_height = Height::new(1, 2).unwrap(); @@ -268,11 +302,15 @@ fn test_consensus_state_pruning() { ); assert!(ctx + .ibc_store .client_update_meta(&client_id, &earliest_valid_height) .is_ok()); - assert!(ctx.consensus_state(&client_cons_state_path).is_ok()); + assert!(ctx + .ibc_store + .consensus_state(&client_cons_state_path) + .is_ok()); - let end_host_timestamp = ctx.host_timestamp().unwrap(); + let end_host_timestamp = ctx.ibc_store.host_timestamp().unwrap(); assert_eq!( end_host_timestamp, @@ -294,7 +332,7 @@ fn test_update_nonexisting_client(fixture: Fixture) { let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg)); - let res = validate(&ctx, &router, msg_envelope); + let res = validate(&ctx.ibc_store, &router, msg_envelope); assert!(res.is_err()); } @@ -306,30 +344,33 @@ fn test_update_synthetic_tendermint_client_adjacent_ok() { let update_height = Height::new(1, 21).unwrap(); let chain_id_b = ChainId::new("mockgaiaB-1").unwrap(); - let mut ctx = MockContextConfig::builder() - .host_id(ChainId::new("mockgaiaA-1").unwrap()) + let ctx_b = TestContextConfig::builder() + .host(TendermintHost::builder().chain_id(chain_id_b).build()) + .latest_height(update_height) + .build::(); + + let mut ctx = TestContextConfig::builder() + .host( + MockHost::builder() + .chain_id(ChainId::new("mockgaiaA-1").unwrap()) + .build(), + ) .latest_height(Height::new(1, 1).unwrap()) - .build() - .with_client_config( - MockClientConfig::builder() - .client_chain_id(chain_id_b.clone()) - .client_id(client_id.clone()) - .client_type(tm_client_type()) // The target host chain (B) is synthetic TM. - .latest_height(client_height) + .build::() + .with_light_client( + &client_id, + LightClientBuilder::init() + .context(&ctx_b) + .consensus_heights([client_height]) .build(), ); let mut router = MockRouter::new_with_transfer(); - let ctx_b = MockContextConfig::builder() - .host_id(chain_id_b) - .host_type(HostType::SyntheticTendermint) - .latest_height(update_height) - .build(); - let signer = dummy_account_id(); - let mut block = ctx_b.host_block(&update_height).unwrap().clone(); + let block = ctx_b.host_block(&update_height).unwrap(); + let mut block = block.into_header(); block.set_trusted_height(client_height); let latest_header_height = block.height(); @@ -340,16 +381,16 @@ fn test_update_synthetic_tendermint_client_adjacent_ok() { }; let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg.clone())); - let res = validate(&ctx, &router, msg_envelope.clone()); + let res = validate(&ctx.ibc_store, &router, msg_envelope.clone()); assert!(res.is_ok()); - let res = execute(&mut ctx, &mut router, msg_envelope); + let res = execute(&mut ctx.ibc_store, &mut router, msg_envelope); assert!(res.is_ok(), "result: {res:?}"); - let client_state = ctx.client_state(&msg.client_id).unwrap(); + let client_state = ctx.ibc_store.client_state(&msg.client_id).unwrap(); assert!(client_state - .status(&ctx, &msg.client_id) + .status(&ctx.ibc_store, &msg.client_id) .unwrap() .is_active()); @@ -362,68 +403,74 @@ fn test_update_synthetic_tendermint_client_validator_change_ok() { let client_height = Height::new(1, 20).unwrap(); let chain_id_b = ChainId::new("mockgaiaB-1").unwrap(); - let mut ctx_a = MockContextConfig::builder() - .host_id(ChainId::new("mockgaiaA-1").unwrap()) - .latest_height(Height::new(1, 1).unwrap()) - .build() - .with_client_config( - // client state initialized with client_height, and - // [{id: 1, power: 50}, {id: 2, power: 50}] for validator set and next validator set. - MockClientConfig::builder() - .client_chain_id(chain_id_b.clone()) - .client_id(client_id.clone()) - .latest_height(client_height) - .client_type(tm_client_type()) - .build(), - ); - - let mut router_a = MockRouter::new_with_transfer(); - let ctx_b_val_history = vec![ - // First two validator sets are default at client creation - // // validator set of height-20 vec![ TestgenValidator::new("1").voting_power(50), TestgenValidator::new("2").voting_power(50), ], + // next validator set of height-20 // validator set of height-21 vec![ - TestgenValidator::new("1").voting_power(50), - TestgenValidator::new("2").voting_power(50), + TestgenValidator::new("1").voting_power(34), + TestgenValidator::new("2").voting_power(66), ], + // next validator set of height-21 // validator set of height-22 + // overlap maintains 1/3 power in older set vec![ - TestgenValidator::new("1").voting_power(30), - TestgenValidator::new("2").voting_power(70), + TestgenValidator::new("1").voting_power(1), + TestgenValidator::new("4").voting_power(99), ], - // validator set of height-23 + // next validator set of height-22 vec![ TestgenValidator::new("1").voting_power(20), TestgenValidator::new("2").voting_power(80), ], ]; - let update_height = client_height.add(ctx_b_val_history.len() as u64 - 2); + let block_params = BlockParams::from_validator_history(ctx_b_val_history); + + let update_height = client_height.add(block_params.len() as u64 - 1); + + assert_eq!(update_height.revision_height(), 22); - let ctx_b = MockContextConfig::builder() - .host_id(chain_id_b.clone()) - .host_type(HostType::SyntheticTendermint) + let ctx_b = TestContextConfig::builder() + .host(TendermintHost::builder().chain_id(chain_id_b).build()) .latest_height(update_height) - .max_history_size(ctx_b_val_history.len() as u64 - 1) - .validator_set_history(ctx_b_val_history) - .build(); + .block_params_history(block_params) + .build::(); + + let mut ctx_a = TestContextConfig::builder() + .host( + MockHost::builder() + .chain_id(ChainId::new("mockgaiaA-1").unwrap()) + .build(), + ) + .latest_height(Height::new(1, 1).unwrap()) + .build::() + .with_light_client( + &client_id, + // remote light client initialized with client_height + LightClientBuilder::init() + .context(&ctx_b) + .consensus_heights([client_height]) + .build(), + ); + + let mut router_a = MockRouter::new_with_transfer(); let signer = dummy_account_id(); - let mut block = ctx_b.host_block(&update_height).unwrap().clone(); - block.set_trusted_height(client_height); + let mut block = ctx_b.host_block(&update_height).unwrap().into_header(); - let trusted_next_validator_set = match ctx_b.host_block(&client_height).expect("no error") { - HostBlock::SyntheticTendermint(header) => header.light_block.next_validators.clone(), - _ => panic!("unexpected host block type"), - }; + let trusted_next_validator_set = ctx_b + .host_block(&client_height) + .expect("no error") + .next_validators + .clone(); + block.set_trusted_height(client_height); block.set_trusted_next_validators_set(trusted_next_validator_set); let latest_header_height = block.height(); @@ -434,61 +481,151 @@ fn test_update_synthetic_tendermint_client_validator_change_ok() { }; let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg.clone())); - let res = validate(&ctx_a, &router_a, msg_envelope.clone()); + let res = validate(&ctx_a.ibc_store, &router_a, msg_envelope.clone()); assert!(res.is_ok()); - let res = execute(&mut ctx_a, &mut router_a, msg_envelope); + let res = execute(&mut ctx_a.ibc_store, &mut router_a, msg_envelope); assert!(res.is_ok(), "result: {res:?}"); - let client_state = ctx_a.client_state(&msg.client_id).unwrap(); + let client_state = ctx_a.ibc_store.client_state(&msg.client_id).unwrap(); assert!(client_state - .status(&ctx_a, &msg.client_id) + .status(&ctx_a.ibc_store, &msg.client_id) .unwrap() .is_active()); assert_eq!(client_state.latest_height(), latest_header_height); } +// TODO(rano): refactor the validator change tests to use a single test function + #[rstest] -fn test_update_synthetic_tendermint_client_validator_change_fail() { +fn test_update_synthetic_tendermint_client_wrong_trusted_validator_change_fail() { let client_id = tm_client_type().build_client_id(0); let client_height = Height::new(1, 20).unwrap(); let chain_id_b = ChainId::new("mockgaiaB-1").unwrap(); - let ctx_a = MockContextConfig::builder() - .host_id(ChainId::new("mockgaiaA-1").unwrap()) + let ctx_b_val_history = vec![ + // validator set of height-20 + vec![ + TestgenValidator::new("1").voting_power(50), + TestgenValidator::new("2").voting_power(50), + ], + // next validator set of height-20 + // validator set of height-21 + vec![ + TestgenValidator::new("1").voting_power(45), + TestgenValidator::new("2").voting_power(55), + ], + // next validator set of height-21 + // validator set of height-22 + vec![ + TestgenValidator::new("1").voting_power(30), + TestgenValidator::new("2").voting_power(70), + ], + // next validator set of height-22 + vec![ + TestgenValidator::new("1").voting_power(20), + TestgenValidator::new("2").voting_power(80), + ], + ]; + + let block_params = BlockParams::from_validator_history(ctx_b_val_history); + + let update_height = client_height.add(block_params.len() as u64 - 1); + + assert_eq!(update_height.revision_height(), 22); + + let ctx_b = TestContextConfig::builder() + .host(TendermintHost::builder().chain_id(chain_id_b).build()) + .latest_height(update_height) + .block_params_history(block_params) + .build::(); + + let ctx_a = TestContextConfig::builder() + .host( + MockHost::builder() + .chain_id(ChainId::new("mockgaiaA-1").unwrap()) + .build(), + ) .latest_height(Height::new(1, 1).unwrap()) - .build() - .with_client_config( - // client state initialized with client_height, and - // [{id: 1, power: 50}, {id: 2, power: 50}] for validator set and next validator set. - MockClientConfig::builder() - .client_chain_id(chain_id_b.clone()) - .client_id(client_id.clone()) - .latest_height(client_height) - .client_type(tm_client_type()) + .build::() + .with_light_client( + &client_id, + // remote light client initialized with client_height + LightClientBuilder::init() + .context(&ctx_b) + .consensus_heights([client_height]) .build(), ); let router = MockRouter::new_with_transfer(); + let signer = dummy_account_id(); + + // next validator set from height-20 + let trusted_next_validator_set = ctx_b + .host_block(&client_height) + .expect("no error") + .next_validators + .clone(); + + // next validator set from height-21 + let mistrusted_next_validator_set = ctx_b + .host_block(&client_height.increment()) + .expect("no error") + .next_validators + .clone(); + + // ensure the next validator sets are different + assert_ne!( + mistrusted_next_validator_set.hash(), + trusted_next_validator_set.hash() + ); + + let mut block = ctx_b.host_block(&update_height).unwrap().into_header(); + + // set the trusted height to height-20 + block.set_trusted_height(client_height); + // set the trusted next validator set from height-21, which is different than height-20 + block.set_trusted_next_validators_set(mistrusted_next_validator_set); + + let msg = MsgUpdateClient { + client_id, + client_message: block.into(), + signer, + }; + + let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg)); + + let res = validate(&ctx_a.ibc_store, &router, msg_envelope); + + assert!(res.is_err()); +} + +#[rstest] +fn test_update_synthetic_tendermint_client_validator_change_fail() { + let client_id = tm_client_type().build_client_id(0); + let client_height = Height::new(1, 20).unwrap(); + let chain_id_b = ChainId::new("mockgaiaB-1").unwrap(); + let ctx_b_val_history = vec![ - // First two validator sets are default at client creation - // // validator set of height-20 vec![ TestgenValidator::new("1").voting_power(50), TestgenValidator::new("2").voting_power(50), ], - // incorrect next validator set for height-20 + // next validator set of height-20 // validator set of height-21 vec![ - TestgenValidator::new("1").voting_power(45), - TestgenValidator::new("2").voting_power(55), + TestgenValidator::new("1").voting_power(90), + TestgenValidator::new("2").voting_power(10), ], + // next validator set of height-21 // validator set of height-22 + // overlap doesn't maintain 1/3 power in older set vec![ - TestgenValidator::new("1").voting_power(30), - TestgenValidator::new("2").voting_power(70), + // TestgenValidator::new("1").voting_power(0), + TestgenValidator::new("4").voting_power(90), + TestgenValidator::new("2").voting_power(10), ], // validator set of height-23 vec![ @@ -497,37 +634,248 @@ fn test_update_synthetic_tendermint_client_validator_change_fail() { ], ]; - let update_height = client_height.add(ctx_b_val_history.len() as u64 - 2); + let block_params = BlockParams::from_validator_history(ctx_b_val_history); + + let update_height = client_height.add(block_params.len() as u64 - 1); - let ctx_b = MockContextConfig::builder() - .host_id(chain_id_b.clone()) - .host_type(HostType::SyntheticTendermint) + assert_eq!(update_height.revision_height(), 22); + + let ctx_b = TestContextConfig::builder() + .host(TendermintHost::builder().chain_id(chain_id_b).build()) .latest_height(update_height) - .max_history_size(ctx_b_val_history.len() as u64 - 1) - .validator_set_history(ctx_b_val_history) - .build(); + .block_params_history(block_params) + .build::(); + + let ctx_a = TestContextConfig::builder() + .host( + MockHost::builder() + .chain_id(ChainId::new("mockgaiaA-1").unwrap()) + .build(), + ) + .latest_height(Height::new(1, 1).unwrap()) + .build::() + .with_light_client( + &client_id, + // remote light client initialized with client_height + LightClientBuilder::init() + .context(&ctx_b) + .consensus_heights([client_height]) + .build(), + ); + + let router_a = MockRouter::new_with_transfer(); let signer = dummy_account_id(); - let mut block = ctx_b.host_block(&update_height).unwrap().clone(); + let trusted_next_validator_set = ctx_b + .host_block(&client_height) + .expect("no error") + .next_validators + .clone(); + + let mut block = ctx_b.host_block(&update_height).unwrap().into_header(); + block.set_trusted_height(client_height); + block.set_trusted_next_validators_set(trusted_next_validator_set); - let trusted_next_validator_set = match ctx_b.host_block(&client_height).expect("no error") { - HostBlock::SyntheticTendermint(header) => header.light_block.next_validators.clone(), - _ => panic!("unexpected host block type"), + let msg = MsgUpdateClient { + client_id, + client_message: block.into(), + signer, }; + let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg)); + + let res = validate(&ctx_a.ibc_store, &router_a, msg_envelope); + + assert!(res.is_err()); +} + +#[rstest] +fn test_update_synthetic_tendermint_client_malicious_validator_change_pass() { + let client_id = tm_client_type().build_client_id(0); + let client_height = Height::new(1, 20).unwrap(); + let chain_id_b = ChainId::new("mockgaiaB-1").unwrap(); + + let ctx_b_val_history = vec![ + // First two validator sets are default at client creation + // + // validator set of height-20 + vec![ + TestgenValidator::new("1").voting_power(50), + TestgenValidator::new("2").voting_power(50), + ], + // validator set of height-21 + // next validator set of height-20 + vec![ + TestgenValidator::new("1").voting_power(34), + TestgenValidator::new("2").voting_power(66), + ], + // validator set of height-22 + // next validator set of height-21 + vec![ + TestgenValidator::new("4").voting_power(90), + TestgenValidator::new("2").voting_power(10), + ], + // next validator set of height-22 + vec![ + TestgenValidator::new("1").voting_power(20), + TestgenValidator::new("2").voting_power(80), + ], + ]; + + let mut block_params = BlockParams::from_validator_history(ctx_b_val_history); + + if let Some(block_param) = block_params.last_mut() { + // forged validator set of height-22 + block_param.validators = vec![TestgenValidator::new("1").voting_power(100)]; + } + + let update_height = client_height.add(block_params.len() as u64 - 1); + + assert_eq!(update_height.revision_height(), 22); + + let ctx_b = TestContextConfig::builder() + .host(TendermintHost::builder().chain_id(chain_id_b).build()) + .latest_height(update_height) + .block_params_history(block_params) + .build::(); + + let mut ctx_a = TestContextConfig::builder() + .host( + MockHost::builder() + .chain_id(ChainId::new("mockgaiaA-1").unwrap()) + .build(), + ) + .latest_height(Height::new(1, 1).unwrap()) + .build::() + .with_light_client( + &client_id, + // remote light client initialized with client_height + LightClientBuilder::init() + .context(&ctx_b) + .consensus_heights([client_height]) + .build(), + ); + + let mut router_a = MockRouter::new_with_transfer(); + + let signer = dummy_account_id(); + + let mut block = ctx_b.host_block(&update_height).unwrap().into_header(); + let trusted_next_validator_set = ctx_b + .host_block(&client_height) + .expect("no error") + .next_validators + .clone(); + + block.set_trusted_height(client_height); block.set_trusted_next_validators_set(trusted_next_validator_set); + let latest_header_height = block.height(); let msg = MsgUpdateClient { client_id, client_message: block.into(), signer, }; + let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg.clone())); + + let res = validate(&ctx_a.ibc_store, &router_a, msg_envelope.clone()); + assert!(res.is_ok()); + + let res = execute(&mut ctx_a.ibc_store, &mut router_a, msg_envelope); + assert!(res.is_ok(), "result: {res:?}"); + + let client_state = ctx_a.ibc_store.client_state(&msg.client_id).unwrap(); + assert!(client_state + .status(&ctx_a.ibc_store, &msg.client_id) + .unwrap() + .is_active()); + assert_eq!(client_state.latest_height(), latest_header_height); +} + +#[rstest] +fn test_update_synthetic_tendermint_client_adjacent_malicious_validator_change_fail() { + let client_id = tm_client_type().build_client_id(0); + let client_height = Height::new(1, 21).unwrap(); + let chain_id_b = ChainId::new("mockgaiaB-1").unwrap(); + + let ctx_b_val_history = vec![ + // validator set of height-21 + vec![ + TestgenValidator::new("1").voting_power(34), + TestgenValidator::new("2").voting_power(66), + ], + // next validator set of height-21 + // validator set of height-22 + vec![ + TestgenValidator::new("4").voting_power(90), + TestgenValidator::new("2").voting_power(10), + ], + // next validator set of height-22 + vec![ + TestgenValidator::new("1").voting_power(20), + TestgenValidator::new("2").voting_power(80), + ], + ]; + + let mut block_params = BlockParams::from_validator_history(ctx_b_val_history); + + if let Some(block_param) = block_params.last_mut() { + // forged validator set of height-22 + block_param.validators = vec![TestgenValidator::new("1").voting_power(100)]; + } + + let update_height = client_height.add(block_params.len() as u64 - 1); + assert_eq!(update_height.revision_height(), 22); + + let ctx_b = TestContextConfig::builder() + .host(TendermintHost::builder().chain_id(chain_id_b).build()) + .latest_height(update_height) + .block_params_history(block_params) + .build::(); + + let ctx_a = TestContextConfig::builder() + .host( + MockHost::builder() + .chain_id(ChainId::new("mockgaiaA-1").unwrap()) + .build(), + ) + .latest_height(Height::new(1, 1).unwrap()) + .build::() + .with_light_client( + &client_id, + // remote light client initialized with client_height + LightClientBuilder::init() + .context(&ctx_b) + .consensus_heights([client_height]) + .build(), + ); + + let router_a = MockRouter::new_with_transfer(); + + let signer = dummy_account_id(); + + let mut block = ctx_b.host_block(&update_height).unwrap().into_header(); + + let trusted_next_validator_set = ctx_b + .host_block(&client_height) + .expect("no error") + .next_validators + .clone(); + + block.set_trusted_height(client_height); + block.set_trusted_next_validators_set(trusted_next_validator_set); + + let msg = MsgUpdateClient { + client_id, + client_message: block.into(), + signer, + }; let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg)); - let res = validate(&ctx_a, &router, msg_envelope); + let res = validate(&ctx_a.ibc_store, &router_a, msg_envelope); assert!(res.is_err()); } @@ -539,34 +887,33 @@ fn test_update_synthetic_tendermint_client_non_adjacent_ok() { let update_height = Height::new(1, 21).unwrap(); let chain_id_b = ChainId::new("mockgaiaB-1").unwrap(); - let mut ctx = MockContextConfig::builder() - .host_id(ChainId::new("mockgaiaA-1").unwrap()) + let ctx_b = TestContextConfig::builder() + .host(TendermintHost::builder().chain_id(chain_id_b).build()) + .latest_height(update_height) + .build::(); + + let mut ctx = TestContextConfig::builder() + .host( + MockHost::builder() + .chain_id(ChainId::new("mockgaiaA-1").unwrap()) + .build(), + ) .latest_height(Height::new(1, 1).unwrap()) - .build() - .with_client_config( - MockClientConfig::builder() - .client_chain_id(chain_id_b.clone()) - .client_id(client_id.clone()) - .client_type(tm_client_type()) // The target host chain (B) is synthetic TM. - .latest_height(client_height) - .consensus_state_heights(vec![ - client_height.sub(1).expect("no error"), - client_height, - ]) + .build::() + .with_light_client( + &client_id, + LightClientBuilder::init() + .context(&ctx_b) + .consensus_heights([client_height.sub(1).expect("no error"), client_height]) .build(), ); let mut router = MockRouter::new_with_transfer(); - let ctx_b = MockContextConfig::builder() - .host_id(chain_id_b) - .host_type(HostType::SyntheticTendermint) - .latest_height(update_height) - .build(); - let signer = dummy_account_id(); - let mut block = ctx_b.host_block(&update_height).unwrap().clone(); + let block = ctx_b.host_block(&update_height).unwrap(); + let mut block = block.into_header(); let trusted_height = client_height.clone().sub(1).unwrap(); block.set_trusted_height(trusted_height); @@ -579,16 +926,16 @@ fn test_update_synthetic_tendermint_client_non_adjacent_ok() { let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg.clone())); - let res = validate(&ctx, &router, msg_envelope.clone()); + let res = validate(&ctx.ibc_store, &router, msg_envelope.clone()); assert!(res.is_ok()); - let res = execute(&mut ctx, &mut router, msg_envelope); + let res = execute(&mut ctx.ibc_store, &mut router, msg_envelope); assert!(res.is_ok(), "result: {res:?}"); - let client_state = ctx.client_state(&msg.client_id).unwrap(); + let client_state = ctx.ibc_store.client_state(&msg.client_id).unwrap(); assert!(client_state - .status(&ctx, &msg.client_id) + .status(&ctx.ibc_store, &msg.client_id) .unwrap() .is_active()); @@ -604,61 +951,52 @@ fn test_update_synthetic_tendermint_client_duplicate_ok() { let ctx_b_chain_id = ChainId::new("mockgaiaB-1").unwrap(); let start_height = Height::new(1, 11).unwrap(); - let mut ctx_a = MockContextConfig::builder() - .host_id(ctx_a_chain_id) + let ctx_b = TestContextConfig::builder() + .host(TendermintHost::builder().chain_id(ctx_b_chain_id).build()) + .latest_height(client_height) + .build::(); + + let mut ctx_a = TestContextConfig::builder() + .host(MockHost::builder().chain_id(ctx_a_chain_id).build()) .latest_height(start_height) - .build() - .with_client_config( - MockClientConfig::builder() - .client_chain_id(ctx_b_chain_id.clone()) - .client_id(client_id.clone()) - .client_type(tm_client_type()) // The target host chain (B) is synthetic TM. - .latest_height(client_height) - .consensus_state_heights(vec![start_height]) + .build::() + .with_light_client( + &client_id, + LightClientBuilder::init() + .context(&ctx_b) + .consensus_heights([start_height]) .build(), ); let mut router_a = MockRouter::new_with_transfer(); - let ctx_b = MockContextConfig::builder() - .host_id(ctx_b_chain_id) - .host_type(HostType::SyntheticTendermint) - .latest_height(client_height) - .build(); - let signer = dummy_account_id(); - let block = ctx_b.host_block(&client_height).unwrap().clone(); + let block = ctx_b.host_block(&client_height).unwrap(); + let mut block = block.into_header(); // Update the trusted height of the header to point to the previous height // (`start_height` in this case). // - // Note: The current MockContext interface doesn't allow us to + // Note: The current `TestContext` interface doesn't allow us to // do this without a major redesign. - let block = match block { - HostBlock::SyntheticTendermint(mut theader) => { - // current problem: the timestamp of the new header doesn't match the timestamp of - // the stored consensus state. If we hack them to match, then commit check fails. - // FIXME: figure out why they don't match. - theader.trusted_height = start_height; - - HostBlock::SyntheticTendermint(theader) - } - _ => block, - }; + + // current problem: the timestamp of the new header doesn't match the timestamp of + // the stored consensus state. If we hack them to match, then commit check fails. + // FIXME: figure out why they don't match. + + block.set_trusted_height(start_height); // Update the client height to `client_height` // - // Note: The current MockContext interface doesn't allow us to + // Note: The current `TestContext` interface doesn't allow us to // do this without a major redesign. { // FIXME: idea: we need to update the light client with the latest block from // chain B - let consensus_state: AnyConsensusState = block.clone().into(); + let consensus_state: AnyConsensusState = block.clone().into_consensus_state().into(); - let HostBlock::SyntheticTendermint(tm_block) = &block else { - panic!("unexpected host block type"); - }; + let tm_block = █ let chain_id = ChainId::from_str(tm_block.header().chain_id.as_str()).unwrap(); @@ -696,14 +1034,9 @@ fn test_update_synthetic_tendermint_client_duplicate_ok() { ClientState::from(client_state).into() }; - let mut ibc_store = ctx_a.ibc_store.lock(); - let client_record = ibc_store.clients.get_mut(&client_id).unwrap(); + ctx_a = ctx_a.with_client_state(&client_id, client_state); - client_record - .consensus_states - .insert(client_height, consensus_state); - - client_record.client_state = Some(client_state); + ctx_a = ctx_a.with_consensus_state(&client_id, client_height, consensus_state); } let latest_header_height = block.height(); @@ -715,19 +1048,18 @@ fn test_update_synthetic_tendermint_client_duplicate_ok() { let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg.clone())); - let res = validate(&ctx_a, &router_a, msg_envelope.clone()); + let res = validate(&ctx_a.ibc_store, &router_a, msg_envelope.clone()); assert!(res.is_ok(), "result: {res:?}"); - let res = execute(&mut ctx_a, &mut router_a, msg_envelope); + let res = execute(&mut ctx_a.ibc_store, &mut router_a, msg_envelope); assert!(res.is_ok(), "result: {res:?}"); - let client_state = ctx_a.client_state(&msg.client_id).unwrap(); + let client_state = ctx_a.ibc_store.client_state(&msg.client_id).unwrap(); assert!(client_state - .status(&ctx_a, &msg.client_id) + .status(&ctx_a.ibc_store, &msg.client_id) .unwrap() .is_active()); assert_eq!(client_state.latest_height(), latest_header_height); - assert_eq!(client_state, ctx_a.latest_client_states(&msg.client_id)); } #[rstest] @@ -739,39 +1071,43 @@ fn test_update_synthetic_tendermint_client_lower_height() { let chain_start_height = Height::new(1, 11).unwrap(); - let ctx = MockContextConfig::builder() - .host_id(ChainId::new("mockgaiaA-1").unwrap()) - .latest_height(chain_start_height) - .build() - .with_client_config( - MockClientConfig::builder() - .client_id(client_id.clone()) - .client_type(tm_client_type()) // The target host chain (B) is synthetic TM. - .latest_height(client_height) + let ctx_b = TestContextConfig::builder() + .host( + TendermintHost::builder() + .chain_id(ChainId::new("mockgaiaB-1").unwrap()) + .build(), + ) + .latest_height(client_height) + .build::(); + + let ctx = TestContextConfig::builder() + .host( + MockHost::builder() + .chain_id(ChainId::new("mockgaiaA-1").unwrap()) .build(), + ) + .latest_height(chain_start_height) + .build::() + .with_light_client( + &client_id, + LightClientBuilder::init().context(&ctx_b).build(), ); let router = MockRouter::new_with_transfer(); - let ctx_b = MockContextConfig::builder() - .host_id(ChainId::new("mockgaiaB-1").unwrap()) - .host_type(HostType::SyntheticTendermint) - .latest_height(client_height) - .build(); - let signer = dummy_account_id(); let block_ref = ctx_b.host_block(&client_update_height).unwrap(); let msg = MsgUpdateClient { client_id, - client_message: block_ref.clone().into(), + client_message: block_ref.into_header().into(), signer, }; let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg)); - let res = validate(&ctx, &router, msg_envelope); + let res = validate(&ctx.ibc_store, &router, msg_envelope); assert!(res.is_err()); } @@ -795,7 +1131,7 @@ fn test_update_client_events(fixture: Fixture) { }; let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg)); - let res = execute(&mut ctx, &mut router, msg_envelope); + let res = execute(&mut ctx.ibc_store, &mut router, msg_envelope); assert!(res.is_ok()); let ibc_events = ctx.get_events(); @@ -804,8 +1140,9 @@ fn test_update_client_events(fixture: Fixture) { ibc_events[0], IbcEvent::Message(MessageEvent::Client) )); + let IbcEvent::UpdateClient(update_client_event) = &ibc_events[1] else { - panic!("unexpected event variant"); + panic!("UpdateClient event is expected") }; assert_eq!(update_client_event.client_id(), &client_id); @@ -815,21 +1152,25 @@ fn test_update_client_events(fixture: Fixture) { assert_eq!(update_client_event.header(), &header.to_vec()); } -fn ensure_misbehaviour(ctx: &MockContext, client_id: &ClientId, client_type: &ClientType) { +fn ensure_misbehaviour( + ctx: &MockIbcStore, + client_id: &ClientId, + client_type: &ClientType, +) { let client_state = ctx.client_state(client_id).unwrap(); let status = client_state.status(ctx, client_id).unwrap(); assert!(status.is_frozen(), "client_state status: {status}"); // check events - let ibc_events = ctx.get_events(); + let ibc_events = ctx.events.lock(); assert_eq!(ibc_events.len(), 2); assert!(matches!( ibc_events[0], IbcEvent::Message(MessageEvent::Client), )); let IbcEvent::ClientMisbehaviour(misbehaviour_client_event) = &ibc_events[1] else { - panic!("unexpected event variant"); + panic!("ClientMisbehaviour event is expected") }; assert_eq!(misbehaviour_client_event.client_id(), client_id); assert_eq!(misbehaviour_client_event.client_type(), client_type); @@ -849,13 +1190,13 @@ fn test_misbehaviour_client_ok(fixture: Fixture) { let client_id = ClientId::new("07-tendermint", 0).expect("no error"); let msg_envelope = msg_update_client(&client_id); - let res = validate(&ctx, &router, msg_envelope.clone()); + let res = validate(&ctx.ibc_store, &router, msg_envelope.clone()); assert!(res.is_ok()); - let res = execute(&mut ctx, &mut router, msg_envelope); + let res = execute(&mut ctx.ibc_store, &mut router, msg_envelope); assert!(res.is_ok()); - ensure_misbehaviour(&ctx, &client_id, &mock_client_type()); + ensure_misbehaviour(&ctx.ibc_store, &client_id, &mock_client_type()); } #[rstest] @@ -866,13 +1207,11 @@ fn test_submit_misbehaviour_nonexisting_client(fixture: Fixture) { let msg_envelope = msg_update_client(&ClientId::from_str("nonexistingclient").unwrap()); - let ctx = MockContext::default().with_client_config( - MockClientConfig::builder() - .client_id(client_id.clone()) - .latest_height(Height::new(0, 42).unwrap()) - .build(), + let ctx = MockContext::default().with_light_client( + &client_id, + LightClientState::::with_latest_height(Height::new(0, 42).unwrap()), ); - let res = validate(&ctx, &router, msg_envelope); + let res = validate(&ctx.ibc_store, &router, msg_envelope); assert!(res.is_err()); } @@ -884,13 +1223,11 @@ fn test_client_update_misbehaviour_nonexisting_client(fixture: Fixture) { let msg_envelope = msg_update_client(&ClientId::from_str("nonexistingclient").unwrap()); - let ctx = MockContext::default().with_client_config( - MockClientConfig::builder() - .client_id(client_id.clone()) - .latest_height(Height::new(0, 42).unwrap()) - .build(), + let ctx = MockContext::default().with_light_client( + &client_id, + LightClientState::::with_latest_height(Height::new(0, 42).unwrap()), ); - let res = validate(&ctx, &router, msg_envelope); + let res = validate(&ctx.ibc_store, &router, msg_envelope); assert!(res.is_err()); } @@ -903,44 +1240,56 @@ fn test_misbehaviour_synthetic_tendermint_equivocation() { let misbehaviour_height = Height::new(1, 21).unwrap(); let chain_id_b = ChainId::new("mockgaiaB-1").unwrap(); + // Create a mock context for chain-B + let ctx_b = TestContextConfig::builder() + .host( + TendermintHost::builder() + .chain_id(chain_id_b.clone()) + .build(), + ) + .latest_height(misbehaviour_height) + .build::(); + // Create a mock context for chain-A with a synthetic tendermint light client for chain-B - let mut ctx_a = MockContextConfig::builder() - .host_id(ChainId::new("mockgaiaA-1").unwrap()) + let mut ctx_a = TestContextConfig::builder() + .host( + MockHost::builder() + .chain_id(ChainId::new("mockgaiaA-1").unwrap()) + .build(), + ) .latest_height(Height::new(1, 1).unwrap()) - .build() - .with_client_config( - MockClientConfig::builder() - .client_chain_id(chain_id_b.clone()) - .client_id(client_id.clone()) - .client_type(tm_client_type()) - .latest_height(client_height) + .build::() + .with_light_client( + &client_id, + LightClientBuilder::init() + .context(&ctx_b) + .consensus_heights([client_height]) .build(), ); let mut router_a = MockRouter::new_with_transfer(); - // Create a mock context for chain-B - let ctx_b = MockContextConfig::builder() - .host_id(chain_id_b.clone()) - .host_type(HostType::SyntheticTendermint) - .latest_height(misbehaviour_height) - .build(); - // Get chain-B's header at `misbehaviour_height` let header1: TmHeader = { - let mut block = ctx_b.host_block(&misbehaviour_height).unwrap().clone(); + let block = ctx_b.host_block(&misbehaviour_height).unwrap(); + let mut block = block.into_header(); block.set_trusted_height(client_height); - block.try_into_tm_block().unwrap().into() + block.into() }; // Generate an equivocal header for chain-B at `misbehaviour_height` let header2 = { - let mut tm_block = HostBlock::generate_tm_block( - chain_id_b, - misbehaviour_height.revision_height(), - Timestamp::now(), - ); - tm_block.trusted_height = client_height; + let mut tm_block = TendermintHost::builder() + .chain_id(chain_id_b) + .build() + .generate_block( + Vec::new(), + misbehaviour_height.revision_height(), + Timestamp::now(), + &Default::default(), + ) + .into_header(); + tm_block.set_trusted_height(client_height); tm_block.into() }; @@ -951,11 +1300,11 @@ fn test_misbehaviour_synthetic_tendermint_equivocation() { }; let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg)); - let res = validate(&ctx_a, &router_a, msg_envelope.clone()); + let res = validate(&ctx_a.ibc_store, &router_a, msg_envelope.clone()); assert!(res.is_ok()); - let res = execute(&mut ctx_a, &mut router_a, msg_envelope); + let res = execute(&mut ctx_a.ibc_store, &mut router_a, msg_envelope); assert!(res.is_ok()); - ensure_misbehaviour(&ctx_a, &client_id, &tm_client_type()); + ensure_misbehaviour(&ctx_a.ibc_store, &client_id, &tm_client_type()); } #[rstest] @@ -965,30 +1314,44 @@ fn test_misbehaviour_synthetic_tendermint_bft_time() { let misbehaviour_height = Height::new(1, 21).unwrap(); let chain_id_b = ChainId::new("mockgaiaB-1").unwrap(); + let ctx_b = TestContextConfig::builder() + .host( + TendermintHost::builder() + .chain_id(chain_id_b.clone()) + .build(), + ) + .latest_height(client_height) + .build::(); + // Create a mock context for chain-A with a synthetic tendermint light client for chain-B - let mut ctx_a = MockContextConfig::builder() - .host_id(ChainId::new("mockgaiaA-1").unwrap()) - .latest_height(Height::new(1, 1).unwrap()) - .build() - .with_client_config( - MockClientConfig::builder() - .client_chain_id(chain_id_b.clone()) - .client_id(client_id.clone()) - .client_type(tm_client_type()) - .latest_height(client_height) + let mut ctx_a = TestContextConfig::builder() + .host( + MockHost::builder() + .chain_id(ChainId::new("mockgaiaA-1").unwrap()) .build(), + ) + .latest_height(Height::new(1, 1).unwrap()) + .build::() + .with_light_client( + &client_id, + LightClientBuilder::init().context(&ctx_b).build(), ); let mut router_a = MockRouter::new_with_transfer(); // Generate `header1` for chain-B let header1 = { - let mut tm_block = HostBlock::generate_tm_block( - chain_id_b.clone(), - misbehaviour_height.revision_height(), - Timestamp::now(), - ); - tm_block.trusted_height = client_height; + let mut tm_block = TendermintHost::builder() + .chain_id(chain_id_b.clone()) + .build() + .generate_block( + Vec::new(), + misbehaviour_height.revision_height(), + Timestamp::now(), + &Default::default(), + ) + .into_header(); + tm_block.set_trusted_height(client_height); tm_block }; @@ -997,12 +1360,17 @@ fn test_misbehaviour_synthetic_tendermint_bft_time() { let header2 = { let timestamp = Timestamp::from_nanoseconds(Timestamp::now().nanoseconds() + 1_000_000_000).unwrap(); - let mut tm_block = HostBlock::generate_tm_block( - chain_id_b, - misbehaviour_height.revision_height(), - timestamp, - ); - tm_block.trusted_height = client_height; + let mut tm_block = TendermintHost::builder() + .chain_id(chain_id_b) + .build() + .generate_block( + Vec::new(), + misbehaviour_height.revision_height(), + timestamp, + &Default::default(), + ) + .into_header(); + tm_block.set_trusted_height(client_height); tm_block }; @@ -1015,11 +1383,11 @@ fn test_misbehaviour_synthetic_tendermint_bft_time() { let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg)); - let res = validate(&ctx_a, &router_a, msg_envelope.clone()); + let res = validate(&ctx_a.ibc_store, &router_a, msg_envelope.clone()); assert!(res.is_ok()); - let res = execute(&mut ctx_a, &mut router_a, msg_envelope); + let res = execute(&mut ctx_a.ibc_store, &mut router_a, msg_envelope); assert!(res.is_ok()); - ensure_misbehaviour(&ctx_a, &client_id, &tm_client_type()); + ensure_misbehaviour(&ctx_a.ibc_store, &client_id, &tm_client_type()); } #[rstest] @@ -1035,30 +1403,45 @@ fn test_expired_client() { let trusting_period = Duration::from_secs(64); - let mut ctx = MockContextConfig::builder() - .host_id(ChainId::new("mockgaiaA-1").unwrap()) + let ctx_b = TestContextConfig::builder() + .host(TendermintHost::builder().chain_id(chain_id_b).build()) + .latest_height(client_height) + .latest_timestamp(timestamp) + .build::(); + + let mut ctx = TestContextConfig::builder() + .host( + MockHost::builder() + .chain_id(ChainId::new("mockgaiaA-1").unwrap()) + .build(), + ) .latest_height(Height::new(1, 1).unwrap()) .latest_timestamp(timestamp) - .build() - .with_client_config( - MockClientConfig::builder() - .client_chain_id(chain_id_b.clone()) - .client_id(client_id.clone()) - .latest_height(client_height) - .client_type(tm_client_type()) - .latest_timestamp(timestamp) - .trusting_period(trusting_period) + .build::() + .with_light_client( + &client_id, + LightClientBuilder::init() + .context(&ctx_b) + .params( + ClientStateConfig::builder() + .trusting_period(trusting_period) + .build(), + ) .build(), ); - while ctx.host_timestamp().expect("no error") < (timestamp + trusting_period).expect("no error") + while ctx.ibc_store.host_timestamp().expect("no error") + < (timestamp + trusting_period).expect("no error") { - ctx.advance_host_chain_height(); + ctx.advance_block_height(); } - let client_state = ctx.client_state(&client_id).unwrap(); + let client_state = ctx.ibc_store.client_state(&client_id).unwrap(); - assert!(client_state.status(&ctx, &client_id).unwrap().is_expired()); + assert!(client_state + .status(&ctx.ibc_store, &client_id) + .unwrap() + .is_expired()); } #[rstest] @@ -1073,52 +1456,57 @@ fn test_client_update_max_clock_drift() { let max_clock_drift = Duration::from_secs(64); - let ctx_a = MockContextConfig::builder() - .host_id(ChainId::new("mockgaiaA-1").unwrap()) + let mut ctx_b = TestContextConfig::builder() + .host(TendermintHost::builder().chain_id(chain_id_b).build()) + .latest_height(client_height) + .latest_timestamp(timestamp) + .build::(); + + let ctx_a = TestContextConfig::builder() + .host( + MockHost::builder() + .chain_id(ChainId::new("mockgaiaA-1").unwrap()) + .build(), + ) .latest_height(Height::new(1, 1).unwrap()) .latest_timestamp(timestamp) - .build() - .with_client_config( - MockClientConfig::builder() - .client_chain_id(chain_id_b.clone()) - .client_id(client_id.clone()) - .latest_height(client_height) - .client_type(tm_client_type()) - .latest_timestamp(timestamp) - .max_clock_drift(max_clock_drift) + .build::() + .with_light_client( + &client_id, + LightClientBuilder::init() + .context(&ctx_b) + .params( + ClientStateConfig::builder() + .max_clock_drift(max_clock_drift) + .build(), + ) .build(), ); let router_a = MockRouter::new_with_transfer(); - let mut ctx_b = MockContextConfig::builder() - .host_id(chain_id_b.clone()) - .host_type(HostType::SyntheticTendermint) - .latest_height(client_height) - .latest_timestamp(timestamp) - .max_history_size(u64::MAX) - .build(); - - while ctx_b.host_timestamp().expect("no error") - < (ctx_a.host_timestamp().expect("no error") + max_clock_drift).expect("no error") + while ctx_b.ibc_store.host_timestamp().expect("no error") + < (ctx_a.ibc_store.host_timestamp().expect("no error") + max_clock_drift).expect("no error") { - ctx_b.advance_host_chain_height(); + ctx_b.advance_block_height(); } // include current block - ctx_b.advance_host_chain_height(); + ctx_b.advance_block_height(); let update_height = ctx_b.latest_height(); let signer = dummy_account_id(); - let mut block = ctx_b.host_block(&update_height).unwrap().clone(); + let block = ctx_b.host_block(&update_height).unwrap(); + let mut block = block.into_header(); block.set_trusted_height(client_height); - let trusted_next_validator_set = match ctx_b.host_block(&client_height).expect("no error") { - HostBlock::SyntheticTendermint(header) => header.light_block.next_validators.clone(), - _ => panic!("unexpected host block type"), - }; + let trusted_next_validator_set = ctx_b + .host_block(&client_height) + .expect("no error") + .next_validators + .clone(); block.set_trusted_next_validators_set(trusted_next_validator_set); @@ -1130,6 +1518,166 @@ fn test_client_update_max_clock_drift() { let msg_envelope = MsgEnvelope::from(ClientMsg::from(msg)); - let res = validate(&ctx_a, &router_a, msg_envelope); + let res = validate(&ctx_a.ibc_store, &router_a, msg_envelope); assert!(res.is_err()); } + +/// Builds 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(crate) fn build_client_update_datagram( + dest: &TestContext, + client_id: &ClientId, + src_header: &H, +) -> Result +where + HostClientState: ClientStateValidation, +{ + // Check if client for ibc0 on ibc1 has been updated to latest height: + // - query client state on destination chain + let dest_client_latest_height = dest.light_client_latest_height(client_id); + + if src_header.height() == dest_client_latest_height { + return Err(RelayerError::ClientAlreadyUpToDate { + client_id: client_id.clone(), + source_height: src_header.height(), + destination_height: dest_client_latest_height, + }); + }; + + if dest_client_latest_height > src_header.height() { + return Err(RelayerError::ClientAtHigherHeight { + client_id: client_id.clone(), + source_height: src_header.height(), + destination_height: dest_client_latest_height, + }); + }; + + // Client on destination chain can be updated. + Ok(ClientMsg::UpdateClient(MsgUpdateClient { + client_id: client_id.clone(), + client_message: src_header.clone().into(), + signer: dummy_account_id(), + })) +} + +/// Serves to test both ICS-26 `dispatch` & `build_client_update_datagram` functions. +/// Implements a "ping pong" of client update messages, so that two chains repeatedly +/// process a client update message and update their height in succession. +#[rstest] +fn client_update_ping_pong() { + let chain_a_start_height = Height::new(1, 11).unwrap(); + let chain_b_start_height = Height::new(1, 20).unwrap(); + let client_on_b_for_a_height = Height::new(1, 10).unwrap(); // Should be smaller than `chain_a_start_height` + let client_on_a_for_b_height = Height::new(1, 20).unwrap(); // Should be smaller than `chain_b_start_height` + let num_iterations = 4; + + let client_on_a_for_b = tm_client_type().build_client_id(0); + let client_on_b_for_a = mock_client_type().build_client_id(0); + + let chain_id_a = ChainId::new("mockgaiaA-1").unwrap(); + let chain_id_b = ChainId::new("mockgaiaB-1").unwrap(); + + // Create two mock contexts, one for each chain. + let mut ctx_a = TestContextConfig::builder() + .host(MockHost::builder().chain_id(chain_id_a).build()) + .latest_height(chain_a_start_height) + .build::(); + + let mut ctx_b = TestContextConfig::builder() + .host(TendermintHost::builder().chain_id(chain_id_b).build()) + .latest_height(chain_b_start_height) + .latest_timestamp(ctx_a.timestamp_at(chain_a_start_height.decrement().unwrap())) // chain B is running slower than chain A + .build::(); + + ctx_a = ctx_a.with_light_client( + &client_on_a_for_b, + LightClientBuilder::init() + .context(&ctx_b) + .consensus_heights([client_on_a_for_b_height]) + .build(), + ); + + ctx_b = ctx_b.with_light_client( + &client_on_b_for_a, + LightClientBuilder::init() + .context(&ctx_a) + .consensus_heights([client_on_b_for_a_height]) + .build(), + ); + + for _i in 0..num_iterations { + // Update client on chain B to latest height of A. + // - create the client update message with the latest header from A + let a_latest_header = ctx_a.query_latest_block().unwrap(); + let client_msg_b_res = build_client_update_datagram( + &ctx_b, + &client_on_b_for_a, + &a_latest_header.into_header(), + ); + + assert!( + client_msg_b_res.is_ok(), + "create_client_update failed for context destination {ctx_b:?}, error: {client_msg_b_res:?}", + ); + + let client_msg_b = client_msg_b_res.unwrap(); + + // - send the message to B. We bypass ICS18 interface and call directly into + // `TestContext` `recv` method (to avoid additional serialization steps). + let dispatch_res_b = ctx_b.deliver(MsgEnvelope::Client(client_msg_b)); + let validation_res = ctx_b.host.validate(); + assert!( + validation_res.is_ok(), + "context validation failed with error {validation_res:?} for context {ctx_b:?}", + ); + + // Check if the update succeeded. + assert!( + dispatch_res_b.is_ok(), + "Dispatch failed for host chain b with error: {dispatch_res_b:?}" + ); + + assert_eq!( + ctx_b.light_client_latest_height(&client_on_b_for_a), + ctx_a.latest_height() + ); + + // Update client on chain A to latest height of B. + // - create the client update message with the latest header from B + // The test uses LightClientBlock that does not store the trusted height + let mut b_latest_header = ctx_b.query_latest_block().unwrap().clone().into_header(); + + let th = b_latest_header.height(); + b_latest_header.set_trusted_height(th.decrement().unwrap()); + + let client_msg_a_res = + build_client_update_datagram(&ctx_a, &client_on_a_for_b, &b_latest_header); + + assert!( + client_msg_a_res.is_ok(), + "create_client_update failed for context destination {ctx_a:?}, error: {client_msg_a_res:?}", + ); + + let client_msg_a = client_msg_a_res.unwrap(); + + debug!("client_msg_a = {:?}", client_msg_a); + + // - send the message to A + let dispatch_res_a = ctx_a.deliver(MsgEnvelope::Client(client_msg_a)); + let validation_res = ctx_a.host.validate(); + assert!( + validation_res.is_ok(), + "context validation failed with error {validation_res:?} for context {ctx_a:?}", + ); + + // Check if the update succeeded. + assert!( + dispatch_res_a.is_ok(), + "Dispatch failed for host chain a with error: {dispatch_res_a:?}" + ); + assert_eq!( + ctx_a.light_client_latest_height(&client_on_a_for_b), + ctx_b.latest_height() + ); + } +} diff --git a/ibc-testkit/tests/core/ics02_client/upgrade_client.rs b/ibc-testkit/tests/core/ics02_client/upgrade_client.rs index c9bdadab5..015bf8921 100644 --- a/ibc-testkit/tests/core/ics02_client/upgrade_client.rs +++ b/ibc-testkit/tests/core/ics02_client/upgrade_client.rs @@ -1,5 +1,5 @@ use ibc::clients::tendermint::types::client_type; -use ibc::core::client::context::ClientValidationContext; +use ibc::core::client::context::{ClientValidationContext, ExtClientValidationContext}; use ibc::core::client::types::error::{ClientError, UpgradeClientError}; use ibc::core::client::types::msgs::{ClientMsg, MsgUpgradeClient}; use ibc::core::client::types::Height; @@ -8,15 +8,17 @@ use ibc::core::handler::types::error::ContextError; use ibc::core::handler::types::events::{IbcEvent, MessageEvent}; use ibc::core::handler::types::msgs::MsgEnvelope; use ibc::core::host::types::path::ClientConsensusStatePath; +use ibc_testkit::context::MockContext; use ibc_testkit::fixtures::clients::tendermint::{ dummy_tendermint_header, dummy_tm_client_state_from_header, }; use ibc_testkit::fixtures::core::client::dummy_msg_upgrade_client; use ibc_testkit::fixtures::{Expect, Fixture}; +use ibc_testkit::hosts::MockHost; use ibc_testkit::testapp::ibc::clients::mock::client_state::client_type as mock_client_type; use ibc_testkit::testapp::ibc::clients::{AnyClientState, AnyConsensusState}; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::{MockClientConfig, MockContext}; +use ibc_testkit::testapp::ibc::core::types::LightClientState; enum Ctx { Default, @@ -33,15 +35,13 @@ fn msg_upgrade_client_fixture(ctx_variant: Ctx, msg_variant: Msg) -> Fixture::with_latest_height(Height::new(0, 42).unwrap()), ); let ctx = match ctx_variant { - Ctx::Default => ctx_default, - Ctx::WithClient => ctx_with_client, + Ctx::Default => ctx_default.ibc_store, + Ctx::WithClient => ctx_with_client.ibc_store, }; let upgrade_height = Height::new(1, 26).unwrap(); @@ -96,13 +96,14 @@ fn upgrade_client_execute(fxt: &mut Fixture, expect: Expect) { } Expect::Success => { assert!(res.is_ok(), "{err_msg}"); - let ibc_events = fxt.ctx.get_events(); + let ibc_events = fxt.ctx.events.lock(); assert!(matches!( ibc_events[0], IbcEvent::Message(MessageEvent::Client) )); + let IbcEvent::UpgradeClient(upgrade_client_event) = &ibc_events[1] else { - panic!("unexpected event variant"); + panic!("UpgradeClient event is expected") }; let plan_height = Height::new(1, 26).unwrap(); @@ -152,7 +153,7 @@ fn upgrade_client_fail_low_upgrade_height() { msg_upgrade_client_fixture(Ctx::WithClient, Msg::LowUpgradeHeight); let expected_err: ClientError = UpgradeClientError::LowUpgradeHeight { upgraded_height: Height::new(0, 26).unwrap(), - client_height: fxt.ctx.latest_height(), + client_height: fxt.ctx.host_height().unwrap(), } .into(); upgrade_client_validate( diff --git a/ibc-testkit/tests/core/ics03_connection/conn_open_ack.rs b/ibc-testkit/tests/core/ics03_connection/conn_open_ack.rs index 654c894c1..37733b60c 100644 --- a/ibc-testkit/tests/core/ics03_connection/conn_open_ack.rs +++ b/ibc-testkit/tests/core/ics03_connection/conn_open_ack.rs @@ -1,5 +1,6 @@ use core::str::FromStr; +use basecoin_store::impls::{GrowingStore, InMemoryStore, RevertibleStore}; use ibc::core::client::types::Height; use ibc::core::commitment_types::commitment::CommitmentPrefix; use ibc::core::connection::types::error::ConnectionError; @@ -13,11 +14,13 @@ use ibc::core::host::types::identifiers::{ChainId, ClientId}; use ibc::core::host::ValidationContext; use ibc::core::primitives::prelude::*; use ibc::core::primitives::ZERO_DURATION; +use ibc_testkit::context::MockContext; use ibc_testkit::fixtures::core::connection::dummy_msg_conn_open_ack; -use ibc_testkit::fixtures::core::context::MockContextConfig; +use ibc_testkit::fixtures::core::context::TestContextConfig; use ibc_testkit::fixtures::{Expect, Fixture}; +use ibc_testkit::hosts::MockHost; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::{MockClientConfig, MockContext}; +use ibc_testkit::testapp::ibc::core::types::{LightClientState, MockIbcStore}; use test_log::test; enum Ctx { @@ -58,36 +61,45 @@ fn conn_open_ack_fixture(ctx: Ctx) -> Fixture { conn_end_open.set_state(State::Open); // incorrect field let ctx_default = MockContext::default(); - let ctx_new = MockContextConfig::builder() - .host_id(ChainId::new(&format!("mockgaia-{}", latest_height.revision_number())).unwrap()) + let ctx_new = TestContextConfig::builder() + .host( + MockHost::builder() + .chain_id( + ChainId::new(&format!("mockgaia-{}", latest_height.revision_number())).unwrap(), + ) + .build(), + ) .latest_height(latest_height) - .build(); + .build::(); let ctx = match ctx { - Ctx::New => ctx_new, - Ctx::NewWithConnection => ctx_new - .with_client_config( - MockClientConfig::builder() - .client_id(client_id.clone()) - .latest_height(proof_height) - .build(), - ) - .with_connection(conn_id, default_conn_end), - Ctx::DefaultWithConnection => ctx_default - .with_client_config( - MockClientConfig::builder() - .client_id(client_id.clone()) - .latest_height(proof_height) - .build(), - ) - .with_connection(conn_id, default_conn_end), - Ctx::NewWithConnectionEndOpen => ctx_new - .with_client_config( - MockClientConfig::builder() - .client_id(client_id.clone()) - .latest_height(proof_height) - .build(), - ) - .with_connection(conn_id, conn_end_open), + Ctx::New => ctx_new.ibc_store, + Ctx::NewWithConnection => { + ctx_new + .with_light_client( + &client_id, + LightClientState::::with_latest_height(proof_height), + ) + .with_connection(conn_id, default_conn_end) + .ibc_store + } + Ctx::DefaultWithConnection => { + ctx_default + .with_light_client( + &client_id, + LightClientState::::with_latest_height(proof_height), + ) + .with_connection(conn_id, default_conn_end) + .ibc_store + } + Ctx::NewWithConnectionEndOpen => { + ctx_new + .with_light_client( + &client_id, + LightClientState::::with_latest_height(proof_height), + ) + .with_connection(conn_id, conn_end_open) + .ibc_store + } }; Fixture { ctx, msg } @@ -142,7 +154,7 @@ fn conn_open_ack_execute(fxt: &mut Fixture, expect: Expect assert!(res.is_err(), "{err_msg}"); } Expect::Success => { - let ibc_events = fxt.ctx.get_events(); + let ibc_events = fxt.ctx.events.lock(); assert!(res.is_ok(), "{err_msg}"); assert_eq!(ibc_events.len(), 2); @@ -156,7 +168,7 @@ fn conn_open_ack_execute(fxt: &mut Fixture, expect: Expect let IbcEvent::OpenAckConnection(conn_open_try_event) = event else { unreachable!() }; - let conn_end = ::connection_end( + let conn_end = >> as ValidationContext>::connection_end( &fxt.ctx, conn_open_try_event.conn_id_on_a(), ) diff --git a/ibc-testkit/tests/core/ics03_connection/conn_open_confirm.rs b/ibc-testkit/tests/core/ics03_connection/conn_open_confirm.rs index ba0e4479f..dc21dadef 100644 --- a/ibc-testkit/tests/core/ics03_connection/conn_open_confirm.rs +++ b/ibc-testkit/tests/core/ics03_connection/conn_open_confirm.rs @@ -11,10 +11,12 @@ use ibc::core::host::types::identifiers::ClientId; use ibc::core::host::ValidationContext; use ibc::core::primitives::prelude::*; use ibc::core::primitives::ZERO_DURATION; +use ibc_testkit::context::MockContext; use ibc_testkit::fixtures::core::connection::dummy_conn_open_confirm; use ibc_testkit::fixtures::{Expect, Fixture}; +use ibc_testkit::hosts::MockHost; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::{MockClientConfig, MockContext}; +use ibc_testkit::testapp::ibc::core::types::LightClientState; use test_log::test; enum Ctx { @@ -38,7 +40,7 @@ fn conn_open_confirm_fixture(ctx: Ctx) -> Fixture { State::Init, client_id.clone(), counterparty, - ValidationContext::get_compatible_versions(&ctx_default), + ValidationContext::get_compatible_versions(&ctx_default.ibc_store), ZERO_DURATION, ) .unwrap(); @@ -47,23 +49,25 @@ fn conn_open_confirm_fixture(ctx: Ctx) -> Fixture { correct_conn_end.set_state(State::TryOpen); let ctx = match ctx { - Ctx::Default => ctx_default, - Ctx::IncorrectConnection => ctx_default - .with_client_config( - MockClientConfig::builder() - .client_id(client_id.clone()) - .latest_height(Height::new(0, 10).unwrap()) - .build(), - ) - .with_connection(msg.conn_id_on_b.clone(), incorrect_conn_end_state), - Ctx::CorrectConnection => ctx_default - .with_client_config( - MockClientConfig::builder() - .client_id(client_id.clone()) - .latest_height(Height::new(0, 10).unwrap()) - .build(), - ) - .with_connection(msg.conn_id_on_b.clone(), correct_conn_end), + Ctx::Default => ctx_default.ibc_store, + Ctx::IncorrectConnection => { + ctx_default + .with_light_client( + &client_id, + LightClientState::::with_latest_height(Height::new(0, 10).unwrap()), + ) + .with_connection(msg.conn_id_on_b.clone(), incorrect_conn_end_state) + .ibc_store + } + Ctx::CorrectConnection => { + ctx_default + .with_light_client( + &client_id, + LightClientState::::with_latest_height(Height::new(0, 10).unwrap()), + ) + .with_connection(msg.conn_id_on_b.clone(), correct_conn_end) + .ibc_store + } }; Fixture { ctx, msg } @@ -94,7 +98,7 @@ fn conn_open_confirm_execute(fxt: &mut Fixture, expect assert!(res.is_err(), "{err_msg}"); } Expect::Success => { - let ibc_events = fxt.ctx.get_events(); + let ibc_events = fxt.ctx.events.lock(); assert!(res.is_ok(), "{err_msg}"); assert_eq!(ibc_events.len(), 2); diff --git a/ibc-testkit/tests/core/ics03_connection/conn_open_init.rs b/ibc-testkit/tests/core/ics03_connection/conn_open_init.rs index 76c974892..1a9218e24 100644 --- a/ibc-testkit/tests/core/ics03_connection/conn_open_init.rs +++ b/ibc-testkit/tests/core/ics03_connection/conn_open_init.rs @@ -7,13 +7,15 @@ use ibc::core::handler::types::events::{IbcEvent, MessageEvent}; use ibc::core::handler::types::msgs::MsgEnvelope; use ibc::core::host::ValidationContext; use ibc::core::primitives::prelude::*; +use ibc_testkit::context::MockContext; use ibc_testkit::fixtures::core::connection::{ dummy_msg_conn_open_init, msg_conn_open_init_with_counterparty_conn_id, msg_conn_open_with_version, }; use ibc_testkit::fixtures::{Expect, Fixture}; +use ibc_testkit::hosts::MockHost; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::{MockClientConfig, MockContext}; +use ibc_testkit::testapp::ibc::core::types::LightClientState; use test_log::test; enum Ctx { @@ -31,7 +33,7 @@ enum Msg { fn conn_open_init_fixture(ctx_variant: Ctx, msg_variant: Msg) -> Fixture { let msg_default = dummy_msg_conn_open_init(); let msg = match msg_variant { - Msg::Default => msg_default.clone(), + Msg::Default => msg_default, Msg::NoVersion => msg_conn_open_with_version(msg_default, None), Msg::BadVersion => { msg_conn_open_with_version(msg_default, Some("random identifier 424242")) @@ -41,13 +43,15 @@ fn conn_open_init_fixture(ctx_variant: Ctx, msg_variant: Msg) -> Fixture ctx_default.with_client_config( - MockClientConfig::builder() - .client_id(msg.client_id_on_a.clone()) - .latest_height(Height::new(0, 10).unwrap()) - .build(), - ), - _ => ctx_default, + Ctx::WithClient => { + ctx_default + .with_light_client( + &msg.client_id_on_a, + LightClientState::::with_latest_height(Height::new(0, 10).unwrap()), + ) + .ibc_store + } + _ => ctx_default.ibc_store, }; Fixture { ctx, msg } @@ -82,7 +86,7 @@ fn conn_open_init_execute( assert!(res.is_err(), "{err_msg}") } Expect::Success => { - let ibc_events = fxt.ctx.get_events(); + let ibc_events = fxt.ctx.events.lock(); assert!(res.is_ok(), "{err_msg}"); @@ -127,7 +131,7 @@ fn conn_open_init_no_context() { fn conn_open_init_no_version() { let mut fxt = conn_open_init_fixture(Ctx::WithClient, Msg::NoVersion); conn_open_init_validate(&fxt, Expect::Success); - let expected_version = ValidationContext::get_compatible_versions(&fxt.ctx.clone()); + let expected_version = ValidationContext::get_compatible_versions(&fxt.ctx); conn_open_init_execute(&mut fxt, Expect::Success, expected_version); } #[test] diff --git a/ibc-testkit/tests/core/ics03_connection/conn_open_try.rs b/ibc-testkit/tests/core/ics03_connection/conn_open_try.rs index e5e718e5b..e466b403d 100644 --- a/ibc-testkit/tests/core/ics03_connection/conn_open_try.rs +++ b/ibc-testkit/tests/core/ics03_connection/conn_open_try.rs @@ -6,11 +6,13 @@ use ibc::core::handler::types::events::{IbcEvent, MessageEvent}; use ibc::core::handler::types::msgs::MsgEnvelope; use ibc::core::host::ValidationContext; use ibc::core::primitives::prelude::*; +use ibc_testkit::context::MockContext; use ibc_testkit::fixtures::core::connection::dummy_msg_conn_open_try; -use ibc_testkit::fixtures::core::context::MockContextConfig; +use ibc_testkit::fixtures::core::context::TestContextConfig; use ibc_testkit::fixtures::{Expect, Fixture}; +use ibc_testkit::hosts::MockHost; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::{MockClientConfig, MockContext}; +use ibc_testkit::testapp::ibc::core::types::{DefaultIbcStore, LightClientState}; use test_log::test; enum Ctx { @@ -26,13 +28,10 @@ enum Msg { } fn conn_open_try_fixture(ctx_variant: Ctx, msg_variant: Msg) -> Fixture { - let max_history_size = 5; + let retained_history_size = 5; let client_cons_state_height = 10; let host_chain_height = Height::new(0, 35).unwrap(); - let pruned_height = host_chain_height - .sub(max_history_size + 1) - .unwrap() - .revision_height(); + let pruned_height = host_chain_height.sub(retained_history_size + 1).unwrap(); let msg = match msg_variant { Msg::Default => dummy_msg_conn_open_try( @@ -43,26 +42,33 @@ fn conn_open_try_fixture(ctx_variant: Ctx, msg_variant: Msg) -> Fixture dummy_msg_conn_open_try(client_cons_state_height, pruned_height), + Msg::HeightOld => { + dummy_msg_conn_open_try(client_cons_state_height, pruned_height.revision_height()) + } Msg::ProofHeightMissing => dummy_msg_conn_open_try( client_cons_state_height - 1, host_chain_height.revision_height(), ), }; - let ctx_new = MockContextConfig::builder() - .max_history_size(max_history_size) + let ctx_new = TestContextConfig::builder() .latest_height(host_chain_height) - .build(); + .build::(); let ctx = match ctx_variant { - Ctx::Default => MockContext::default(), - Ctx::WithClient => ctx_new.with_client_config( - MockClientConfig::builder() - .client_id(msg.client_id_on_b.clone()) - .latest_height(Height::new(0, client_cons_state_height).unwrap()) - .build(), - ), + Ctx::Default => DefaultIbcStore::default(), + Ctx::WithClient => { + ctx_new + .with_light_client( + &msg.client_id_on_b, + LightClientState::::with_latest_height( + Height::new(0, client_cons_state_height).unwrap(), + ), + ) + .ibc_store + } }; + + ctx.prune_host_consensus_states_till(&pruned_height); Fixture { ctx, msg } } @@ -95,7 +101,7 @@ fn conn_open_try_execute(fxt: &mut Fixture, expect: Expect assert_eq!(fxt.ctx.connection_counter().unwrap(), 1); - let ibc_events = fxt.ctx.get_events(); + let ibc_events = fxt.ctx.events.lock(); assert_eq!(ibc_events.len(), 2); diff --git a/ibc-testkit/tests/core/ics04_channel/acknowledgement.rs b/ibc-testkit/tests/core/ics04_channel/acknowledgement.rs index 7ac747dc5..1afd22fe1 100644 --- a/ibc-testkit/tests/core/ics04_channel/acknowledgement.rs +++ b/ibc-testkit/tests/core/ics04_channel/acknowledgement.rs @@ -2,7 +2,6 @@ use ibc::core::channel::types::channel::{ChannelEnd, Counterparty, Order, State} use ibc::core::channel::types::commitment::{compute_packet_commitment, PacketCommitment}; use ibc::core::channel::types::msgs::{MsgAcknowledgement, PacketMsg}; use ibc::core::channel::types::Version; -use ibc::core::client::context::ClientExecutionContext; use ibc::core::client::types::Height; use ibc::core::commitment_types::commitment::CommitmentPrefix; use ibc::core::connection::types::version::Version as ConnectionVersion; @@ -13,11 +12,12 @@ use ibc::core::entrypoint::{execute, validate}; use ibc::core::handler::types::events::{IbcEvent, MessageEvent}; use ibc::core::handler::types::msgs::MsgEnvelope; use ibc::core::host::types::identifiers::{ChannelId, ClientId, ConnectionId, PortId}; -use ibc::core::host::ExecutionContext; use ibc::core::primitives::*; +use ibc_testkit::context::MockContext; use ibc_testkit::fixtures::core::channel::dummy_raw_msg_acknowledgement; +use ibc_testkit::hosts::MockHost; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::{MockClientConfig, MockContext}; +use ibc_testkit::testapp::ibc::core::types::LightClientState; use rstest::*; use test_log::test; @@ -37,10 +37,9 @@ fn fixture() -> Fixture { let default_client_id = ClientId::new("07-tendermint", 0).expect("no error"); let client_height = Height::new(0, 2).unwrap(); - let ctx = MockContext::default().with_client_config( - MockClientConfig::builder() - .latest_height(client_height) - .build(), + let ctx = MockContext::default().with_light_client( + &ClientId::new("07-tendermint", 0).expect("no error"), + LightClientState::::with_latest_height(client_height), ); let router = MockRouter::new_with_transfer(); @@ -74,9 +73,9 @@ fn fixture() -> Fixture { ConnectionState::Open, default_client_id.clone(), ConnectionCounterparty::new( - default_client_id.clone(), + default_client_id, Some(ConnectionId::zero()), - CommitmentPrefix::empty(), + CommitmentPrefix::try_from(vec![0]).expect("no error"), ), ConnectionVersion::compatibles(), ZERO_DURATION, @@ -90,8 +89,8 @@ fn fixture() -> Fixture { msg, packet_commitment, conn_end_on_a, - chan_end_on_a_unordered, chan_end_on_a_ordered, + chan_end_on_a_unordered, } } @@ -103,7 +102,7 @@ fn ack_fail_no_channel(fixture: Fixture) { let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = validate(&ctx, &router, msg_envelope); + let res = validate(&ctx.ibc_store, &router, msg_envelope); assert!( res.is_err(), @@ -124,10 +123,9 @@ fn ack_success_no_packet_commitment(fixture: Fixture) { .. } = fixture; let ctx = ctx - .with_client_config( - MockClientConfig::builder() - .latest_height(client_height) - .build(), + .with_light_client( + &ClientId::new("07-tendermint", 0).expect("no error"), + LightClientState::::with_latest_height(client_height), ) .with_channel( PortId::transfer(), @@ -138,7 +136,7 @@ fn ack_success_no_packet_commitment(fixture: Fixture) { let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = validate(&ctx, &router, msg_envelope); + let res = validate(&ctx.ibc_store, &router, msg_envelope); assert!( res.is_ok(), @@ -148,7 +146,6 @@ fn ack_success_no_packet_commitment(fixture: Fixture) { #[rstest] fn ack_success_happy_path(fixture: Fixture) { - let default_client_id = ClientId::new("07-tendermint", 0).expect("no error"); let Fixture { ctx, router, @@ -159,11 +156,10 @@ fn ack_success_happy_path(fixture: Fixture) { client_height, .. } = fixture; - let mut ctx: MockContext = ctx - .with_client_config( - MockClientConfig::builder() - .latest_height(client_height) - .build(), + let ctx = ctx + .with_light_client( + &ClientId::new("07-tendermint", 0).expect("no error"), + LightClientState::::with_latest_height(client_height), ) .with_channel( PortId::transfer(), @@ -177,18 +173,10 @@ fn ack_success_happy_path(fixture: Fixture) { msg.packet.seq_on_a, packet_commitment, ); - ctx.get_client_execution_context() - .store_update_meta( - default_client_id, - client_height, - Timestamp::from_nanoseconds(1000).unwrap(), - Height::new(0, 4).unwrap(), - ) - .unwrap(); let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = validate(&ctx, &router, msg_envelope); + let res = validate(&ctx.ibc_store, &router, msg_envelope); assert!( res.is_ok(), @@ -223,7 +211,7 @@ fn ack_unordered_chan_execute(fixture: Fixture) { let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = execute(&mut ctx, &mut router, msg_envelope); + let res = execute(&mut ctx.ibc_store, &mut router, msg_envelope); assert!(res.is_ok()); @@ -260,7 +248,7 @@ fn ack_ordered_chan_execute(fixture: Fixture) { let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = execute(&mut ctx, &mut router, msg_envelope); + let res = execute(&mut ctx.ibc_store, &mut router, msg_envelope); assert!(res.is_ok()); diff --git a/ibc-testkit/tests/core/ics04_channel/chan_close_confirm.rs b/ibc-testkit/tests/core/ics04_channel/chan_close_confirm.rs index 14b358702..f3491f9ca 100644 --- a/ibc-testkit/tests/core/ics04_channel/chan_close_confirm.rs +++ b/ibc-testkit/tests/core/ics04_channel/chan_close_confirm.rs @@ -11,18 +11,20 @@ use ibc::core::handler::types::msgs::MsgEnvelope; use ibc::core::host::types::identifiers::ConnectionId; use ibc::core::host::ValidationContext; use ibc::core::primitives::*; +use ibc_testkit::context::MockContext; use ibc_testkit::fixtures::core::channel::dummy_raw_msg_chan_close_confirm; use ibc_testkit::fixtures::core::connection::dummy_raw_counterparty_conn; +use ibc_testkit::hosts::MockHost; use ibc_testkit::testapp::ibc::clients::mock::client_state::client_type as mock_client_type; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::{MockClientConfig, MockContext}; +use ibc_testkit::testapp::ibc::core::types::LightClientState; #[test] fn test_chan_close_confirm_validate() { let client_id = mock_client_type().build_client_id(24); let conn_id = ConnectionId::new(2); let default_context = MockContext::default(); - let client_consensus_state_height = default_context.host_height().unwrap(); + let client_consensus_state_height = default_context.ibc_store.host_height().unwrap(); let conn_end = ConnectionEnd::new( ConnectionState::Open, @@ -53,22 +55,20 @@ fn test_chan_close_confirm_validate() { .unwrap(); let context = default_context - .with_client_config( - MockClientConfig::builder() - .client_id(client_id.clone()) - .latest_height(client_consensus_state_height) - .build(), + .with_light_client( + &client_id, + LightClientState::::with_latest_height(client_consensus_state_height), ) .with_connection(conn_id, conn_end) .with_channel( msg_chan_close_confirm.port_id_on_b.clone(), - msg_chan_close_confirm.chan_id_on_b.clone(), + msg_chan_close_confirm.chan_id_on_b, chan_end, ); let router = MockRouter::new_with_transfer(); - let res = validate(&context, &router, msg_envelope); + let res = validate(&context.ibc_store, &router, msg_envelope); assert!( res.is_ok(), @@ -81,7 +81,7 @@ fn test_chan_close_confirm_execute() { let client_id = mock_client_type().build_client_id(24); let conn_id = ConnectionId::new(2); let default_context = MockContext::default(); - let client_consensus_state_height = default_context.host_height().unwrap(); + let client_consensus_state_height = default_context.ibc_store.host_height().unwrap(); let conn_end = ConnectionEnd::new( ConnectionState::Open, @@ -112,22 +112,20 @@ fn test_chan_close_confirm_execute() { .unwrap(); let mut context = default_context - .with_client_config( - MockClientConfig::builder() - .client_id(client_id.clone()) - .latest_height(client_consensus_state_height) - .build(), + .with_light_client( + &client_id, + LightClientState::::with_latest_height(client_consensus_state_height), ) .with_connection(conn_id, conn_end) .with_channel( msg_chan_close_confirm.port_id_on_b.clone(), - msg_chan_close_confirm.chan_id_on_b.clone(), + msg_chan_close_confirm.chan_id_on_b, chan_end, ); let mut router = MockRouter::new_with_transfer(); - let res = execute(&mut context, &mut router, msg_envelope); + let res = execute(&mut context.ibc_store, &mut router, msg_envelope); assert!(res.is_ok(), "Execution success: happy path"); diff --git a/ibc-testkit/tests/core/ics04_channel/chan_close_init.rs b/ibc-testkit/tests/core/ics04_channel/chan_close_init.rs index 00ee8cdab..63fd30d65 100644 --- a/ibc-testkit/tests/core/ics04_channel/chan_close_init.rs +++ b/ibc-testkit/tests/core/ics04_channel/chan_close_init.rs @@ -11,11 +11,13 @@ use ibc::core::handler::types::msgs::MsgEnvelope; use ibc::core::host::types::identifiers::ConnectionId; use ibc::core::host::ValidationContext; use ibc::core::primitives::*; +use ibc_testkit::context::MockContext; use ibc_testkit::fixtures::core::channel::dummy_raw_msg_chan_close_init; use ibc_testkit::fixtures::core::connection::dummy_raw_counterparty_conn; +use ibc_testkit::hosts::MockHost; use ibc_testkit::testapp::ibc::clients::mock::client_state::client_type as mock_client_type; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::{MockClientConfig, MockContext}; +use ibc_testkit::testapp::ibc::core::types::LightClientState; #[test] fn test_chan_close_init_validate() { @@ -50,26 +52,24 @@ fn test_chan_close_init_validate() { let context = { let default_context = MockContext::default(); - let client_consensus_state_height = default_context.host_height().unwrap(); + let client_consensus_state_height = default_context.ibc_store.host_height().unwrap(); default_context - .with_client_config( - MockClientConfig::builder() - .client_id(client_id.clone()) - .latest_height(client_consensus_state_height) - .build(), + .with_light_client( + &client_id, + LightClientState::::with_latest_height(client_consensus_state_height), ) .with_connection(conn_id, conn_end) .with_channel( msg_chan_close_init.port_id_on_a.clone(), - msg_chan_close_init.chan_id_on_a.clone(), + msg_chan_close_init.chan_id_on_a, chan_end, ) }; let router = MockRouter::new_with_transfer(); - let res = validate(&context, &router, msg_envelope); + let res = validate(&context.ibc_store, &router, msg_envelope); assert!( res.is_ok(), @@ -110,26 +110,24 @@ fn test_chan_close_init_execute() { let mut context = { let default_context = MockContext::default(); - let client_consensus_state_height = default_context.host_height().unwrap(); + let client_consensus_state_height = default_context.ibc_store.host_height().unwrap(); default_context - .with_client_config( - MockClientConfig::builder() - .client_id(client_id.clone()) - .latest_height(client_consensus_state_height) - .build(), + .with_light_client( + &client_id, + LightClientState::::with_latest_height(client_consensus_state_height), ) .with_connection(conn_id, conn_end) .with_channel( msg_chan_close_init.port_id_on_a.clone(), - msg_chan_close_init.chan_id_on_a.clone(), + msg_chan_close_init.chan_id_on_a, chan_end, ) }; let mut router = MockRouter::new_with_transfer(); - let res = execute(&mut context, &mut router, msg_envelope); + let res = execute(&mut context.ibc_store, &mut router, msg_envelope); assert!(res.is_ok(), "Execution happy path"); diff --git a/ibc-testkit/tests/core/ics04_channel/chan_open_ack.rs b/ibc-testkit/tests/core/ics04_channel/chan_open_ack.rs index 8c3140910..e4250dd83 100644 --- a/ibc-testkit/tests/core/ics04_channel/chan_open_ack.rs +++ b/ibc-testkit/tests/core/ics04_channel/chan_open_ack.rs @@ -12,11 +12,13 @@ use ibc::core::handler::types::msgs::MsgEnvelope; use ibc::core::host::types::identifiers::{ClientId, ConnectionId}; use ibc::core::primitives::*; use ibc::core::router::types::module::ModuleId; +use ibc_testkit::context::MockContext; use ibc_testkit::fixtures::core::channel::dummy_raw_msg_chan_open_ack; use ibc_testkit::fixtures::core::connection::dummy_raw_counterparty_conn; +use ibc_testkit::hosts::MockHost; use ibc_testkit::testapp::ibc::clients::mock::client_state::client_type as mock_client_type; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::{MockClientConfig, MockContext}; +use ibc_testkit::testapp::ibc::core::types::LightClientState; use rstest::*; use test_log::test; @@ -90,11 +92,9 @@ fn chan_open_ack_happy_path(fixture: Fixture) { } = fixture; let context = context - .with_client_config( - MockClientConfig::builder() - .client_id(client_id_on_a.clone()) - .latest_height(Height::new(0, proof_height).unwrap()) - .build(), + .with_light_client( + &client_id_on_a, + LightClientState::::with_latest_height(Height::new(0, proof_height).unwrap()), ) .with_connection(conn_id_on_a, conn_end_on_a) .with_channel( @@ -105,7 +105,7 @@ fn chan_open_ack_happy_path(fixture: Fixture) { let msg_envelope = MsgEnvelope::from(ChannelMsg::from(msg)); - let res = validate(&context, &router, msg_envelope); + let res = validate(&context.ibc_store, &router, msg_envelope); assert!(res.is_ok(), "Validation happy path") } @@ -125,11 +125,9 @@ fn chan_open_ack_execute_happy_path(fixture: Fixture) { } = fixture; let mut context = context - .with_client_config( - MockClientConfig::builder() - .client_id(client_id_on_a.clone()) - .latest_height(Height::new(0, proof_height).unwrap()) - .build(), + .with_light_client( + &client_id_on_a, + LightClientState::::with_latest_height(Height::new(0, proof_height).unwrap()), ) .with_connection(conn_id_on_a, conn_end_on_a) .with_channel( @@ -138,9 +136,9 @@ fn chan_open_ack_execute_happy_path(fixture: Fixture) { chan_end_on_a, ); - let msg_envelope = MsgEnvelope::from(ChannelMsg::from(msg.clone())); + let msg_envelope = MsgEnvelope::from(ChannelMsg::from(msg)); - let res = execute(&mut context, &mut router, msg_envelope); + let res = execute(&mut context.ibc_store, &mut router, msg_envelope); assert!(res.is_ok(), "Execution happy path"); @@ -167,11 +165,9 @@ fn chan_open_ack_fail_no_connection(fixture: Fixture) { } = fixture; let context = context - .with_client_config( - MockClientConfig::builder() - .client_id(client_id_on_a.clone()) - .latest_height(Height::new(0, proof_height).unwrap()) - .build(), + .with_light_client( + &client_id_on_a, + LightClientState::::with_latest_height(Height::new(0, proof_height).unwrap()), ) .with_channel( msg.port_id_on_a.clone(), @@ -181,7 +177,7 @@ fn chan_open_ack_fail_no_connection(fixture: Fixture) { let msg_envelope = MsgEnvelope::from(ChannelMsg::from(msg)); - let res = validate(&context, &router, msg_envelope); + let res = validate(&context.ibc_store, &router, msg_envelope); assert!( res.is_err(), @@ -202,17 +198,15 @@ fn chan_open_ack_fail_no_channel(fixture: Fixture) { .. } = fixture; let context = context - .with_client_config( - MockClientConfig::builder() - .client_id(client_id_on_a.clone()) - .latest_height(Height::new(0, proof_height).unwrap()) - .build(), + .with_light_client( + &client_id_on_a, + LightClientState::::with_latest_height(Height::new(0, proof_height).unwrap()), ) .with_connection(conn_id_on_a, conn_end_on_a); let msg_envelope = MsgEnvelope::from(ChannelMsg::from(msg)); - let res = validate(&context, &router, msg_envelope); + let res = validate(&context.ibc_store, &router, msg_envelope); assert!( res.is_err(), @@ -242,11 +236,9 @@ fn chan_open_ack_fail_channel_wrong_state(fixture: Fixture) { ) .unwrap(); let context = context - .with_client_config( - MockClientConfig::builder() - .client_id(client_id_on_a.clone()) - .latest_height(Height::new(0, proof_height).unwrap()) - .build(), + .with_light_client( + &client_id_on_a, + LightClientState::::with_latest_height(Height::new(0, proof_height).unwrap()), ) .with_connection(conn_id_on_a, conn_end_on_a) .with_channel( @@ -257,7 +249,7 @@ fn chan_open_ack_fail_channel_wrong_state(fixture: Fixture) { let msg_envelope = MsgEnvelope::from(ChannelMsg::from(msg)); - let res = validate(&context, &router, msg_envelope); + let res = validate(&context.ibc_store, &router, msg_envelope); assert!( res.is_err(), diff --git a/ibc-testkit/tests/core/ics04_channel/chan_open_confirm.rs b/ibc-testkit/tests/core/ics04_channel/chan_open_confirm.rs index c97338f2e..0b399aa40 100644 --- a/ibc-testkit/tests/core/ics04_channel/chan_open_confirm.rs +++ b/ibc-testkit/tests/core/ics04_channel/chan_open_confirm.rs @@ -11,11 +11,13 @@ use ibc::core::handler::types::events::{IbcEvent, MessageEvent}; use ibc::core::handler::types::msgs::MsgEnvelope; use ibc::core::host::types::identifiers::{ChannelId, ClientId, ConnectionId}; use ibc::core::primitives::*; +use ibc_testkit::context::MockContext; use ibc_testkit::fixtures::core::channel::dummy_raw_msg_chan_open_confirm; use ibc_testkit::fixtures::core::connection::dummy_raw_counterparty_conn; +use ibc_testkit::hosts::MockHost; use ibc_testkit::testapp::ibc::clients::mock::client_state::client_type as mock_client_type; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::{MockClientConfig, MockContext}; +use ibc_testkit::testapp::ibc::core::types::LightClientState; use rstest::*; use test_log::test; @@ -87,18 +89,16 @@ fn chan_open_confirm_validate_happy_path(fixture: Fixture) { } = fixture; let context = context - .with_client_config( - MockClientConfig::builder() - .client_id(client_id_on_b.clone()) - .latest_height(Height::new(0, proof_height).unwrap()) - .build(), + .with_light_client( + &client_id_on_b, + LightClientState::::with_latest_height(Height::new(0, proof_height).unwrap()), ) .with_connection(conn_id_on_b, conn_end_on_b) .with_channel(msg.port_id_on_b.clone(), ChannelId::zero(), chan_end_on_b); let msg_envelope = MsgEnvelope::from(ChannelMsg::from(msg)); - let res = validate(&context, &router, msg_envelope); + let res = validate(&context.ibc_store, &router, msg_envelope); assert!(res.is_ok(), "Validation happy path") } @@ -118,18 +118,16 @@ fn chan_open_confirm_execute_happy_path(fixture: Fixture) { } = fixture; let mut context = context - .with_client_config( - MockClientConfig::builder() - .client_id(client_id_on_b.clone()) - .latest_height(Height::new(0, proof_height).unwrap()) - .build(), + .with_light_client( + &client_id_on_b, + LightClientState::::with_latest_height(Height::new(0, proof_height).unwrap()), ) .with_connection(conn_id_on_b, conn_end_on_b) .with_channel(msg.port_id_on_b.clone(), ChannelId::zero(), chan_end_on_b); let msg_envelope = MsgEnvelope::from(ChannelMsg::from(msg)); - let res = execute(&mut context, &mut router, msg_envelope); + let res = execute(&mut context.ibc_store, &mut router, msg_envelope); assert!(res.is_ok(), "Execution happy path"); @@ -158,17 +156,15 @@ fn chan_open_confirm_fail_no_channel(fixture: Fixture) { .. } = fixture; let context = context - .with_client_config( - MockClientConfig::builder() - .client_id(client_id_on_b.clone()) - .latest_height(Height::new(0, proof_height).unwrap()) - .build(), + .with_light_client( + &client_id_on_b, + LightClientState::::with_latest_height(Height::new(0, proof_height).unwrap()), ) .with_connection(conn_id_on_b, conn_end_on_b); let msg_envelope = MsgEnvelope::from(ChannelMsg::from(msg)); - let res = validate(&context, &router, msg_envelope); + let res = validate(&context.ibc_store, &router, msg_envelope); assert!( res.is_err(), @@ -198,18 +194,16 @@ fn chan_open_confirm_fail_channel_wrong_state(fixture: Fixture) { ) .unwrap(); let context = context - .with_client_config( - MockClientConfig::builder() - .client_id(client_id_on_b.clone()) - .latest_height(Height::new(0, proof_height).unwrap()) - .build(), + .with_light_client( + &client_id_on_b, + LightClientState::::with_latest_height(Height::new(0, proof_height).unwrap()), ) .with_connection(conn_id_on_b, conn_end_on_b) .with_channel(msg.port_id_on_b.clone(), ChannelId::zero(), wrong_chan_end); let msg_envelope = MsgEnvelope::from(ChannelMsg::from(msg)); - let res = validate(&context, &router, msg_envelope); + let res = validate(&context.ibc_store, &router, msg_envelope); assert!( res.is_err(), diff --git a/ibc-testkit/tests/core/ics04_channel/chan_open_init.rs b/ibc-testkit/tests/core/ics04_channel/chan_open_init.rs index a76f34d03..844af3d65 100644 --- a/ibc-testkit/tests/core/ics04_channel/chan_open_init.rs +++ b/ibc-testkit/tests/core/ics04_channel/chan_open_init.rs @@ -8,10 +8,12 @@ use ibc::core::handler::types::events::{IbcEvent, MessageEvent}; use ibc::core::handler::types::msgs::MsgEnvelope; use ibc::core::host::types::identifiers::ConnectionId; use ibc::core::host::ValidationContext; +use ibc_testkit::context::MockContext; use ibc_testkit::fixtures::core::channel::dummy_raw_msg_chan_open_init; use ibc_testkit::fixtures::core::connection::dummy_msg_conn_open_init; +use ibc_testkit::hosts::MockHost; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::{MockClientConfig, MockContext}; +use ibc_testkit::testapp::ibc::core::types::{DefaultIbcStore, LightClientState}; use rstest::*; use test_log::test; @@ -46,11 +48,9 @@ fn fixture() -> Fixture { .unwrap(); let ctx = default_ctx - .with_client_config( - MockClientConfig::builder() - .client_id(client_id_on_a.clone()) - .latest_height(client_height) - .build(), + .with_light_client( + &client_id_on_a, + LightClientState::::with_latest_height(client_height), ) .with_connection(ConnectionId::zero(), conn_end_on_a); @@ -63,7 +63,7 @@ fn chan_open_init_validate_happy_path(fixture: Fixture) { ctx, router, msg, .. } = fixture; - let res = validate(&ctx, &router, msg); + let res = validate(&ctx.ibc_store, &router, msg); assert!(res.is_ok(), "Validation succeeds; good parameters") } @@ -76,7 +76,7 @@ fn chan_open_init_validate_counterparty_chan_id_set(fixture: Fixture) { let msg_envelope = MsgEnvelope::from(ChannelMsg::from(msg)); - let res = validate(&ctx, &router, msg_envelope); + let res = validate(&ctx.ibc_store, &router, msg_envelope); assert!( res.is_ok(), @@ -93,11 +93,11 @@ fn chan_open_init_execute_happy_path(fixture: Fixture) { .. } = fixture; - let res = execute(&mut ctx, &mut router, msg); + let res = execute(&mut ctx.ibc_store, &mut router, msg); assert!(res.is_ok(), "Execution succeeds; good parameters"); - assert_eq!(ctx.channel_counter().unwrap(), 1); + assert_eq!(ctx.ibc_store.channel_counter().unwrap(), 1); let ibc_events = ctx.get_events(); @@ -114,7 +114,7 @@ fn chan_open_init_execute_happy_path(fixture: Fixture) { fn chan_open_init_fail_no_connection(fixture: Fixture) { let Fixture { router, msg, .. } = fixture; - let res = validate(&MockContext::default(), &router, msg); + let res = validate(&DefaultIbcStore::default(), &router, msg); assert!( res.is_err(), diff --git a/ibc-testkit/tests/core/ics04_channel/chan_open_try.rs b/ibc-testkit/tests/core/ics04_channel/chan_open_try.rs index e9b121dba..3a43d0dfb 100644 --- a/ibc-testkit/tests/core/ics04_channel/chan_open_try.rs +++ b/ibc-testkit/tests/core/ics04_channel/chan_open_try.rs @@ -10,11 +10,13 @@ use ibc::core::handler::types::msgs::MsgEnvelope; use ibc::core::host::types::identifiers::{ClientId, ConnectionId}; use ibc::core::host::ValidationContext; use ibc::core::primitives::*; +use ibc_testkit::context::MockContext; use ibc_testkit::fixtures::core::channel::dummy_raw_msg_chan_open_try; use ibc_testkit::fixtures::core::connection::dummy_raw_counterparty_conn; +use ibc_testkit::hosts::MockHost; use ibc_testkit::testapp::ibc::clients::mock::client_state::client_type as mock_client_type; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::{MockClientConfig, MockContext}; +use ibc_testkit::testapp::ibc::core::types::LightClientState; use rstest::*; use test_log::test; @@ -83,15 +85,13 @@ fn chan_open_try_validate_happy_path(fixture: Fixture) { } = fixture; let ctx = ctx - .with_client_config( - MockClientConfig::builder() - .client_id(client_id_on_b.clone()) - .latest_height(Height::new(0, proof_height).unwrap()) - .build(), + .with_light_client( + &client_id_on_b, + LightClientState::::with_latest_height(Height::new(0, proof_height).unwrap()), ) .with_connection(conn_id_on_b, conn_end_on_b); - let res = validate(&ctx, &router, msg); + let res = validate(&ctx.ibc_store, &router, msg); assert!(res.is_ok(), "Validation success: happy path") } @@ -110,19 +110,17 @@ fn chan_open_try_execute_happy_path(fixture: Fixture) { } = fixture; let mut ctx = ctx - .with_client_config( - MockClientConfig::builder() - .client_id(client_id_on_b.clone()) - .latest_height(Height::new(0, proof_height).unwrap()) - .build(), + .with_light_client( + &client_id_on_b, + LightClientState::::with_latest_height(Height::new(0, proof_height).unwrap()), ) .with_connection(conn_id_on_b, conn_end_on_b); - let res = execute(&mut ctx, &mut router, msg); + let res = execute(&mut ctx.ibc_store, &mut router, msg); assert!(res.is_ok(), "Execution success: happy path"); - assert_eq!(ctx.channel_counter().unwrap(), 1); + assert_eq!(ctx.ibc_store.channel_counter().unwrap(), 1); let ibc_events = ctx.get_events(); @@ -141,7 +139,7 @@ fn chan_open_try_fail_no_connection(fixture: Fixture) { ctx, router, msg, .. } = fixture; - let res = validate(&ctx, &router, msg); + let res = validate(&ctx.ibc_store, &router, msg); assert!( res.is_err(), @@ -161,7 +159,7 @@ fn chan_open_try_fail_no_client_state(fixture: Fixture) { } = fixture; let ctx = ctx.with_connection(conn_id_on_b, conn_end_on_b); - let res = validate(&ctx, &router, msg); + let res = validate(&ctx.ibc_store, &router, msg); assert!( res.is_err(), diff --git a/ibc-testkit/tests/core/ics04_channel/recv_packet.rs b/ibc-testkit/tests/core/ics04_channel/recv_packet.rs index a11a90837..df946f1e3 100644 --- a/ibc-testkit/tests/core/ics04_channel/recv_packet.rs +++ b/ibc-testkit/tests/core/ics04_channel/recv_packet.rs @@ -2,7 +2,6 @@ use ibc::core::channel::types::channel::{ChannelEnd, Counterparty, Order, State} use ibc::core::channel::types::msgs::{MsgRecvPacket, PacketMsg}; use ibc::core::channel::types::packet::Packet; use ibc::core::channel::types::Version; -use ibc::core::client::context::ClientExecutionContext; use ibc::core::client::types::Height; use ibc::core::commitment_types::commitment::CommitmentPrefix; use ibc::core::connection::types::version::Version as ConnectionVersion; @@ -13,13 +12,13 @@ use ibc::core::entrypoint::{execute, validate}; use ibc::core::handler::types::events::{IbcEvent, MessageEvent}; use ibc::core::handler::types::msgs::MsgEnvelope; use ibc::core::host::types::identifiers::{ChannelId, ClientId, ConnectionId, PortId}; -use ibc::core::host::ExecutionContext; use ibc::core::primitives::*; +use ibc_testkit::context::MockContext; use ibc_testkit::fixtures::core::channel::{dummy_msg_recv_packet, dummy_raw_msg_recv_packet}; use ibc_testkit::fixtures::core::signer::dummy_account_id; -use ibc_testkit::relayer::context::RelayerContext; +use ibc_testkit::hosts::MockHost; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::{MockClientConfig, MockContext}; +use ibc_testkit::testapp::ibc::core::types::LightClientState; use rstest::*; use test_log::test; @@ -42,7 +41,7 @@ fn fixture() -> Fixture { let router = MockRouter::new_with_transfer(); - let host_height = context.query_latest_height().unwrap().increment(); + let host_height = context.latest_height().increment(); let client_height = host_height.increment(); @@ -66,7 +65,7 @@ fn fixture() -> Fixture { ConnectionCounterparty::new( client_id.clone(), Some(ConnectionId::zero()), - CommitmentPrefix::empty(), + CommitmentPrefix::try_from(vec![0]).expect("no error"), ), ConnectionVersion::compatibles(), ZERO_DURATION, @@ -96,7 +95,7 @@ fn recv_packet_fail_no_channel(fixture: Fixture) { let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = validate(&context, &router, msg_envelope); + let res = validate(&context.ibc_store, &router, msg_envelope); assert!( res.is_err(), @@ -114,16 +113,14 @@ fn recv_packet_validate_happy_path(fixture: Fixture) { chan_end_on_b, client_height, host_height, - client_id, .. } = fixture; let packet = &msg.packet; - let mut context = context - .with_client_config( - MockClientConfig::builder() - .latest_height(client_height) - .build(), + let context = context + .with_light_client( + &ClientId::new("07-tendermint", 0).expect("no error"), + LightClientState::::with_latest_height(client_height), ) .with_connection(ConnectionId::zero(), conn_end_on_b) .with_channel( @@ -136,7 +133,7 @@ fn recv_packet_validate_happy_path(fixture: Fixture) { packet.chan_id_on_b.clone(), 1.into(), ) - .with_height(host_height) + .advance_block_up_to_height(host_height) // This `with_recv_sequence` is required for ordered channels .with_recv_sequence( packet.port_id_on_b.clone(), @@ -144,19 +141,9 @@ fn recv_packet_validate_happy_path(fixture: Fixture) { packet.seq_on_a, ); - context - .get_client_execution_context() - .store_update_meta( - client_id, - client_height, - Timestamp::from_nanoseconds(1000).unwrap(), - Height::new(0, 5).unwrap(), - ) - .unwrap(); - let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = validate(&context, &router, msg_envelope); + let res = validate(&context.ibc_store, &router, msg_envelope); assert!( res.is_ok(), @@ -198,17 +185,16 @@ fn recv_packet_timeout_expired(fixture: Fixture) { let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg_packet_old)); let context = context - .with_client_config( - MockClientConfig::builder() - .latest_height(client_height) - .build(), + .with_light_client( + &ClientId::new("07-tendermint", 0).expect("no error"), + LightClientState::::with_latest_height(client_height), ) .with_connection(ConnectionId::zero(), conn_end_on_b) .with_channel(PortId::transfer(), ChannelId::zero(), chan_end_on_b) .with_send_sequence(PortId::transfer(), ChannelId::zero(), 1.into()) - .with_height(host_height); + .advance_block_up_to_height(host_height); - let res = validate(&context, &router, msg_envelope); + let res = validate(&context.ibc_store, &router, msg_envelope); assert!( res.is_err(), @@ -228,17 +214,16 @@ fn recv_packet_execute_happy_path(fixture: Fixture) { .. } = fixture; let mut ctx = context - .with_client_config( - MockClientConfig::builder() - .latest_height(client_height) - .build(), + .with_light_client( + &ClientId::new("07-tendermint", 0).expect("no error"), + LightClientState::::with_latest_height(client_height), ) .with_connection(ConnectionId::zero(), conn_end_on_b) .with_channel(PortId::transfer(), ChannelId::zero(), chan_end_on_b); let msg_env = MsgEnvelope::from(PacketMsg::from(msg)); - let res = execute(&mut ctx, &mut router, msg_env); + let res = execute(&mut ctx.ibc_store, &mut router, msg_env); assert!(res.is_ok()); diff --git a/ibc-testkit/tests/core/ics04_channel/send_packet.rs b/ibc-testkit/tests/core/ics04_channel/send_packet.rs index 15d3c82f4..3a2bace44 100644 --- a/ibc-testkit/tests/core/ics04_channel/send_packet.rs +++ b/ibc-testkit/tests/core/ics04_channel/send_packet.rs @@ -15,8 +15,10 @@ use ibc::core::connection::types::{ use ibc::core::handler::types::events::{IbcEvent, MessageEvent}; use ibc::core::host::types::identifiers::{ChannelId, ClientId, ConnectionId, PortId}; use ibc::core::primitives::*; +use ibc_testkit::context::MockContext; use ibc_testkit::fixtures::core::channel::dummy_raw_packet; -use ibc_testkit::testapp::ibc::core::types::{MockClientConfig, MockContext}; +use ibc_testkit::hosts::MockHost; +use ibc_testkit::testapp::ibc::core::types::LightClientState; use test_log::test; #[test] @@ -30,8 +32,6 @@ fn send_packet_processing() { want_pass: bool, } - let context = MockContext::default(); - let chan_end_on_a = ChannelEnd::new( State::Open, Order::Unordered, @@ -45,9 +45,9 @@ fn send_packet_processing() { ConnectionState::Open, default_client_id.clone(), ConnectionCounterparty::new( - default_client_id.clone(), + default_client_id, Some(ConnectionId::zero()), - CommitmentPrefix::empty(), + CommitmentPrefix::try_from(vec![0]).expect("no error"), ), ConnectionVersion::compatibles(), ZERO_DURATION, @@ -95,18 +95,16 @@ fn send_packet_processing() { let tests: Vec = vec![ Test { name: "Processing fails because no channel exists in the context".to_string(), - ctx: context.clone(), + ctx: MockContext::default(), packet: packet.clone(), want_pass: false, }, Test { name: "Good parameters".to_string(), - ctx: context - .clone() - .with_client_config( - MockClientConfig::builder() - .latest_height(client_height) - .build(), + ctx: MockContext::default() + .with_light_client( + &ClientId::new("07-tendermint", 0).expect("no error"), + LightClientState::::with_latest_height(client_height), ) .with_connection(ConnectionId::zero(), conn_end_on_a.clone()) .with_channel(PortId::transfer(), ChannelId::zero(), chan_end_on_a.clone()) @@ -116,12 +114,10 @@ fn send_packet_processing() { }, Test { name: "Packet timeout height same as destination chain height".to_string(), - ctx: context - .clone() - .with_client_config( - MockClientConfig::builder() - .latest_height(client_height) - .build(), + ctx: MockContext::default() + .with_light_client( + &ClientId::new("07-tendermint", 0).expect("no error"), + LightClientState::::with_latest_height(client_height), ) .with_connection(ConnectionId::zero(), conn_end_on_a.clone()) .with_channel(PortId::transfer(), ChannelId::zero(), chan_end_on_a.clone()) @@ -131,12 +127,10 @@ fn send_packet_processing() { }, Test { name: "Packet timeout height one more than destination chain height".to_string(), - ctx: context - .clone() - .with_client_config( - MockClientConfig::builder() - .latest_height(client_height) - .build(), + ctx: MockContext::default() + .with_light_client( + &ClientId::new("07-tendermint", 0).expect("no error"), + LightClientState::::with_latest_height(client_height), ) .with_connection(ConnectionId::zero(), conn_end_on_a.clone()) .with_channel(PortId::transfer(), ChannelId::zero(), chan_end_on_a.clone()) @@ -146,12 +140,10 @@ fn send_packet_processing() { }, Test { name: "Packet without height and timestamp timeout".to_string(), - ctx: context - .clone() - .with_client_config( - MockClientConfig::builder() - .latest_height(client_height) - .build(), + ctx: MockContext::default() + .with_light_client( + &ClientId::new("07-tendermint", 0).expect("no error"), + LightClientState::::with_latest_height(client_height), ) .with_connection(ConnectionId::zero(), conn_end_on_a.clone()) .with_channel(PortId::transfer(), ChannelId::zero(), chan_end_on_a.clone()) @@ -161,11 +153,10 @@ fn send_packet_processing() { }, Test { name: "Packet timeout due to timestamp".to_string(), - ctx: context - .with_client_config( - MockClientConfig::builder() - .latest_height(client_height) - .build(), + ctx: MockContext::default() + .with_light_client( + &ClientId::new("07-tendermint", 0).expect("no error"), + LightClientState::::with_latest_height(client_height), ) .with_connection(ConnectionId::zero(), conn_end_on_a) .with_channel(PortId::transfer(), ChannelId::zero(), chan_end_on_a) @@ -178,7 +169,7 @@ fn send_packet_processing() { .collect(); for mut test in tests { - let res = send_packet(&mut test.ctx, test.packet.clone()); + let res = send_packet(&mut test.ctx.ibc_store, test.packet.clone()); // Additionally check the events and the output objects in the result. match res { Ok(()) => { @@ -187,7 +178,7 @@ fn send_packet_processing() { "send_packet: test passed but was supposed to fail for test: {}, \nparams {:?} {:?}", test.name, test.packet.clone(), - test.ctx.clone() + test.ctx ); let ibc_events = test.ctx.get_events(); @@ -208,7 +199,7 @@ fn send_packet_processing() { "send_packet: did not pass test: {}, \nparams {:?} {:?} error: {:?}", test.name, test.packet.clone(), - test.ctx.clone(), + test.ctx, e, ); } diff --git a/ibc-testkit/tests/core/ics04_channel/timeout.rs b/ibc-testkit/tests/core/ics04_channel/timeout.rs index 05036636a..240ddd4a8 100644 --- a/ibc-testkit/tests/core/ics04_channel/timeout.rs +++ b/ibc-testkit/tests/core/ics04_channel/timeout.rs @@ -13,23 +13,25 @@ use ibc::core::entrypoint::{execute, validate}; use ibc::core::handler::types::events::{IbcEvent, MessageEvent}; use ibc::core::handler::types::msgs::MsgEnvelope; use ibc::core::host::types::identifiers::{ChannelId, ClientId, ConnectionId, PortId}; -use ibc::core::host::ExecutionContext; +use ibc::core::host::types::path::ClientConsensusStatePath; use ibc::core::primitives::*; +use ibc_testkit::context::MockContext; use ibc_testkit::fixtures::core::channel::dummy_raw_msg_timeout; +use ibc_testkit::hosts::MockHost; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::{MockClientConfig, MockContext}; +use ibc_testkit::testapp::ibc::core::types::LightClientState; use rstest::*; struct Fixture { ctx: MockContext, pub router: MockRouter, + client_id: ClientId, client_height: Height, msg: MsgTimeout, packet_commitment: PacketCommitment, conn_end_on_a: ConnectionEnd, chan_end_on_a_ordered: ChannelEnd, chan_end_on_a_unordered: ChannelEnd, - client_id: ClientId, } #[fixture] @@ -37,19 +39,19 @@ fn fixture() -> Fixture { let client_id = ClientId::new("07-tendermint", 0).expect("no error"); let client_height = Height::new(0, 2).unwrap(); - let ctx = MockContext::default().with_client_config( - MockClientConfig::builder() - .latest_height(client_height) - .build(), + let ctx = MockContext::default().with_light_client( + &ClientId::new("07-tendermint", 0).expect("no error"), + LightClientState::::with_latest_height(client_height), ); let client_height = Height::new(0, 2).unwrap(); let router = MockRouter::new_with_transfer(); + // in case of timeout, timeout timestamp should be less than host's timestamp + let timeout_timestamp = ctx.latest_timestamp().nanoseconds() - 1; let msg_proof_height = 2; let msg_timeout_height = 5; - let timeout_timestamp = Timestamp::now().nanoseconds(); let msg = MsgTimeout::try_from(dummy_raw_msg_timeout( msg_proof_height, @@ -84,7 +86,7 @@ fn fixture() -> Fixture { ConnectionCounterparty::new( client_id.clone(), Some(ConnectionId::zero()), - CommitmentPrefix::empty(), + CommitmentPrefix::try_from(vec![0]).expect("no error"), ), ConnectionVersion::compatibles(), ZERO_DURATION, @@ -94,13 +96,13 @@ fn fixture() -> Fixture { Fixture { ctx, router, + client_id, client_height, msg, packet_commitment, conn_end_on_a, chan_end_on_a_ordered, chan_end_on_a_unordered, - client_id, } } @@ -113,13 +115,12 @@ fn timeout_fail_no_channel(fixture: Fixture) { client_height, .. } = fixture; - let ctx = ctx.with_client_config( - MockClientConfig::builder() - .latest_height(client_height) - .build(), + let ctx = ctx.with_light_client( + &ClientId::new("07-tendermint", 0).expect("no error"), + LightClientState::::with_latest_height(client_height), ); let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = validate(&ctx, &router, msg_envelope); + let res = validate(&ctx.ibc_store, &router, msg_envelope); assert!( res.is_err(), @@ -136,12 +137,13 @@ fn timeout_fail_no_consensus_state_for_height(fixture: Fixture) { chan_end_on_a_unordered, conn_end_on_a, packet_commitment, + client_id, .. } = fixture; let packet = msg.packet.clone(); - let ctx = ctx + let mut ctx = ctx .with_channel( PortId::transfer(), ChannelId::zero(), @@ -155,9 +157,19 @@ fn timeout_fail_no_consensus_state_for_height(fixture: Fixture) { packet_commitment, ); + let consensus_state_path = ClientConsensusStatePath::new( + client_id, + msg.proof_height_on_b.revision_number(), + msg.proof_height_on_b.revision_height(), + ); + + ctx.ibc_store + .delete_consensus_state(consensus_state_path) + .expect("consensus state exists"); + let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = validate(&ctx, &router, msg_envelope); + let res = validate(&ctx.ibc_store, &router, msg_envelope); assert!( res.is_err(), @@ -174,7 +186,6 @@ fn timeout_fail_proof_timeout_not_reached(fixture: Fixture) { chan_end_on_a_unordered, conn_end_on_a, client_height, - client_id, .. } = fixture; @@ -190,11 +201,10 @@ fn timeout_fail_proof_timeout_not_reached(fixture: Fixture) { let packet = msg.packet.clone(); - let mut ctx = ctx - .with_client_config( - MockClientConfig::builder() - .latest_height(client_height) - .build(), + let ctx = ctx + .with_light_client( + &ClientId::new("07-tendermint", 0).expect("no error"), + LightClientState::::with_latest_height(client_height), ) .with_connection(ConnectionId::zero(), conn_end_on_a) .with_channel( @@ -209,17 +219,9 @@ fn timeout_fail_proof_timeout_not_reached(fixture: Fixture) { packet_commitment, ); - ctx.store_update_meta( - client_id, - client_height, - Timestamp::from_nanoseconds(5).unwrap(), - Height::new(0, 4).unwrap(), - ) - .unwrap(); - let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = validate(&ctx, &router, msg_envelope); + let res = validate(&ctx.ibc_store, &router, msg_envelope); assert!( res.is_err(), @@ -248,7 +250,7 @@ fn timeout_success_no_packet_commitment(fixture: Fixture) { let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = validate(&ctx, &router, msg_envelope); + let res = validate(&ctx.ibc_store, &router, msg_envelope); assert!( res.is_ok(), @@ -266,17 +268,15 @@ fn timeout_unordered_channel_validate(fixture: Fixture) { conn_end_on_a, packet_commitment, client_height, - client_id, .. } = fixture; let packet = msg.packet.clone(); - let mut ctx = ctx - .with_client_config( - MockClientConfig::builder() - .latest_height(client_height) - .build(), + let ctx = ctx + .with_light_client( + &ClientId::new("07-tendermint", 0).expect("no error"), + LightClientState::::with_latest_height(client_height), ) .with_connection(ConnectionId::zero(), conn_end_on_a) .with_channel( @@ -291,18 +291,9 @@ fn timeout_unordered_channel_validate(fixture: Fixture) { packet_commitment, ); - ctx.get_client_execution_context() - .store_update_meta( - client_id, - client_height, - Timestamp::from_nanoseconds(1000).unwrap(), - Height::new(0, 5).unwrap(), - ) - .unwrap(); - let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = validate(&ctx, &router, msg_envelope); + let res = validate(&ctx.ibc_store, &router, msg_envelope); assert!(res.is_ok(), "Good parameters for unordered channels") } @@ -317,17 +308,15 @@ fn timeout_ordered_channel_validate(fixture: Fixture) { conn_end_on_a, packet_commitment, client_height, - client_id, .. } = fixture; let packet = msg.packet.clone(); - let mut ctx = ctx - .with_client_config( - MockClientConfig::builder() - .latest_height(client_height) - .build(), + let ctx = ctx + .with_light_client( + &ClientId::new("07-tendermint", 0).expect("no error"), + LightClientState::::with_latest_height(client_height), ) .with_connection(ConnectionId::zero(), conn_end_on_a) .with_channel(PortId::transfer(), ChannelId::zero(), chan_end_on_a_ordered) @@ -338,17 +327,9 @@ fn timeout_ordered_channel_validate(fixture: Fixture) { packet_commitment, ); - ctx.store_update_meta( - client_id, - client_height, - Timestamp::from_nanoseconds(1000).unwrap(), - Height::new(0, 4).unwrap(), - ) - .unwrap(); - let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = validate(&ctx, &router, msg_envelope); + let res = validate(&ctx.ibc_store, &router, msg_envelope); assert!(res.is_ok(), "Good parameters for unordered channels") } @@ -380,7 +361,7 @@ fn timeout_unordered_chan_execute(fixture: Fixture) { let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = execute(&mut ctx, &mut router, msg_envelope); + let res = execute(&mut ctx.ibc_store, &mut router, msg_envelope); assert!(res.is_ok()); @@ -418,7 +399,7 @@ fn timeout_ordered_chan_execute(fixture: Fixture) { let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = execute(&mut ctx, &mut router, msg_envelope); + let res = execute(&mut ctx.ibc_store, &mut router, msg_envelope); assert!(res.is_ok()); diff --git a/ibc-testkit/tests/core/ics04_channel/timeout_on_close.rs b/ibc-testkit/tests/core/ics04_channel/timeout_on_close.rs index 39a4c306a..a454261fd 100644 --- a/ibc-testkit/tests/core/ics04_channel/timeout_on_close.rs +++ b/ibc-testkit/tests/core/ics04_channel/timeout_on_close.rs @@ -2,7 +2,6 @@ use ibc::core::channel::types::channel::{ChannelEnd, Counterparty, Order, State} use ibc::core::channel::types::commitment::{compute_packet_commitment, PacketCommitment}; use ibc::core::channel::types::msgs::{MsgTimeoutOnClose, PacketMsg}; use ibc::core::channel::types::Version; -use ibc::core::client::context::ClientExecutionContext; use ibc::core::client::types::Height; use ibc::core::commitment_types::commitment::CommitmentPrefix; use ibc::core::connection::types::version::Version as ConnectionVersion; @@ -12,11 +11,12 @@ use ibc::core::connection::types::{ use ibc::core::entrypoint::validate; use ibc::core::handler::types::msgs::MsgEnvelope; use ibc::core::host::types::identifiers::{ChannelId, ClientId, ConnectionId, PortId}; -use ibc::core::host::ExecutionContext; use ibc::core::primitives::*; +use ibc_testkit::context::MockContext; use ibc_testkit::fixtures::core::channel::dummy_raw_msg_timeout_on_close; +use ibc_testkit::hosts::MockHost; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::{MockClientConfig, MockContext}; +use ibc_testkit::testapp::ibc::core::types::LightClientState; use rstest::*; pub struct Fixture { @@ -33,10 +33,9 @@ fn fixture() -> Fixture { let default_client_id = ClientId::new("07-tendermint", 0).expect("no error"); let client_height = Height::new(0, 2).unwrap(); - let context = MockContext::default().with_client_config( - MockClientConfig::builder() - .latest_height(client_height) - .build(), + let context = MockContext::default().with_light_client( + &ClientId::new("07-tendermint", 0).expect("no error"), + LightClientState::::with_latest_height(client_height), ); let router = MockRouter::new_with_transfer(); @@ -68,9 +67,9 @@ fn fixture() -> Fixture { ConnectionState::Open, default_client_id.clone(), ConnectionCounterparty::new( - default_client_id.clone(), + default_client_id, Some(ConnectionId::zero()), - CommitmentPrefix::empty(), + CommitmentPrefix::try_from(vec![0]).expect("no error"), ), ConnectionVersion::compatibles(), ZERO_DURATION, @@ -98,7 +97,7 @@ fn timeout_on_close_fail_no_channel(fixture: Fixture) { let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = validate(&context, &router, msg_envelope); + let res = validate(&context.ibc_store, &router, msg_envelope); assert!( res.is_err(), @@ -123,7 +122,7 @@ fn timeout_on_close_success_no_packet_commitment(fixture: Fixture) { let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = validate(&context, &router, msg_envelope); + let res = validate(&context.ibc_store, &router, msg_envelope); assert!( res.is_ok(), @@ -133,8 +132,6 @@ fn timeout_on_close_success_no_packet_commitment(fixture: Fixture) { #[rstest] fn timeout_on_close_success_happy_path(fixture: Fixture) { - let default_client_id = ClientId::new("07-tendermint", 0).expect("no error"); - let Fixture { context, router, @@ -144,7 +141,7 @@ fn timeout_on_close_success_happy_path(fixture: Fixture) { chan_end_on_a, .. } = fixture; - let mut context = context + let context = context .with_channel(PortId::transfer(), ChannelId::zero(), chan_end_on_a) .with_connection(ConnectionId::zero(), conn_end_on_a) .with_packet_commitment( @@ -154,19 +151,9 @@ fn timeout_on_close_success_happy_path(fixture: Fixture) { packet_commitment, ); - context - .get_client_execution_context() - .store_update_meta( - default_client_id, - Height::new(0, 2).unwrap(), - Timestamp::from_nanoseconds(5000).unwrap(), - Height::new(0, 5).unwrap(), - ) - .unwrap(); - let msg_envelope = MsgEnvelope::from(PacketMsg::from(msg)); - let res = validate(&context, &router, msg_envelope); + let res = validate(&context.ibc_store, &router, msg_envelope); assert!( res.is_ok(), diff --git a/ibc-testkit/tests/core/router.rs b/ibc-testkit/tests/core/router.rs index 0c00926c6..5bc657702 100644 --- a/ibc-testkit/tests/core/router.rs +++ b/ibc-testkit/tests/core/router.rs @@ -1,3 +1,5 @@ +use core::ops::Add; + use ibc::apps::transfer::handler::send_transfer; use ibc::apps::transfer::types::error::TokenTransferError; use ibc::apps::transfer::types::msgs::transfer::MsgTransfer; @@ -20,6 +22,7 @@ use ibc::core::host::types::path::CommitmentPath; use ibc::core::host::ValidationContext; use ibc::core::primitives::prelude::*; use ibc::core::primitives::Timestamp; +use ibc_testkit::context::MockContext; use ibc_testkit::fixtures::applications::transfer::{ extract_transfer_packet, MsgTransferConfig, PacketDataConfig, }; @@ -33,13 +36,13 @@ use ibc_testkit::fixtures::core::connection::{ dummy_msg_conn_open_ack, dummy_msg_conn_open_init, dummy_msg_conn_open_init_with_client_id, dummy_msg_conn_open_try, msg_conn_open_try_with_client_id, }; +use ibc_testkit::fixtures::core::context::TestContextConfig; use ibc_testkit::fixtures::core::signer::dummy_account_id; use ibc_testkit::testapp::ibc::applications::transfer::types::DummyTransferModule; use ibc_testkit::testapp::ibc::clients::mock::client_state::MockClientState; use ibc_testkit::testapp::ibc::clients::mock::consensus_state::MockConsensusState; use ibc_testkit::testapp::ibc::clients::mock::header::MockHeader; use ibc_testkit::testapp::ibc::core::router::MockRouter; -use ibc_testkit::testapp::ibc::core::types::MockContext; use test_log::test; #[test] @@ -87,14 +90,23 @@ fn routing_module_and_keepers() { let upgrade_client_height_second = Height::new(1, 1).unwrap(); // We reuse this same context across all tests. Nothing in particular needs parametrizing. - let mut ctx = MockContext::default(); + let mut ctx = TestContextConfig::builder() + // a future timestamp, so that submitted packets are considered from past + // not more than 5 secs, as later dummy_raw_msg_timeout_on_close(*, 5) is used + .latest_timestamp( + Timestamp::now() + .add(core::time::Duration::from_secs(4)) + .unwrap(), + ) + .build::(); let mut router = MockRouter::new_with_transfer(); + let header = MockHeader::new(start_client_height).with_current_timestamp(); + let create_client_msg = MsgCreateClient::new( - MockClientState::new(MockHeader::new(start_client_height).with_current_timestamp()).into(), - MockConsensusState::new(MockHeader::new(start_client_height).with_current_timestamp()) - .into(), + MockClientState::new(header).into(), + MockConsensusState::new(header).into(), default_signer.clone(), ); @@ -156,7 +168,7 @@ fn routing_module_and_keepers() { .build(); let msg_transfer_no_timeout_or_timestamp = MsgTransferConfig::builder() - .packet_data(packet_data.clone()) + .packet_data(packet_data) // Timestamp::from_nanoseconds(0) and Timestamp::none() are equivalent .timeout_timestamp_on_b(Timestamp::from_nanoseconds(0).unwrap()) .build(); @@ -181,7 +193,7 @@ fn routing_module_and_keepers() { // First, create a client.. let res = dispatch( - &mut ctx, + &mut ctx.ibc_store, &mut router, MsgEnvelope::Client(ClientMsg::CreateClient(create_client_msg.clone())), ); @@ -318,12 +330,13 @@ fn routing_module_and_keepers() { msg: MsgEnvelope::Packet(PacketMsg::Ack(msg_ack_packet.clone())).into(), want_pass: true, state_check: Some(Box::new(move |ctx| { - ctx.get_packet_commitment(&CommitmentPath::new( - &msg_ack_packet.packet.port_id_on_a, - &msg_ack_packet.packet.chan_id_on_a, - msg_ack_packet.packet.seq_on_a, - )) - .is_err() + ctx.ibc_store + .get_packet_commitment(&CommitmentPath::new( + &msg_ack_packet.packet.port_id_on_a, + &msg_ack_packet.packet.chan_id_on_a, + msg_ack_packet.packet.seq_on_a, + )) + .is_err() })), }, Test { @@ -404,8 +417,8 @@ fn routing_module_and_keepers() { for test in tests { let res = match test.msg.clone() { - TestMsg::Ics26(msg) => dispatch(&mut ctx, &mut router, msg).map(|_| ()), - TestMsg::Ics20(msg) => send_transfer(&mut ctx, &mut DummyTransferModule, msg) + TestMsg::Ics26(msg) => dispatch(&mut ctx.ibc_store, &mut router, msg), + TestMsg::Ics20(msg) => send_transfer(&mut ctx.ibc_store, &mut DummyTransferModule, msg) .map_err(|e: TokenTransferError| ChannelError::AppModule { description: e.to_string(), }) diff --git a/ibc-testkit/tests/cosmwasm/fixture.rs b/ibc-testkit/tests/cosmwasm/fixture.rs index 946f9ac66..8e8403342 100644 --- a/ibc-testkit/tests/cosmwasm/fixture.rs +++ b/ibc-testkit/tests/cosmwasm/fixture.rs @@ -89,11 +89,9 @@ impl Fixture { // Setting the `trusting_period` to 1 second allows the quick // client expiry for the tests. let tm_client_state: TmClientState = ClientStateConfig::builder() - .chain_id(self.chain_id.clone()) .trusting_period(Duration::from_secs(1)) - .latest_height(self.trusted_height) .build() - .try_into() + .into_client_state(self.chain_id.clone(), self.trusted_height) .expect("never fails"); let tm_consensus_state = dummy_sov_consensus_state(self.trusted_timestamp);