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

imp: Implement custom JSON and Borsh deserialization for ChainId #1013

Merged
merged 27 commits into from
Dec 21, 2023

Conversation

seanchen1991
Copy link
Contributor

Closes: #996

Description


PR author checklist:

  • Added changelog entry, using unclog.
  • Added tests.
  • Linked to GitHub issue.
  • Updated code comments and documentation (e.g., docs/).
  • Tagged one reviewer who will be the one responsible for shepherding this PR.

Reviewer checklist:

  • Reviewed Files changed in the GitHub PR explorer.
  • Manually tested (in case integration/unit/mock tests are absent).

@seanchen1991 seanchen1991 changed the title Implement custom JSON and Borsh deserialization for ChainId imp: Implement custom JSON and Borsh deserialization for ChainId Dec 20, 2023
Copy link

codecov bot commented Dec 20, 2023

Codecov Report

Attention: 8 lines in your changes are missing coverage. Please review.

Comparison is base (d2a91ea) 70.71% compared to head (7fd8827) 70.90%.

Files Patch % Lines
...-core/ics24-host/types/src/identifiers/chain_id.rs 94.11% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1013      +/-   ##
==========================================
+ Coverage   70.71%   70.90%   +0.18%     
==========================================
  Files         178      178              
  Lines       17861    17995     +134     
==========================================
+ Hits        12631    12759     +128     
- Misses       5230     5236       +6     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

ibc-core/ics24-host/types/src/identifiers/chain_id.rs Outdated Show resolved Hide resolved
ibc-core/ics24-host/types/src/identifiers/chain_id.rs Outdated Show resolved Hide resolved
ibc-core/ics24-host/types/src/identifiers/chain_id.rs Outdated Show resolved Hide resolved
Comment on lines 266 to 268
let Ok((_, rn)) = parse_chain_id_string(&inner.id) else {
return Err(Error::new(ErrorKind::Other, "failed to parse chain ID"));
};
Copy link
Member

Choose a reason for hiding this comment

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

Despite not being parsed, a chain id string is not necessarily invalid. The #[case(r#"{"id":"foo","revision_number":"0"}"#)] should be successfully deserialized.
Could you add tests for valid borsh desrialization cases as well?

Copy link
Contributor

Choose a reason for hiding this comment

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

What about {"id":"foo-bar-42","revision_number":"0"}? The condition below accepts it but my intuition is that it should be rejected as well. IMO deserialised object should be the same as one constructed via ChainId::from_str(&deserialised.id).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's possible to have ChainIds that don't strictly follow the format, but are still considered valid. {"id":"foo-bar-42","revision_number":"0"} is an example of a ChainId that is valid but that doesn't strictly follow the format.

ibc-core/ics24-host/types/src/identifiers/chain_id.rs Outdated Show resolved Hide resolved
ibc-core/ics24-host/types/expanded.rs Outdated Show resolved Hide resolved
where
D: Deserializer<'de>,
{
const FIELDS: &[&str] = &["id", "revision_number"];
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not something like:

#[derive(serde::Deserialize)]
struct IdAndRevision {
    id: String,
    revision_number: u64,
}

let IdAndRevision { id, revision_number } =
    IdAndRevision::deserialize(D)?;
if revision_number == parse_revision_number(chain_id.as_str())? {
    Ok(Self { id, revision_number })
} else {
    Err(...)
}

And add fn parse_revision_number(id: &str) -> Result<u64, IdentifierError>; function

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not really clear to me which part of the serde::Deserialize impl you're thinking your suggested code should replace.

Copy link
Contributor

@mina86 mina86 Dec 21, 2023

Choose a reason for hiding this comment

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

What I’ve posted would be body of the deserialize method, i.e.:

#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for ChainId {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        struct IdAndRevision {
            id: String,
            revision_number: u64,
        }

        let IdAndRevision { id, revision_number } =
            IdAndRevision::deserialize(deserializer)?;
        if revision_number == 0 {
            // If `revision_number` is zero we allow whatever valid identifier.
            // In particular, `{id: "foo-bar-42", revision_number: 0}` is valid.
            validate_chain_id(id.as_str())?;
        } else if revision_number != parse_revision_number(id.as_str())? {
            return Err(...);
        }
        Ok(Self { id, revision_number })
    }
}

Copy link
Member

Choose a reason for hiding this comment

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

Still IdAndRevision::deserialize(deserializer)? is not working properly for all cases. Anyhow let’s leave further optimization for later PRs.

Copy link
Contributor

Choose a reason for hiding this comment

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

What’s the issue? IdAndRevision with a derive should have the exact same deserialisation logic as ChainId.

ibc-core/ics24-host/types/src/identifiers/chain_id.rs Outdated Show resolved Hide resolved
Comment on lines 266 to 268
let Ok((_, rn)) = parse_chain_id_string(&inner.id) else {
return Err(Error::new(ErrorKind::Other, "failed to parse chain ID"));
};
Copy link
Contributor

Choose a reason for hiding this comment

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

What about {"id":"foo-bar-42","revision_number":"0"}? The condition below accepts it but my intuition is that it should be rejected as well. IMO deserialised object should be the same as one constructed via ChainId::from_str(&deserialised.id).

Copy link
Member

@Farhad-Shabani Farhad-Shabani left a comment

Choose a reason for hiding this comment

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

Thanks @seanchen1991 for sorting this out, and a nod to @mina86 for chiming in!

@Farhad-Shabani Farhad-Shabani merged commit 7ff743b into main Dec 21, 2023
14 checks passed
@Farhad-Shabani Farhad-Shabani deleted the sean/verify-chain-id-de branch December 21, 2023 21:50
Farhad-Shabani added a commit that referenced this pull request Sep 9, 2024
…1013)

* Add ChainId json deserialize test

* Add BorshDeserialization test

* Add some debugging to ChainId deserialize impl

* Change assertion to unwrap

* First stab at custom Visitor and Deserialize impls

* Implement custom Deserialize for ChainId

* Remove unnecessary ChainId::from_str call

* Add some additional assertions to test valid ChainIds

* Stub out custom BorshDeserialize impl

* Get custom borshDeserialize impl compiling

* Add rstest test case testing invalid borsh deserialization

* Add changelog entry

* Cargo fmt

* Incorporate some PR feedback

* Remove expanded.rs file

* Remove expanded.rs file

* Clean up borshDeserialize impl

* Add test the verifies valid borsh deserialization

* Update BorshDeserialize test

* test: add test_valid_borsh_ser_de_roundtrip

* Clean up

* nit

* Move `use core::fmt` statement into serde::Deserialize impl

* fix: core::fmt::Result

---------

Co-authored-by: Farhad Shabani <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

Code accepts inconsistent serialised ChainId objects
3 participants