From e7c23df2bdbf95f71afa6ca43be48b4e508f119e Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Tue, 18 Jun 2024 14:06:04 +0200 Subject: [PATCH 1/3] Add storage to channel lifecycle --- .../ibc/diy-protocol/channel-lifecycle.mdx | 62 ++++++++++++++++--- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/src/pages/ibc/diy-protocol/channel-lifecycle.mdx b/src/pages/ibc/diy-protocol/channel-lifecycle.mdx index de34c2bb..4ea31e39 100644 --- a/src/pages/ibc/diy-protocol/channel-lifecycle.mdx +++ b/src/pages/ibc/diy-protocol/channel-lifecycle.mdx @@ -60,6 +60,8 @@ entrypoint is called on chain B with the `IbcChannelOpenMsg::OpenTry` variant. See the following example and the [`IbcChannelOpenMsg`] documentation. ```rust filename="ibc.rs" template="core" +use cw_storage_plus::Item; + /// enforces ordering and versioning constraints #[entry_point] pub fn ibc_channel_open( @@ -69,10 +71,15 @@ pub fn ibc_channel_open( ) -> StdResult { let channel = msg.channel(); - // here we should check if the channel is what we expect (e.g. the order) + // in this example, we only allow a single channel per contract instance + // you can do more complex checks here + ensure!(!CHANNEL.exists(deps.storage), StdError::generic_err("channel already exists")); + + // we should check if the channel is what we expect (e.g. the order) if channel.order != IbcOrder::Ordered { return Err(StdError::generic_err("only ordered channels are supported")); } + // the OpenTry variant (on chain B) also has the counterparty version // we should check if it is what we expect if let Some(counter_version) = msg.counterparty_version() { @@ -83,12 +90,27 @@ pub fn ibc_channel_open( } } + // now, we save the channel ID to storage, so we can use it later + // this also prevents any further channel openings + CHANNEL.save(deps.storage, &ChannelInfo { + channel_id: channel.endpoint.channel_id.clone(), + finalized: false, + })?; + // return the channel version we support Ok(Some(Ibc3ChannelOpenResponse { version: IBC_APP_VERSION.to_string(), })) } +#[cw_serde] +struct ChannelInfo { + channel_id: String, + /// whether the channel is completely set up + finalized: bool, +} + +const CHANNEL: Item = Item::new("channel"); const IBC_APP_VERSION: &str = "my-protocol-v1"; ``` @@ -104,11 +126,17 @@ counterparty version. #### Permissions Opening a channel is generally a permissionless process, so make sure to keep -that in mind when implementing the entrypoints. You can add additional checks to -ensure that the channel is connecting to the correct counterparty or have a -state item containing a `bool` that is checked here to explicitly disable new -channels. Depending on the protocol, you might also want to limit yourself to a -single channel per contract. +that in mind when implementing the entrypoints. In the examples above, we only +allow a single channel per contract instance and always make sure not to +overwrite an existing channel. Note that we already save the channel in +`ibc_channel_open`. This causes overlapping channel openings to fail the channel +handshake. The drawback is that the contract can only connect to that one +channel, so if the handshake fails, the contract cannot connect to another +channel. + +You can add additional checks to ensure that the channel is connecting to the +correct counterparty or use a map to keep track of multiple channels connecting +to different counterparties. You can also be more restrictive and only allow the contract itself to initiate the channel creation handshake. This can be done by adding a state item to the @@ -234,6 +262,8 @@ chain B. The full data this entrypoint receives can be seen in the [`IbcChannelConnectMsg`] documentation. Here is more example code: ```rust filename="ibc.rs" template="core" +use cw_storage_plus::Item; + pub fn ibc_channel_connect( deps: DepsMut, env: Env, @@ -241,11 +271,27 @@ pub fn ibc_channel_connect( ) -> StdResult { let channel = msg.channel(); - // you probably want to save the `channel.endpoint.channel_id` to storage, - // so you can use it when sending packets + // in this example, we only allow a single channel per contract instance + // you can do more complex checks here + let mut channel_info = CHANNEL.load(deps.storage)?; + ensure!(!channel_info.finalized, StdError::generic_err("channel already finalized")); + debug_assert_eq!(channel_info.channel_id, channel.endpoint.channel_id, "channel ID mismatch"); + + // at this point, we are finished setting up the channel and can mark it as finalized + channel_info.finalized = true; + CHANNEL.save(deps.storage, &channel_info)?; Ok(IbcBasicResponse::new()) } + +#[cw_serde] +struct ChannelInfo { + channel_id: String, + /// whether the channel is completely set up + finalized: bool, +} + +const CHANNEL: Item = Item::new("channel"); ``` [`IbcChannelConnectMsg`]: From 6a4c1d7e84d3be525906a19f55e8fa17d8eaa79e Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Tue, 18 Jun 2024 13:26:27 +0200 Subject: [PATCH 2/3] Add storey --- .github/workflows/doc-test.yml | 1 + docs-test-gen/Cargo.lock | 58 ++++++++++++++++++++++++++++++++++ docs-test-gen/Cargo.toml | 1 + 3 files changed, 60 insertions(+) diff --git a/.github/workflows/doc-test.yml b/.github/workflows/doc-test.yml index fbbf4127..5ca7f660 100644 --- a/.github/workflows/doc-test.yml +++ b/.github/workflows/doc-test.yml @@ -28,6 +28,7 @@ jobs: cargo update -p cosmwasm-std cargo update -p cw2 cargo update -p cw-storage-plus + cargo update -p cw-storey cargo update -p serde - uses: Swatinem/rust-cache@v2 with: diff --git a/docs-test-gen/Cargo.lock b/docs-test-gen/Cargo.lock index 5df94131..25f0f8df 100644 --- a/docs-test-gen/Cargo.lock +++ b/docs-test-gen/Cargo.lock @@ -265,6 +265,18 @@ dependencies = [ "serde", ] +[[package]] +name = "cw-storey" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cc5d79b71429ffc0669b32ec2cf7c5556cec2cf9027ec77ec1c80dbea9b012" +dependencies = [ + "cosmwasm-std", + "rmp-serde", + "serde", + "storey", +] + [[package]] name = "cw2" version = "2.0.0" @@ -413,6 +425,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus", + "cw-storey", "cw2", "glob", "phf", @@ -855,6 +868,28 @@ dependencies = [ "subtle", ] +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -1037,6 +1072,29 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "storey" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aead83852cc9da7676bfa7c3443be9db7f44db6d2980e6d45bb1260dcee494b" +dependencies = [ + "storey-encoding", + "storey-storage", + "thiserror", +] + +[[package]] +name = "storey-encoding" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29845670645c9d3aa61a7419080040a5c7c098677c88fb12d7adb88aa8f94ad0" + +[[package]] +name = "storey-storage" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6a63ea9638817e9fa2f39d9c957bcdb9f3998b9cd9a8045025d35eefa6eb485" + [[package]] name = "strsim" version = "0.11.1" diff --git a/docs-test-gen/Cargo.toml b/docs-test-gen/Cargo.toml index 6784b20f..8f77c380 100644 --- a/docs-test-gen/Cargo.toml +++ b/docs-test-gen/Cargo.toml @@ -19,3 +19,4 @@ sha2 = "0.10.8" cosmos-sdk-proto = { version = "0.21.1", default-features = false } # Used in IBC code cw-storage-plus = "*" serde = "*" +cw-storey = "*" From e19981b55fefb630bec0c7e82f887f3e4793d6dc Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Tue, 18 Jun 2024 15:33:46 +0200 Subject: [PATCH 3/3] Add storey examples --- .../ibc/diy-protocol/channel-lifecycle.mdx | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/src/pages/ibc/diy-protocol/channel-lifecycle.mdx b/src/pages/ibc/diy-protocol/channel-lifecycle.mdx index 4ea31e39..cb203c31 100644 --- a/src/pages/ibc/diy-protocol/channel-lifecycle.mdx +++ b/src/pages/ibc/diy-protocol/channel-lifecycle.mdx @@ -59,6 +59,9 @@ entrypoint with the `IbcChannelOpenMsg::OpenInit` variant. Then the same entrypoint is called on chain B with the `IbcChannelOpenMsg::OpenTry` variant. See the following example and the [`IbcChannelOpenMsg`] documentation. + + + ```rust filename="ibc.rs" template="core" use cw_storage_plus::Item; @@ -114,6 +117,72 @@ const CHANNEL: Item = Item::new("channel"); const IBC_APP_VERSION: &str = "my-protocol-v1"; ``` + + + +```rust filename="ibc.rs" template="core" +use cw_storey::{containers::Item, CwStorage}; + +/// enforces ordering and versioning constraints +#[entry_point] +pub fn ibc_channel_open( + deps: DepsMut, + env: Env, + msg: IbcChannelOpenMsg, +) -> StdResult { + let mut storage = CwStorage(deps.storage); + + let channel = msg.channel(); + + // in this example, we only allow a single channel per contract instance + // you can do more complex checks here + ensure!( + CHANNEL.access(&storage).get()?.is_none(), + StdError::generic_err("channel already exists") + ); + + // we should check if the channel is what we expect (e.g. the order) + if channel.order != IbcOrder::Ordered { + return Err(StdError::generic_err("only ordered channels are supported")); + } + + // the OpenTry variant (on chain B) also has the counterparty version + // we should check if it is what we expect + if let Some(counter_version) = msg.counterparty_version() { + if counter_version != IBC_APP_VERSION { + return Err(StdError::generic_err(format!( + "Counterparty version must be `{IBC_APP_VERSION}`" + ))); + } + } + + // now, we save the channel ID to storage, so we can use it later + // this also prevents any further channel openings + CHANNEL.access(&mut storage).set(&ChannelInfo { + channel_id: channel.endpoint.channel_id.clone(), + finalized: false, + })?; + + // return the channel version we support + Ok(Some(Ibc3ChannelOpenResponse { + version: IBC_APP_VERSION.to_string(), + })) +} + +#[cw_serde] +struct ChannelInfo { + channel_id: String, + /// whether the channel is completely set up + finalized: bool, +} + +const CHANNEL: Item = Item::new(0); +const IBC_APP_VERSION: &str = "my-protocol-v1"; +``` + + + + In the example above, we return the same version we expect from the counterparty, but you can return a different version if the counterparty accepts it. The version is used to ensure that both chains are running the protocol that @@ -261,6 +330,9 @@ variant on chain A, then the `IbcChannelConnectMsg::OpenConfirm` variant on chain B. The full data this entrypoint receives can be seen in the [`IbcChannelConnectMsg`] documentation. Here is more example code: + + + ```rust filename="ibc.rs" template="core" use cw_storage_plus::Item; @@ -294,6 +366,56 @@ struct ChannelInfo { const CHANNEL: Item = Item::new("channel"); ``` + + + +```rust filename="ibc.rs" template="core" +use cw_storey::{containers::Item, CwStorage}; + +pub fn ibc_channel_connect( + deps: DepsMut, + env: Env, + msg: IbcChannelConnectMsg, +) -> StdResult { + let mut storage = CwStorage(deps.storage); + + let channel = msg.channel(); + + // in this example, we only allow a single channel per contract instance + // you can do more complex checks here + let mut channel_info = CHANNEL + .access(&storage) + .get()? + .ok_or_else(|| StdError::generic_err("channel not found"))?; + ensure!( + !channel_info.finalized, + StdError::generic_err("channel already finalized") + ); + debug_assert_eq!( + channel_info.channel_id, channel.endpoint.channel_id, + "channel ID mismatch" + ); + + // at this point, we are finished setting up the channel and can mark it as finalized + channel_info.finalized = true; + CHANNEL.access(&mut storage).set(&channel_info)?; + + Ok(IbcBasicResponse::new()) +} + +#[cw_serde] +struct ChannelInfo { + channel_id: String, + /// whether the channel is completely set up + finalized: bool, +} + +const CHANNEL: Item = Item::new(0); +``` + + + + [`IbcChannelConnectMsg`]: https://docs.rs/cosmwasm-std/latest/cosmwasm_std/enum.IbcChannelConnectMsg.html