Skip to content
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

fix(ics20): PrefixedDenom parsing #1178

Merged
merged 52 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
c81f2da
add failing test
rnbguy Apr 19, 2024
6c0a324
add TracePath::new
rnbguy Apr 19, 2024
ad096f5
imp PrefixedDenom parsing
rnbguy Apr 19, 2024
08710f6
add validate_named_u64_index
rnbguy Apr 19, 2024
9d2a9ea
validate_named_u64_index in channel and connection validation
rnbguy Apr 19, 2024
eba0690
add tests for validate_named_u64_index
rnbguy Apr 19, 2024
dfac6b5
update PrefixDenom parsing tests
rnbguy Apr 19, 2024
be171a1
update accepted cases in ibc-go
rnbguy Apr 19, 2024
e59060e
update accepted cases
rnbguy Apr 19, 2024
04b40dc
add test cases from ibc-go
rnbguy Apr 19, 2024
a9b8d8b
use valid connection id
rnbguy Apr 19, 2024
6542a23
failing doc tests
rnbguy Apr 19, 2024
3337f65
use existing constants
rnbguy Apr 19, 2024
a88e1d2
changelog entry
rnbguy Apr 19, 2024
dd021d9
refactor tests
rnbguy Apr 19, 2024
4a7c034
apply suggestions from code review
rnbguy Apr 20, 2024
9d246d4
add comment for PrefixedDenom::from_str
rnbguy Apr 20, 2024
be0f693
rm TracePath::new
rnbguy Apr 20, 2024
02d57c8
collect the same tests
rnbguy Apr 20, 2024
b1804b0
test parsed PrefixedDenom string repr
rnbguy Apr 20, 2024
baedfb7
test trace order
rnbguy Apr 20, 2024
32f5407
more tests
rnbguy Apr 20, 2024
e3c109b
update tests
rnbguy Apr 20, 2024
dfe5a7c
rm redundant error variant
rnbguy Apr 20, 2024
5dd5559
refactor tests
rnbguy Apr 20, 2024
5d04ebf
update doc string
rnbguy Apr 20, 2024
50ebb2f
add comment on virtual split_twice
rnbguy Apr 20, 2024
063c1df
update doc str
rnbguy Apr 20, 2024
f72517f
add TracePrefix::strip and TracePath::trim
rnbguy Apr 22, 2024
25111df
add whitespace cases
rnbguy Apr 22, 2024
76cf402
refactor tests
rnbguy Apr 22, 2024
1524a50
spelling
rnbguy Apr 22, 2024
8720be0
parse over from_str
rnbguy Apr 22, 2024
93a81d5
nit
rnbguy Apr 22, 2024
2abbeab
Merge branch 'main' into rano/fix/parsing-prefixed-denom
seanchen1991 Apr 22, 2024
94ac941
apply suggestions from code review
rnbguy Apr 22, 2024
7408994
add failing test
rnbguy Apr 24, 2024
d34d946
fix TracePath::from_str
rnbguy Apr 24, 2024
03b08d7
fix parsing
rnbguy Apr 24, 2024
29be17b
fix test
rnbguy Apr 24, 2024
2b11733
add ibc-app-transfer-types dep
rnbguy Apr 24, 2024
1eecc2b
rm reusable code
rnbguy Apr 24, 2024
6ff2abf
update impls
rnbguy Apr 24, 2024
bd7922e
update tests
rnbguy Apr 24, 2024
736d4a1
accepted class ids
rnbguy Apr 24, 2024
badb018
refactor tests
rnbguy Apr 24, 2024
eb9456f
update changelog entry
rnbguy Apr 24, 2024
43e46ad
opt trace prefix and path parsing
rnbguy Apr 25, 2024
a784640
add new tests
rnbguy Apr 25, 2024
7f465b0
Update changelog entry
seanchen1991 Apr 26, 2024
6f3eddb
Merge branch 'main' into rano/fix/parsing-prefixed-denom
seanchen1991 Apr 26, 2024
53fad12
fix trace-prefix index order
rnbguy Apr 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- [ibc-apps-transfer] Compatible `PrefixedDenom` parsing with `ibc-go`.
rnbguy marked this conversation as resolved.
Show resolved Hide resolved
([\#1177](https://github.com/cosmos/ibc-rs/issues/1177))
141 changes: 112 additions & 29 deletions ibc-apps/ics20-transfer/types/src/denom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ impl Display for TracePrefix {
pub struct TracePath(Vec<TracePrefix>);

impl TracePath {
/// Creates a new trace path from a vector of trace prefixes.
/// Reverse the order of the prefixes for easier addition/removal from the end.
pub fn new(mut trace: Vec<TracePrefix>) -> Self {
trace.reverse();
Self(trace)
}
Farhad-Shabani marked this conversation as resolved.
Show resolved Hide resolved

/// Returns true iff this path starts with the specified prefix
pub fn starts_with(&self, prefix: &TracePrefix) -> bool {
self.0.last().map(|p| p == prefix).unwrap_or(false)
Expand Down Expand Up @@ -294,18 +301,34 @@ impl FromStr for PrefixedDenom {
type Err = TokenTransferError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be helpful to add a description of the logic of this from_str implementation, as it's a little bit convoluted. Here's a suggestion:

/// Initializes a `PrefixedDenom` from a string that adheres to the format
/// `{port-id-1/channel-id-1}/{port-id-2/channel-id-2}/.../{port-id-n/channel-id-n}/base-denom`.
/// A `PrefixedDenom` exhibits between 1..n number of `{port-id/channel-id}` pairs.
/// The set of these pairs makes up the `TracePath`.
///
/// This `from_str` implementation peeks the first two segments split on the `/`
/// delimiter and attempts to convert the first segment into a `PortId` and the 
/// second segment into a `ChannelId`. This continues in a loop until a 
/// `{port-id/channel-id}` pair cannot be created from the top two segments. 
/// The remaining parts of the string are then considered the `BaseDenom`.
///
/// For example, given the following denom trace:
/// "transfer/channel-75/factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/dust",
/// the first two `/`-delimited segments are `"transfer"` and `"channel-75"`. The
/// first is a valid `PortId` and the second is a valid `ChannelId`, so that becomes
/// the first `{port-id/channel-id}` pair that gets added as part of the `TracePath`
/// of the `PrefixedDenom`. The next two segments are `"factory"`, which is not a
/// valid `PortId`, and `"stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4"`, which is
/// not a valid `ChannelId`. The loop breaks at this point, resulting in a
/// `TracePath` of `"transfer/channel-75"` and a `BaseDenom` of
/// `"factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/dust"`.

Feel free to edit the above to make the description more clear.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Sean. The above description helps a lot. Great.

The next two segments are "factory", which is not a valid PortId

Just a small note: "factory" is a valid PortId.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let mut parts: Vec<&str> = s.split('/').collect();
let last_part = parts.pop().expect("split() returned an empty iterator");

let (base_denom, trace_path) = {
if last_part == s {
(BaseDenom::from_str(s)?, TracePath::empty())
} else {
let base_denom = BaseDenom::from_str(last_part)?;
let trace_path = TracePath::try_from(parts)?;
(base_denom, trace_path)
let mut trace_prefixes = vec![];

let mut remaining_parts = s;

loop {
let parsed_prefix = remaining_parts
.split_once('/')
.and_then(|(port_id_s, remaining)| {
remaining
.split_once('/')
.map(|(channel_id_s, remaining)| (port_id_s, channel_id_s, remaining))
})
.and_then(|(port_id_s, channel_id_s, remaining)| {
let port_id = PortId::from_str(port_id_s).ok()?;
let channel_id = ChannelId::from_str(channel_id_s).ok()?;
Some((port_id, channel_id, remaining))
});
match parsed_prefix {
Some((port_id, channel_id, remaining)) => {
trace_prefixes.push(TracePrefix::new(port_id, channel_id));
remaining_parts = remaining;
}
None => break,
}
};
}

let trace_path = TracePath::new(trace_prefixes);
let base_denom = BaseDenom::from_str(remaining_parts)?;

Ok(Self {
trace_path,
Expand Down Expand Up @@ -357,6 +380,8 @@ impl Display for PrefixedDenom {

#[cfg(test)]
mod tests {
use rstest::rstest;

use super::*;

#[test]
Expand All @@ -368,16 +393,6 @@ mod tests {
PrefixedDenom::from_str("transfer/channel-0/").is_err(),
"empty base denom with trace"
);
assert!(PrefixedDenom::from_str("/uatom").is_err(), "empty prefix");
assert!(PrefixedDenom::from_str("//uatom").is_err(), "empty ids");
assert!(
PrefixedDenom::from_str("transfer/").is_err(),
"single trace"
);
assert!(
PrefixedDenom::from_str("transfer/atom").is_err(),
"single trace with base denom"
);
assert!(
PrefixedDenom::from_str("transfer/channel-0/uatom").is_ok(),
"valid single trace info"
Expand All @@ -386,18 +401,86 @@ mod tests {
PrefixedDenom::from_str("transfer/channel-0/transfer/channel-1/uatom").is_ok(),
"valid multiple trace info"
);
assert!(
PrefixedDenom::from_str("(transfer)/channel-0/uatom").is_err(),
"invalid port"
);
assert!(
PrefixedDenom::from_str("transfer/(channel-0)/uatom").is_err(),
"invalid channel"
);

// the followings are valid denom according to `ibc-go`
// https://github.com/cosmos/ibc-go/blob/e2ad31975f2ede592912b86346b5ebf055c9e05f/modules/apps/transfer/types/trace_test.go#L17-L38
PrefixedDenom::from_str("/uatom").expect("no error");
PrefixedDenom::from_str("//uatom").expect("no error");
PrefixedDenom::from_str("transfer/").expect("no error");
PrefixedDenom::from_str("transfer/atom").expect("no error");
PrefixedDenom::from_str("(transfer)/channel-0/uatom").expect("no error");
PrefixedDenom::from_str("transfer/(channel-0)/uatom").expect("no error");
rnbguy marked this conversation as resolved.
Show resolved Hide resolved

Ok(())
}

#[rstest]
#[case(
"transfer/channel-75",
"factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/dust"
)]
#[case(
"transfer/channel-75/transfer/channel-123/transfer/channel-1023/transfer/channel-0",
"factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/dust"
)]
#[case(
"transfer/channel-75/transfer/channel-123/transfer/channel-1023/transfer/channel-0",
"//////////////////////dust"
)]
// https://github.com/cosmos/ibc-go/blob/e2ad31975f2ede592912b86346b5ebf055c9e05f/modules/apps/transfer/types/trace_test.go#L17-L38
#[case("", "uatom")]
#[case("", "uatom/")]
#[case("", "gamm/pool/1")]
#[case("", "gamm//pool//1")]
#[case("transfer/channel-1", "uatom")]
#[case("customtransfer/channel-1", "uatom")]
#[case("transfer/channel-1", "uatom/")]
#[case(
"transfer/channel-1",
"erc20/0x85bcBCd7e79Ec36f4fBBDc54F90C643d921151AA"
)]
#[case("transfer/channel-1", "gamm/pool/1")]
#[case("transfer/channel-1", "gamm//pool//1")]
#[case("transfer/channel-1/transfer/channel-2", "uatom")]
#[case("customtransfer/channel-1/alternativetransfer/channel-2", "uatom")]
Farhad-Shabani marked this conversation as resolved.
Show resolved Hide resolved
#[case("", "transfer/uatom")]
#[case("", "transfer//uatom")]
#[case("", "channel-1/transfer/uatom")]
#[case("", "uatom/transfer")]
#[case("", "transfer/channel-1")]
#[case("transfer/channel-1", "transfer")]
#[case("", "transfer/channelToA/uatom")]
fn test_strange_but_accepted_prefixed_denom(
#[case] prefix: &str,
#[case] denom: &str,
) -> Result<(), TokenTransferError> {
let pd_s = if prefix.is_empty() {
denom.to_owned()
} else {
format!("{prefix}/{denom}")
};
let pd = PrefixedDenom::from_str(&pd_s)?;

assert_eq!(pd.trace_path.to_string(), prefix);
assert_eq!(pd.base_denom.to_string(), denom);

Ok(())
}

#[rstest]
#[case("")]
#[case("transfer/channel-1")]
#[case("transfer/channel-1/transfer/channel-2")]
#[should_panic(expected = "EmptyBaseDenom")]
fn test_prefixed_empty_base_denom(#[case] prefix: &str) {
let pd_s = if prefix.is_empty() {
"".to_owned()
} else {
format!("{prefix}/")
};
PrefixedDenom::from_str(&pd_s).expect("error");
}

#[test]
fn test_denom_trace() -> Result<(), TokenTransferError> {
Farhad-Shabani marked this conversation as resolved.
Show resolved Hide resolved
assert_eq!(
Expand Down
2 changes: 2 additions & 0 deletions ibc-core/ics24-host/types/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub enum IdentifierError {
RevisionNumberOverflow,
/// String `{value}` cannot be converted to packet sequence, error: `{reason}`
InvalidStringAsSequence { value: String, reason: String },
/// String `{id}` does not start with `{name}`
InvalidNamedIndex { id: String, name: String },
Farhad-Shabani marked this conversation as resolved.
Show resolved Hide resolved
}

#[cfg(feature = "std")]
Expand Down
4 changes: 2 additions & 2 deletions ibc-core/ics24-host/types/src/identifiers/channel_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ impl AsRef<str> for ChannelId {
/// ```
/// use core::str::FromStr;
/// use ibc_core_host_types::identifiers::ChannelId;
/// let channel_id = ChannelId::from_str("channelId-0");
/// let channel_id = ChannelId::from_str("channel-0");
/// assert!(channel_id.is_ok());
/// channel_id.map(|id| {assert_eq!(&id, "channelId-0")});
/// channel_id.map(|id| {assert_eq!(&id, "channel-0")});
/// ```
impl PartialEq<str> for ChannelId {
fn eq(&self, other: &str) -> bool {
Expand Down
4 changes: 2 additions & 2 deletions ibc-core/ics24-host/types/src/identifiers/connection_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ impl FromStr for ConnectionId {
/// ```
/// use core::str::FromStr;
/// use ibc_core_host_types::identifiers::ConnectionId;
/// let conn_id = ConnectionId::from_str("connectionId-0");
/// let conn_id = ConnectionId::from_str("connection-0");
/// assert!(conn_id.is_ok());
/// conn_id.map(|id| {assert_eq!(&id, "connectionId-0")});
/// conn_id.map(|id| {assert_eq!(&id, "connection-0")});
/// ```
impl PartialEq<str> for ConnectionId {
fn eq(&self, other: &str) -> bool {
Expand Down
91 changes: 89 additions & 2 deletions ibc-core/ics24-host/types/src/validate.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use ibc_primitives::prelude::*;

use crate::error::IdentifierError as Error;
use crate::identifiers::{ChannelId, ConnectionId};

const VALID_SPECIAL_CHARS: &str = "._+-#[]<>";

Expand Down Expand Up @@ -61,6 +62,38 @@ pub fn validate_prefix_length(
validate_identifier_length(prefix, min, max)
}

/// Checks if the identifier is a valid named u64 index: {name}-{u64}.
/// Example: "connection-0", "connection-100", "channel-0", "channel-100".
pub fn validate_named_u64_index(id: &str, name: &str) -> Result<(), Error> {
let number_s = id
.strip_prefix(name)
.ok_or_else(|| Error::InvalidNamedIndex {
id: id.into(),
name: name.into(),
})?
.strip_prefix('-')
.ok_or_else(|| Error::InvalidNamedIndex {
id: id.into(),
name: name.into(),
})?;

if number_s.starts_with('0') && number_s.len() > 1 {
return Err(Error::InvalidNamedIndex {
id: id.into(),
name: name.into(),
});
}

_ = number_s
.parse::<u64>()
.map_err(|_| Error::InvalidNamedIndex {
id: id.into(),
name: name.into(),
})?;

Ok(())
}

/// Default validator function for the Client types.
pub fn validate_client_type(id: &str) -> Result<(), Error> {
validate_identifier_chars(id)?;
Expand All @@ -82,7 +115,9 @@ pub fn validate_client_identifier(id: &str) -> Result<(), Error> {
/// in the ICS-24 spec.
pub fn validate_connection_identifier(id: &str) -> Result<(), Error> {
validate_identifier_chars(id)?;
validate_identifier_length(id, 10, 64)
validate_identifier_length(id, 10, 64)?;
validate_named_u64_index(id, ConnectionId::prefix())?;
Ok(())
}

/// Default validator function for Port identifiers.
Expand All @@ -100,7 +135,9 @@ pub fn validate_port_identifier(id: &str) -> Result<(), Error> {
/// the ICS-24 spec.
pub fn validate_channel_identifier(id: &str) -> Result<(), Error> {
validate_identifier_chars(id)?;
validate_identifier_length(id, 8, 64)
validate_identifier_length(id, 8, 64)?;
validate_named_u64_index(id, ChannelId::prefix())?;
Ok(())
}

#[cfg(test)]
Expand Down Expand Up @@ -141,6 +178,25 @@ mod tests {
assert!(id.is_err())
}

#[test]
fn parse_invalid_connection_id_indexed() {
// valid connection id with index
validate_connection_identifier("connection-0").expect("no error");
validate_connection_identifier("connection-123").expect("no error");
validate_connection_identifier("connection-18446744073709551615").expect("no error");
}

#[test]
fn parse_invalid_connection_id_non_idexed() {
rnbguy marked this conversation as resolved.
Show resolved Hide resolved
// invalid indexing for connection id
validate_connection_identifier("connection-0123").expect_err("InvalidNamedIndex");
validate_connection_identifier("connection0123").expect_err("InvalidNamedIndex");
validate_connection_identifier("connection000").expect_err("InvalidNamedIndex");
// 1 << 64 = 18446744073709551616
validate_connection_identifier("connection-18446744073709551616")
.expect_err("InvalidNamedIndex");
}

#[test]
fn parse_invalid_channel_id_min() {
// invalid channel id, must be at least 8 characters
Expand All @@ -157,6 +213,24 @@ mod tests {
assert!(id.is_err())
}

#[test]
fn parse_invalid_channel_id_indexed() {
// valid channel id with index
validate_channel_identifier("channel-0").expect("no error");
validate_channel_identifier("channel-123").expect("no error");
validate_channel_identifier("channel-18446744073709551615").expect("no error");
}

#[test]
fn parse_invalid_channel_id_non_idexed() {
rnbguy marked this conversation as resolved.
Show resolved Hide resolved
// invalid indexing for channel id
validate_channel_identifier("channel-0123").expect_err("InvalidNamedIndex");
validate_channel_identifier("channel0123").expect_err("InvalidNamedIndex");
validate_channel_identifier("channel000").expect_err("InvalidNamedIndex");
// 1 << 64 = 18446744073709551616
validate_channel_identifier("channel-18446744073709551616").expect_err("InvalidNamedIndex");
}

#[test]
fn parse_invalid_client_id_min() {
// invalid min client id
Expand Down Expand Up @@ -242,4 +316,17 @@ mod tests {
let result = validate_prefix_length(prefix, min, max);
assert_eq!(result.is_ok(), success);
}

#[rstest]
#[case::zero_padded("channel", "001", false)]
#[case::only_zero("connection", "000", false)]
#[case::zero("channel", "0", true)]
#[case::one("connection", "1", true)]
#[case::n1234("channel", "1234", true)]
#[case::u64_max("chan", "18446744073709551615", true)]
#[case::u64_max_plus_1("chan", "18446744073709551616", false)]
fn test_named_index_validation(#[case] name: &str, #[case] id: &str, #[case] success: bool) {
let result = validate_named_u64_index(format!("{name}-{id}").as_str(), name);
assert_eq!(result.is_ok(), success, "{result:?}");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub fn dummy_conn_open_confirm() -> MsgConnectionOpenConfirm {
/// Returns a dummy `RawMsgConnectionOpenConfirm` for testing purposes only!
pub fn dummy_raw_msg_conn_open_confirm() -> RawMsgConnectionOpenConfirm {
RawMsgConnectionOpenConfirm {
connection_id: "srcconnection".to_string(),
connection_id: "connection-118".to_string(),
proof_ack: dummy_proof(),
proof_height: Some(Height {
revision_number: 0,
Expand Down
Loading