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

ADR 11: Light client extraction #2356

Closed
wants to merge 27 commits into from

Conversation

hu55a1n1
Copy link
Member

@hu55a1n1 hu55a1n1 commented Jun 29, 2022

Description

An ADR for light client extraction by removal of Any* enums and replacing them with trait objects.

Rendered


PR author checklist:

  • Added changelog entry, using unclog.
  • Added tests: integration (for Hermes) or unit/mock tests (for modules).
  • Linked to GitHub issue.
  • Updated code comments and documentation (e.g., docs/).

Reviewer checklist:

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

Copy link
Contributor

@seanchen1991 seanchen1991 left a comment

Choose a reason for hiding this comment

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

Left some nits.

@hu55a1n1 hu55a1n1 marked this pull request as ready for review July 1, 2022 09:19
@hu55a1n1 hu55a1n1 requested a review from adizere as a code owner July 1, 2022 09:19
Furthermore, protobuf has emerged as the canonical serialization scheme for IBC, and IBC's message definitions usually
serialize light client types using the `google::protobuf::Any` type where the `type_url` is accepted to uniquely
represent specific light client types, although this has not been standardized yet.
It is proposed to standardize this and provide a `ibc-client-registry` crate which standardizes this and collects all
Copy link
Member Author

Choose a reason for hiding this comment

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

A cool crate that might be helpful in implementing such a registry -> https://github.com/dtolnay/inventory. (Thanks to @romac for suggesting it).

```

This can be solved using the `ibc_proto::google::protobuf::Any` type instead and having the light client traits provide
an encoding to `Any` ->
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of using Option<Any> for header, why not implement Deserialize ourselves, and apply the transformation encode_any() in deserialize()?

serde might also even provide an attribute that we could tag header with that specifies to apply this transformation before deserializing. Not sure though.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think the problem with that approach is that the deserialize impl would depend on Header::decode() which must have a Self: Sized requirement. This is because we must be aware of the concrete type of the header at the time of deserialization but that's not possible.

Copy link
Member Author

Choose a reason for hiding this comment

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

Seems like there are some ready-made solutions like erased_serde but I am not sure if they can work for our specific use case.

Copy link
Member Author

@hu55a1n1 hu55a1n1 Jul 6, 2022

Choose a reason for hiding this comment

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

Here's my quick attempt at implementing this using the #[serde(with = "/* ... */")] attribute -> https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=de9aa123fcec49b410fec8bb23d5f53e

Copy link
Contributor

Choose a reason for hiding this comment

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

This is because we must be aware of the concrete type of the header at the time of deserialization but that's not possible.

This is a good point. To address this issue, we could modify the IntoRef trait suggested here to inject some context into the conversion:

trait IntoRef<T: ?Sized> { 
    // Same applies to `try_into_ref()`
    fn into_ref(&self, ctx: &ConversionContext) -> Box<T>;
}

ConversionContext (or any context more appropriate) would be implemented by the chain (along with all other Readers and Keepers). It would look something like:

trait ConversionContext {
    fn decode_header(&self, raw_header: &Any) -> Result<Box<dyn Header>, Error>;
    fn decode_client_state(&self, raw_cs: &Any) -> Result<Box<dyn ClientState>, Error>;
    fn decode_consensus_state(&self, raw_cs: &Any) -> Result<Box<dyn ConsensusState>, Error>;
    ...
}

Note that we could facilitate the implementation of this trait with the light client registry crate proposed in this ADR (for all known light clients known to the registry).

However, the downside of this approach then is that the ibc library doesn't know how to convert Raw -> Domain for types who use, for example, dyn Header. This is because by design, we want the chains to tell us how to decode the Header (and ClientState, etc) for the light clients that it knows about.

We then need to introduce an additional trait TryFromContext:

trait TryFromContext<T> {
    type Error;
    
    fn try_from_ctx(value: T, ctx: ConversionContext) -> Result<Self, Self::Error>;
}

which would be used with, say, MsgUpdateClient:

struct MsgUpdateClient {
    pub client_id: ClientId,
    pub header: Box<dyn Header>,
    pub signer: Signer,
}

impl TryFromContext<RawMsgUpdateClient> for MsgUpdateClient {
    type Error = Error;

    // We need to use `TryFromContext` instead of `TryFrom` so that 
    // the `ConversionContext` can be used to decode the raw `Header`
    fn try_from_ctx(raw: RawMsgUpdateClient, ctx: &ConversionContext) -> Result<Self, Self::Error> {
        Ok(MsgUpdateClient {
            client_id: ...,
            header: ctx.decode_header(&raw)?,
            signer: ...,
        })
    }

Ultimately, I still prefer this solution to using the raw Any for Header et al, as it allows us to have proper domain types (where all fields are also domain types), at the cost of more complexity in the conversion code.

WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

I believe we should indeed aim for a better typed solution here rather than rely on Any types everywhere. That said, if going with Any for the moment makes it easier to move forward rapidly with this, we can delay the properly-typed solution to a later stage.

I would still at least try to gauge if @plafer's proposal above works and if so how much more complex it would make the code before going with either solution.

Copy link
Member Author

Choose a reason for hiding this comment

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

I like the idea of grouping the decoding functions into a ConversionContext and using that in a custom TryFromContext impl. 👌 But I don't think it solves our deserialization problem because we would still need access to the ConversionContext impl in the deserialize impl. Additionally, TryFromContext isn't object safe. Looking into how erased_serde works (as Romain suggested).

Copy link
Member Author

Choose a reason for hiding this comment

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

erased_serde uses a cool trick explained here -> https://github.com/dtolnay/erased-serde/blob/master/explanation/main.rs. It allows one to have a separate object safe version of a trait that can operate with the original (non object safe version) seamlessly.
In our case, I don't think we need to support interoperability with the tendermint_proto::Protobuf trait so we can define our own trait that is object safe and just use that instead. We can still use erased_serde's blanket impl trick to keep the original tendermint_proto::Protobuf API on the implementation side.
See experimental PR where Protobuf is object safe and requires no other changes on the implementation side -> https://github.com/informalsystems/ibc-rs/pull/2412/files#diff-2b95cbfaf034ca5aa2b9a3f02436e8bd7a7a937df7c5ffb6dba81a0237141941R70 =>

mod erased {
  use core::convert::{Into as CoreInto, TryFrom as CoreTryFrom};
  
  mod sealed {
      use super::*;
  
      pub trait SealedInto<T: ?Sized> {}
      impl<T, U: Clone + CoreInto<T>> SealedInto<T> for U {}
      pub trait SealedTryFrom<T> {}
      impl<T, U: CoreTryFrom<T>> SealedTryFrom<T> for U {}
  }
  
  pub trait Into<T: ?Sized>: sealed::SealedInto<T> {
      fn into(&self) -> Box<T>;
  }
  
  impl<T, U: Clone + CoreInto<T>> Into<T> for U {
      fn into(&self) -> Box<T> {
          Box::new(self.clone().into())
      }
  }
  
  pub trait TryFrom<T>: sealed::SealedTryFrom<T> {
      type Error;
  
      fn try_from(t: T) -> Result<Self, Self::Error>
      where
          Self: Sized;
  }
  
  impl<T, U: CoreTryFrom<T>> TryFrom<T> for U {
      type Error = <Self as CoreTryFrom<T>>::Error;
  
      fn try_from(t: T) -> Result<Self, Self::Error>
      where
          Self: Sized,
      {
          <Self as CoreTryFrom<T>>::try_from(t)
      }
  }
}

pub trait Protobuf<T: Message + Default>
where
    Self: erased::TryFrom<T> + erased::Into<T>,
    <Self as erased::TryFrom<T>>::Error: Display,
{ /* ... */ }

Copy link
Member Author

Choose a reason for hiding this comment

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

TryFromContext isn't object safe because the trait function is static (and doesn't have a self parameter) and it returns Self. This is the same problem we faced with the From trait which we solved using the IntoRef solution, but that doesn't work here with TryFromContext, that's because an equivalent TryIntoContext trait bound would look like T: TryIntoContext<Self> and that isn't allowed due object safety restrictions.

Furthermore, to be able to decode using TryFromContext we would need to specify the additional context at the point of decoding which may or may not be accessible there, for e.g. Rust's serde deserialization is stateless and so it is not straightforward to access host context at the time of deserialization.

For these reasons, I believe it is better to not use the TryFromContext trait and instead stick to the TryFrom trait with a where Self: Sized bound.

@romac romac self-requested a review July 12, 2022 13:27
Copy link
Member

@romac romac left a comment

Choose a reason for hiding this comment

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

Amazing work, @hu55a1n1 and @plafer! 💐

The restriction introduced under Domain types containing light client specific types is unfortunate but does seem indeed like the best way forward for now.

@romac romac marked this pull request as draft July 21, 2022 09:02
@hu55a1n1 hu55a1n1 changed the title ADR011: Light client extraction Light client extraction Jul 26, 2022
}
```

### Downcasting support
Copy link
Contributor

Choose a reason for hiding this comment

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

Why would we ever need to downcast to a specific concrete type? Isn't the interface provided by, say, Header enough?

Copy link
Member Author

Choose a reason for hiding this comment

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

I believe this will be required (for example) in cases where the light client wants to access its state stored on the host - the host is oblivious to the light client state's concrete type so it would return a trait object (such as &dyn ConsensusState) and the light client would need to downcast it to the concrete light client type (e.g. TmConsensusState) before it can use light client specific fields from it (such as next_validators_hash).

@plafer plafer changed the title Light client extraction ADR 11: Light client extraction Aug 1, 2022
@romac romac marked this pull request as ready for review August 25, 2022 09:12
@hu55a1n1
Copy link
Member Author

Already merged via #2423 and impld in #2483.

@hu55a1n1 hu55a1n1 closed this Aug 25, 2022
@romac romac deleted the hu55a1n1/adr011-light-client-extraction branch October 6, 2022 14:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants