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: support chain identifiers without {chain_name}-{revision_number} pattern #941

Merged
merged 42 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c8d3584
optional revision number for chain id
rnbguy Nov 1, 2023
f93ea96
add additional methods
rnbguy Nov 1, 2023
6a6fc4f
update tests
rnbguy Nov 1, 2023
674501f
add changelog entry
rnbguy Nov 1, 2023
4296b0e
new test for codecov
rnbguy Nov 1, 2023
03301c2
revert optional u64
rnbguy Nov 2, 2023
a068d57
rename method
rnbguy Nov 2, 2023
f0c8c21
update tests
rnbguy Nov 2, 2023
840fabd
visualize actual testcases
rnbguy Nov 2, 2023
60cf0cf
revert method rename
rnbguy Nov 2, 2023
6c89049
update changelog entry
rnbguy Nov 2, 2023
a3b6fd9
refactor ChainId::new
rnbguy Nov 3, 2023
b8534b7
rm ChainId::has_revision_number method
rnbguy Nov 3, 2023
51d5770
refactor ChainId::set_revision_number to ChainId::increment_revision_…
rnbguy Nov 3, 2023
63ac38f
rm old set unset revision_number test
rnbguy Nov 3, 2023
257152e
add tests for ChainId::increment_revision_number
rnbguy Nov 3, 2023
ad57856
add length validation tests
rnbguy Nov 3, 2023
ebe39ea
refactor source code for ChainId::new
rnbguy Nov 3, 2023
20302f0
fix doc test
rnbguy Nov 3, 2023
6cc3d28
fix chain identifier length validation
rnbguy Nov 3, 2023
23ca22e
update tests
rnbguy Nov 3, 2023
091f291
fix typo
rnbguy Nov 3, 2023
5782666
reword changelog entry
rnbguy Nov 3, 2023
22f5b8f
reword doc string
rnbguy Nov 3, 2023
02fb312
fix ChainId::validate_length
rnbguy Nov 3, 2023
a19ac45
add ChainId::validate_length in tests
rnbguy Nov 3, 2023
e8375b0
replace ID with identifier
rnbguy Nov 3, 2023
a4648d7
use validate_prefix_length over validate_identifier_length
rnbguy Nov 3, 2023
69301f8
optimize validate_prefix_length
rnbguy Nov 3, 2023
5302611
rename changelog entry
rnbguy Nov 3, 2023
1ee65f2
fix incorrect doc comment
rnbguy Nov 3, 2023
b53b5bd
use u64::checked_add
rnbguy Nov 4, 2023
784633b
update doc comment
rnbguy Nov 4, 2023
c7c6619
update doc tests
rnbguy Nov 4, 2023
a850f06
simplify validate_prefix_length
rnbguy Nov 4, 2023
6c7ae89
tests for validate_prefix_length
rnbguy Nov 4, 2023
8281ce8
fix tests
rnbguy Nov 4, 2023
198b991
add changelog entry
rnbguy Nov 4, 2023
c0577b7
import test_log::test
rnbguy Nov 6, 2023
ae09ee2
fix rstest test attribute
rnbguy Nov 6, 2023
0e41b74
cargo fmt
rnbguy Nov 6, 2023
94ede00
fix typo
rnbguy Nov 6, 2023
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 @@
- Support `ChainId`s without revision numbers
Farhad-Shabani marked this conversation as resolved.
Show resolved Hide resolved
([\#940](https://github.com/cosmos/ibc-rs/issues/940)).
166 changes: 121 additions & 45 deletions crates/ibc/src/core/ics24_host/identifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,22 @@ impl ChainId {
})
}

pub fn new_without_revision_number(name: &str) -> Result<Self, IdentifierError> {
let prefix = name.trim();
validate_identifier_chars(prefix)?;
validate_identifier_length(prefix, 1, 43)?;
Ok(Self {
id: prefix.to_owned(),
revision_number: 0,
})
}
Farhad-Shabani marked this conversation as resolved.
Show resolved Hide resolved

/// Get a reference to the underlying string.
pub fn as_str(&self) -> &str {
&self.id
}

pub fn split_chain_id(&self) -> (&str, u64) {
pub fn split_chain_id(&self) -> (&str, Option<u64>) {
parse_chain_id_string(self.as_str())
.expect("never fails because a valid chain identifier is parsed")
}
Expand All @@ -92,6 +102,11 @@ impl ChainId {
self.revision_number
}

/// Check if the revision number is set
pub fn has_revision_number(&self) -> bool {
self.split_chain_id().1.is_some()
}
Farhad-Shabani marked this conversation as resolved.
Show resolved Hide resolved

/// Swaps `ChainId`s revision number with the new specified revision number
rnbguy marked this conversation as resolved.
Show resolved Hide resolved
/// ```
/// use ibc::core::ics24_host::identifier::ChainId;
Expand All @@ -105,6 +120,21 @@ impl ChainId {
self.revision_number = revision_number;
}

/// Unsets the revision number of the `ChainId`
/// ```
/// use ibc::core::ics24_host::identifier::ChainId;
/// let mut chain_id = ChainId::new("chainA", 1).unwrap();
/// chain_id.unset_revision_number();
/// assert!(!chain_id.has_revision_number());
/// assert_eq!(chain_id.revision_number(), 0);
/// assert_eq!(chain_id.as_str(), "chainA");
/// ```
pub fn unset_revision_number(&mut self) {
let chain_name = self.chain_name();
self.id = chain_name.to_string();
self.revision_number = 0;
}
Farhad-Shabani marked this conversation as resolved.
Show resolved Hide resolved

/// A convenient method to check if the `ChainId` forms a valid identifier
/// with the desired min/max length. However, ICS-24 does not specify a
/// certain min or max lengths for chain identifiers.
Expand All @@ -122,7 +152,7 @@ impl FromStr for ChainId {
let (_, revision_number) = parse_chain_id_string(id)?;
Ok(Self {
id: id.to_string(),
revision_number,
revision_number: revision_number.unwrap_or(0),
})
}
}
Expand All @@ -134,36 +164,28 @@ impl Display for ChainId {
}

/// Parses a string intended to represent a `ChainId` and, if successful,
/// returns a tuple containing the chain name and revision number.
fn parse_chain_id_string(chain_id_str: &str) -> Result<(&str, u64), IdentifierError> {
let (name, rev_number_str) = match chain_id_str.rsplit_once('-') {
Some((name, rev_number_str)) => (name, rev_number_str),
None => {
return Err(IdentifierError::InvalidCharacter {
id: chain_id_str.to_string(),
})
}
};
/// returns a tuple containing the chain name and (optional) revision number.
rnbguy marked this conversation as resolved.
Show resolved Hide resolved
fn parse_chain_id_string(chain_id_str: &str) -> Result<(&str, Option<u64>), IdentifierError> {
Farhad-Shabani marked this conversation as resolved.
Show resolved Hide resolved
let (chain_name, revision_number) = chain_id_str
.rsplit_once('-')
.filter(|(_, rev_number_str)| {
// Validates the revision number not to start with leading zeros, like "01".
// Zero is the only allowed revision number with leading zero.
rev_number_str.as_bytes().first() != Some(&b'0') || rev_number_str.len() == 1
})
.and_then(|(chain_name, rev_number_str)| {
// Parses the revision number string into a `u64` and checks its validity.
rev_number_str
.parse()
.ok()
.map(|revision_number| (chain_name, Some(revision_number)))
})
.unwrap_or((chain_id_str, None));

// Validates the chain name for allowed characters according to ICS-24.
validate_identifier_chars(name)?;

// Validates the revision number not to start with leading zeros, like "01".
if rev_number_str.as_bytes().first() == Some(&b'0') && rev_number_str.len() > 1 {
return Err(IdentifierError::InvalidCharacter {
id: chain_id_str.to_string(),
});
}
validate_identifier_chars(chain_name)?;

// Parses the revision number string into a `u64` and checks its validity.
let revision_number =
rev_number_str
.parse()
.map_err(|_| IdentifierError::InvalidCharacter {
id: chain_id_str.to_string(),
})?;

Ok((name, revision_number))
Ok((chain_name, revision_number))
}

#[cfg_attr(
Expand Down Expand Up @@ -518,27 +540,81 @@ impl std::error::Error for IdentifierError {}

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

use super::*;

#[test]
fn test_valid_chain_id() {
assert!(ChainId::from_str("chainA-0").is_ok());
assert!(ChainId::from_str("chainA-1").is_ok());
assert!(ChainId::from_str("chainA--1").is_ok());
assert!(ChainId::from_str("chainA-1-2").is_ok());
#[rstest]
rnbguy marked this conversation as resolved.
Show resolved Hide resolved
#[case("chainA-0", "chainA", 0)]
#[case("chainA-1", "chainA", 1)]
#[case("chainA--1", "chainA-", 1)]
#[case("chainA-1-2", "chainA-1", 2)]
#[case("111-2", "111", 2)]
#[case("----1", "---", 1)]
#[case("._+-1", "._+", 1)]
fn test_valid_chain_id_with_rev(
#[case] raw_chain_id: &str,
#[case] chain_name: &str,
#[case] revision_number: u64,
) {
assert_eq!(
ChainId::from_str(raw_chain_id).unwrap(),
ChainId::new(chain_name, revision_number).unwrap()
);
}

#[rstest]
#[case("chainA")]
#[case("chainA.2")]
#[case("123")]
#[case("._+")]
#[case("chainA-")]
#[case("chainA-a")]
#[case("chainA-01")]
#[case("chainA-1-")]
fn test_valid_chain_id_without_rev(#[case] chain_name: &str) {
assert_eq!(
ChainId::from_str(chain_name).unwrap(),
ChainId::new_without_revision_number(chain_name).unwrap()
);
}

#[rstest]
#[case("-1")]
#[case(" ----1")]
#[case(" ")]
#[case(" chainA")]
#[case("chain A")]
#[case(" chainA.2")]
#[case(" chainA.2-1")]
#[case(" 1")]
#[case(" -")]
#[case(" -1")]
#[case("/chainA-1")]
fn test_invalid_chain_id(#[case] chain_id_str: &str) {
assert!(ChainId::from_str(chain_id_str).is_err());
}

#[test]
fn test_invalid_chain_id() {
assert!(ChainId::from_str("1").is_err());
assert!(ChainId::from_str("-1").is_err());
assert!(ChainId::from_str(" -1").is_err());
assert!(ChainId::from_str("chainA").is_err());
assert!(ChainId::from_str("chainA-").is_err());
assert!(ChainId::from_str("chainA-a").is_err());
assert!(ChainId::from_str("chainA-01").is_err());
assert!(ChainId::from_str("/chainA-1").is_err());
assert!(ChainId::from_str("chainA-1-").is_err());
fn test_opt_revision_number() {
let mut chain_id = ChainId::from_str("chainA-1").unwrap();
assert!(chain_id.has_revision_number());
assert_eq!(chain_id.revision_number(), 1);
assert_eq!(chain_id.as_str(), "chainA-1");

chain_id.unset_revision_number();
assert!(!chain_id.has_revision_number());
assert_eq!(chain_id.revision_number(), 0);
assert_eq!(chain_id.as_str(), "chainA");

chain_id.set_revision_number(2);
assert!(chain_id.has_revision_number());
assert_eq!(chain_id.revision_number(), 2);
assert_eq!(chain_id.as_str(), "chainA-2");

chain_id.set_revision_number(0);
assert!(chain_id.has_revision_number());
assert_eq!(chain_id.revision_number(), 0);
assert_eq!(chain_id.as_str(), "chainA-0");
}
}
Loading