-
Notifications
You must be signed in to change notification settings - Fork 350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Investigate different channel types for ibc_channel_open
#888
Comments
So the entry point In this case I suggest an enum argument to Once you have such enums, you also also think about merging all 4 into one entry point. |
The second ones can trigger external actions, the first step not.
This is a nice idea. |
Gathering data here. In CosmWasm, We use However, while the same Go/Protobuf struct is used in all these places, it seems there are some subtle differences of null-ability. In one place, you are guaranteed a field to be present, in another guaranteed to be absent. This is trickier with Go, as they will pass There are 2 questions:
I will follow up with the matrix of what data is defined where to help answer those questions |
Channel Lifecycle: CosmWasm has simplified the ChannelLifecycle callbacks from 6 to 3. You can see them in the full ICS04 spec. Our simplification:
Each side of the connection will receive exactly 2 calls on creation and 1 on closing (corresponsing to the cosmwasm entry points). The difference between the 2 ICS calls is the order (is this side A or side B of the connection doing the first step of the handshake, for example).
(from https://github.com/cosmos/cosmos-sdk/blob/v0.42.7/x/ibc/core/04-channel/keeper/handshake.go) Note that after the handshake, the recorded version on both sides is the version set in
|
My proposal is this:
In addition, this is an optional suggestion to get feedback:
These updates should be made to the |
@webmaster128 I would love your feedback here |
ibc_connect_open
ibc_channel_open
Sounds like a great start. Not sure to what degree #888 (comment) is still useful then.
Great to free the |
I'm not sure if I wouldn't prefer the initiator/non-initiator distinction to be about separate types or enum variants somehow. But I like the idea of checking for something meaningful like |
Something like this? pub struct IbcChannel {
// other fields
pub connection_id: ConnectionID,
}
impl IbcChannel {
pub fn initialized(&self) -> bool {
match self.connection_id {
ConnectionID::Uninit => false,
_ => true,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum ConnectionID {
Uninit,
ID(String),
} |
@uint isnt this basically the same as just impl IbcChannel {
pub fn initialized(&self) -> bool {
!self.counterparty_endpoint.channel_id.is_empty()
}
}
|
Looking at the Go code, I get the feeling that |
The difference is |
Then again, I guess if it's empty strings from Go anyway... yeah, it's not such a strong argument. |
The advantage of |
I think this boolean would be an alternate version of the enum arg you propose. And simpler.
|
The fact that 99% of the usage of the impl IbcChannel {
pub fn initialized(&self) -> bool {
!self.counterparty_endpoint.channel_id.is_empty()
}
} Is one idea and not bad. The pub struct IbcChannelOpenMsg {
pub channel: IbcChannel,
pub side: HandshakeSide,
}
pub enum HandshakeSide {
A,
B,
} If you find this cleaner than a boolean |
My issue is we then have a situation similar to how Go handles errors - a string always exists, but the user has to remember to check another value to even know if the string is meaningful. The enum is nicer since if the thing is |
"Nicer" should be judged by the users (ibc contracts). I never once try to use this field in
|
Possibly a getter that returns an I was wondering about creating a separate channel type called |
I would definitely prefer this to making the |
At that point, I wouldn't worry about making it "easy" to unwrap. People will actually want to switch on both cases explicitly |
I don't. I think we already lost when trying to model 6 different cases in 3 calls. From there, I don't see how we can get back the type safety that is desired in Rust. In #888 (comment) I propose getting back the 6 distinct cases (through 3 entry points), but I think it is fair to consider this overkill. Since I don't have any experience using this, I don't know what is right or wrong. Also no strong oponion how we continue from here. |
I agree with this. I would use Beyond this minor point, I think some people will care to differentiate between the two calls that we merge into one and adding some bool / enum should be included somehow |
Looking back at that, that sounds like a great idea. But yeah, I know little and don't write those contracts. |
I would like to re-iterate my concrete proposal: #888 (comment) There has been a lot of discussion since then, but I am unclear if those points are accepted? Let's make clear what is agreed and what is open. It seems (2) and (3) are agreed up. If so, I would make a new discussion about that point, and then we can focus on those changes we do agree on (2), (3), (4?) |
My idea is this (example): enum IbcChannelOpenMsg {
Init {
channel: IbcChannelInit,
},
Try {
channel: IbcChannel,
counterparty_version: String,
}
}
/// Like IbcChannel but without counterparty_endpoint.channel_id
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct IbcChannelInit {
pub endpoint: IbcEndpoint,
pub counterparty_endpoint: IbcEndpointInit, // Different type here. But do we even have/need port_id in the init case?
pub order: IbcOrder,
pub version: String,
/// The connection upon which this channel was created. If this is a multi-hop
/// channel, we only expose the first hop.
pub connection_id: String,
}
/// Like IbcEndpoint but without channel_id
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct IbcEndpointInit {
pub port_id: String,
// no channel_id
} With that the contract developer switches over |
I'm strongly in favor of something like that. It's clean and we have the notion of "what sort of operation this is" encoded in the type system rather than checked purely at runtime - that's very much Rust's philosophy. For each type of operation the user only gets the data that's meaningful for it - less room for error and clutter. It also looks like it maps more directly and explicitly to concepts users might know from the IBC spec (not that I've read it) - as in different types of operations. Right? |
I feel like we're discussing the direction of the whole thing more broadly at this point, and @webmaster128's proposal affects both 1 and 4. |
Ah, I guess one annoyance here would be if you want to create an entrypoint that doesn't distinguish between pub fn ibc_channel_open(deps: DepsMut, env: Env, msg: IbcChannelOpenMsg) -> StdResult<()> {
let endpoint;
let version;
// yuck!
match msg {
IbcChannelOpenMsg::Init(channel) => {
endpoint = channel.endpoint;
version = channel.version;
}
IbcChannelOpenMsg::Try(channel, _) => {
endpoint = channel.endpoint;
version = channel.version;
}
}
// real code starts here
} |
I think we need to look at the 3 actual example usages here, as you are making an API that is expressive and safe, but I feel difficult to use. pub fn ibc_channel_open(_deps: DepsMut, _env: Env, msg: IbcChannelOpenMsg) -> StdResult<()> {
let channel = msg.channel;
if channel.order != IbcOrder::Ordered {
return Err(StdError::generic_err("Only supports ordered channels"));
}
if channel.version.as_str() != IBC_VERSION {
return Err(StdError::generic_err(format!(
"Must set version to `{}`",
IBC_VERSION
)));
}
// TODO: do we need to check counterparty version as well?
// This flow needs to be well documented
if let Some(counter_version) = channel.counterparty_version {
if counter_version.as_str() != IBC_VERSION {
return Err(StdError::generic_err(format!(
"Counterparty version must be `{}`",
IBC_VERSION
)));
}
}
Ok(())
} pub fn ibc_channel_open(_deps: DepsMut, _env: Env, msg: IbcChannelOpenMsg) -> StdResult<()> {
let channel = msg.channel;
if channel.order != IbcOrder::Ordered {
return Err(StdError::generic_err("Only supports ordered channels"));
}
if channel.version.as_str() != IBC_VERSION {
return Err(StdError::generic_err(format!(
"Must set version to `{}`",
IBC_VERSION
)));
}
// TODO: do we need to check counterparty version as well?
// This flow needs to be well documented
if let Some(counter_version) = channel.counterparty_version {
if counter_version.as_str() != IBC_VERSION {
return Err(StdError::generic_err(format!(
"Counterparty version must be `{}`",
IBC_VERSION
)));
}
}
Ok(())
} https://github.com/CosmWasm/cosmwasm/blob/main/contracts/ibc-reflect-send/src/ibc.rs#L20-L44 pub fn ibc_channel_open(
_deps: DepsMut,
_env: Env,
channel: IbcChannel,
) -> Result<(), ContractError> {
enforce_order_and_version(&channel)?;
Ok(())
}
fn enforce_order_and_version(channel: &IbcChannel) -> Result<(), ContractError> {
if channel.version != ICS20_VERSION {
return Err(ContractError::InvalidIbcVersion {
version: channel.version.clone(),
});
}
if let Some(version) = &channel.counterparty_version {
if version != ICS20_VERSION {
return Err(ContractError::InvalidIbcVersion {
version: version.clone(),
});
}
}
if channel.order != ICS20_ORDERING {
return Err(ContractError::OnlyOrderedChannel {});
}
Ok(())
} https://github.com/CosmWasm/cosmwasm-plus/blob/main/contracts/cw20-ics20/src/ibc.rs#L76-L83 While there is always an |
Also, my (4) is not just about I also want to mention that a large amount of code size and gas cost is JSON structs. And the struct here: #888 (comment) will lead to lots of (almost) duplicate generated code. |
Yeah, but there are ways to make this nicer: pub fn ibc_channel_open(deps: DepsMut, env: Env, msg: IbcChannelOpenMsg) -> StdResult<()> {
let (endpoint, version) = match msg {
IbcChannelOpenMsg::Init { channel } => {
(channel.endpoint, channel.version)
}
IbcChannelOpenMsg::Try { channel, .. } => {
(channel.endpoint, channel.version)
}
}
// real code starts here
} |
We decided NOT to worry about this issue and stick with the empty string in that one location. All the improvements mentioned here that we do want have been extracted to #1010 along with discussion how to acheive that |
Follow up from #884 (comment)
In
ChannelOpenInit
bothcounterparty_version
andcounterparty.channel_id
are unset. We handle the first with an Option, the second ends up as a blank string.counterparty_version
is only set inChannelOpenTry
andChannelOpenAck
callbacks, and unset everywhere else (including queries), so anOption
makes good sense there.counterparty.channel_id
is only unset inChannelOpenInit
(half of ouribc_connect_open
) and making it anOption
would complicate a lot. One idea was a special type used only foribc_connect_open
that hasOption
on thecounterparty.channel_id
. Another was custom entry points for the 4 phases in the handshake (which leads to more clarity, but a lot more code duplication and posibility of errors in contract code).Other ideas?
The text was updated successfully, but these errors were encountered: