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(types): glue code between domain types and their corresponding protobuf types #1070

Open
rnbguy opened this issue Jan 31, 2024 · 5 comments
Labels
O: usability Objective: aims to enhance user experience (UX) and streamline product usability

Comments

@rnbguy
Copy link
Collaborator

rnbguy commented Jan 31, 2024

Feature Summary

We introduced ToProto trait in #995. It did streamline a lot of code, but we can streamline even more.

Proposal

  1. Extend the ToProto trait with try_from_any() method and replace the generic with an associated type.
trait ToProto {
    type ProtoType;
    type Error;

    fn type_url(&self) -> String;

    fn to_any(&self) -> Any;

    fn try_from_any(any: Any) -> Result<Self, Self::Error>;
}

By removing the generic P from ToProto, we can access the corresponding protobuf type of DomainType at <DomainType as ToProto>::ProtoType.

Side-note: we may want to rename ToProto to something like ProtoLike.

  1. For a DomainType with a corresponding ProtoType, we/users only maintain the following code. Note the derive-macro ToProto(ProtoType) for DomainType.
#[derive(ToProto(ProtoType))]
struct DomainType {
    hello: String,
}

impl TryFrom<Self::ProtoType> for DomainType {
    type Error = Error;

    fn try_from(proto: Self::ProtoType) -> Result<Self, Self::Error> {
        Ok(Self { hello: proto.hello })
    }
}

impl Into<Self::ProtoType> for DomainType {
    fn into(self) -> Self::ProtoType {
        Self::ProtoType {
            hello: self.hello,
        }
    }
}
  1. The derive-macro will generate the following code block for the above example.
impl Protobuf<ProtoType> for DomainType {}

impl ToProto for DomainType {
    type ProtoType = ProtoType;
    type Error = Error;

    fn type_url(&self) -> String {
        ProtoType::type_url()
    }

    fn to_any(self) -> Any {
        Any {
            type_url: ProtoType::type_url(),
            value: ProtoType::from(self).encode_to_vec(),
        }   
    }

    fn try_from_any(any: Any) -> Result<Self, Self::Error> {
        if &any.type_url == ProtoType::type_url() {
            ProtoType::decode(any.value.as_slice()).and_then(Self::try_from)
        } else {
            Err(DecodeError::new("Invalid type_url"))
        }
    }
}

impl Protobuf<Any> for DomainType {}

impl TryFrom<Any> for DomainType {
    type Error = Error;

    fn try_from(any: Any) -> Result<Self, Self::Error> {
        let proto = ProtoType::try_from_any(any)?;
        Self::try_from(proto)
    }
}

impl From<DomainType> for Any {
    fn from(domain: DomainType) -> Self {
        let proto = ProtoType::from(domain);
        proto.to_any()
    }
}

With this, we can simplify the implementation of most of the current domain types. For example, the following codes can be removed from the Tendermint client state domain type.

impl Protobuf<RawTmClientState> for ClientState {}

impl Protobuf<Any> for ClientState {}
impl TryFrom<Any> for ClientState {
type Error = ClientError;
fn try_from(raw: Any) -> Result<Self, Self::Error> {
Ok(Self(ClientStateType::try_from(raw)?))
}
}
impl From<ClientState> for Any {
fn from(client_state: ClientState) -> Self {
client_state.0.into()
}
}

@rnbguy
Copy link
Collaborator Author

rnbguy commented Jan 31, 2024

This raises a question to me, do we really need to hardcode constants such as this,

pub const TENDERMINT_CLIENT_STATE_TYPE_URL: &str = "/ibc.lightclients.tendermint.v1.ClientState";

as they can be retrieved by ToProto::type_url() even in current codebase. For example, the above type URL is present as

<TmClientState as ToProto<RawTmClientState>>::type_url()

If we want to make sure that these types of URLs are the ones we expect, we can add unit tests.

@rnbguy
Copy link
Collaborator Author

rnbguy commented Jan 31, 2024

Actually, we don't need

impl Protobuf<Any> for DomainType {}

The compilation error source, after removing these

<Self as Protobuf<Any>>::encode_vec(self)

<Self as Protobuf<Any>>::encode_vec(self)

can be refactored as

use ibc::primitives::ToVec;
Any::from(self).to_vec()

@rnbguy rnbguy changed the title imp(types): code maintenance of domain types and their corresponding protobuf types imp(types): glue code between domain types and their corresponding protobuf types Jan 31, 2024
@Farhad-Shabani Farhad-Shabani added the O: usability Objective: aims to enhance user experience (UX) and streamline product usability label Feb 1, 2024
@mzabaluev
Copy link
Contributor

While the usability with the associated type is better, we do have a use for the parameter on the Protobuf trait: a single domain type can be used to encode to/decode from different versions of protobuf-generated types. Currently it's useful in supporting the CometBFT protocol versions used by 0.34.x, 0.37.x, and 0.38.x nodes, but also in post-v1 future, some messages might evolve to have different versions, and those will be published side by side to .v1 in .v2 packages and so on. The intent is to not proliferate versioned domain types unless there is a clear need for that, instead the versioning complexity is represented at the cometbft-proto level.

@rnbguy
Copy link
Collaborator Author

rnbguy commented Feb 26, 2024

It'd be good if we have a trait to enforce a validate method for these domain types - as we don't perform data validation for the protobuf types.

trait ProtoValidate {
  fn validate(&self) -> bool;
}

trait ToProto: ProtoValidate { ... }

@rnbguy
Copy link
Collaborator Author

rnbguy commented Apr 17, 2024

I recently learned about associated consts for traits and structs. So ToProto trait may look like this

trait ToProto {
    type ProtoType;
    type Error;
    const TYPE_URL: &'static str;

    fn to_any(&self) -> Any;
    fn try_from_any(any: Any) -> Result<Self, Self::Error>;
}

So we can rewrite the Any conversion as following:

    fn to_any(self) -> Any {
        Any {
            type_url: Self::TYPE_URL.to_owned(),
            value: ProtoType::from(self).encode_to_vec(),
        }   
    }

    fn try_from_any(any: Any) -> Result<Self, Self::Error> {
        match any.type_url.as_str() {
            Self::TYPE_URL => ProtoType::decode(any.value.as_slice()).and_then(Self::try_from),
            _ => Err(DecodeError::new("Invalid type_url"))
        }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
O: usability Objective: aims to enhance user experience (UX) and streamline product usability
Projects
Status: 📥 To Do
Development

No branches or pull requests

3 participants