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

codegen for root level error #930

Merged
merged 11 commits into from
May 2, 2023

Conversation

tadeohepperle
Copy link
Contributor

fixes #734.

Added an enum Error to the generated code. It has one variant for each pallet that exposes an error type.
It implements a new trait called RootError. The RootError trait allows the generated root Error type to be constructed from a pallet index (u8) and 4 error bytes:

pub trait RootError {
    fn root_error(pallet_index: &u8, error: &[u8; 4]) -> Result<Self, Error>;
}

The ModuleError struct now exposes a function as_root_error<E: RootError>(&self) -> Result<E, Error> that trys to convert the ModuleError into the generates top level enum Error.

This makes it possible to check if a certain subxt error is a certain error from a certain pallet:

fn is_err(error: subxt::Error, target: api::Error) -> bool {
    let subxt::Error::Runtime(DispatchError::Module(module_err) = subxt_error else {
        return false
    }
    module_err.as_root_error<api::Error>() == target
}

@tadeohepperle tadeohepperle requested a review from a team as a code owner April 25, 2023 08:41
};

let root_error_match_arms = self.metadata.pallets.iter().filter_map(|p| {
let variant_index = p.index;
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should compare based on variant names and not indexes; indexes can chanbe between nodes (as pallets tend to live in different indexes on different chains), so comparing on names is more portable :)

Copy link
Contributor Author

@tadeohepperle tadeohepperle Apr 25, 2023

Choose a reason for hiding this comment

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

Ah okay, so a conversion from index to name should happen inside the as_root_error() function of this struct:

pub struct ModuleError {
    metadata: Metadata,
    raw: RawModuleError,
}

using it's metadata field?

Copy link
Collaborator

Choose a reason for hiding this comment

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

There's a details() function on the ModuleError already which will give you the pallet name that the error came from, so I'd probably call that inside the as_root_error function and then hand the name to the RootError trait so then the codegen can use the name to find the correct variant that was generated.


p.error.as_ref().map(|_|
quote! {
#variant_index => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just as a nit; I did "if arms" and not "match arms" for the RootEvent stuff, so I think we should be consistent one way or the other :)

The reason I did "if arms" was so that the impl below that uses this output is cleaner (match arms mean you have part of the match syntax here and part of it below which imo is a little less clean).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah okay, I liked the match style more, because it is less generated code, but I can change it to if arms

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not strongly against the match style; the "if" one just felt nicer code wise; either way both impls should be identical to be consistent :)

}
}


Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit; unnecessary newline :)

Comment on lines 168 to 171
pub trait RootError: Sized {
/// Given details of the pallet error we want to decode
fn root_error(pallet_index: &u8, error: &[u8; 4]) -> Result<Self, Error>;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd more closely mirror the events version which looks like:

pub trait RootEvent: Sized {
    fn root_event(
        pallet_bytes: &[u8],
        pallet_name: &str,
        pallet_event_ty: u32,
        metadata: &Metadata,
    ) -> Result<Self, Error>;
}

So something more like this maybe:

pub trait RootError: Sized {
    fn root_error(
        pallet_bytes: [u8; 4],
        pallet_name: &str,
        pallet_event_ty: u32,
        metadata: &Metadata,
    ) -> Result<Self, Error>;
}

Reasons:

  1. We should avoid relying on Decode::decode if we can and instead use #mod_name::Error::decode_with_metadata to decode it. Why? because Decode::decode will just decode the bytes based on the static codegenned type, whereas decode_with_metadata will decode into the error enum based on matching up the encoded variant names and fields with the error struct we've codegenned, making it more robust to changes in eg variant order or field order or whatever.
  2. Using a string instead of index for pallet; same as above, just allows more robust matching across nodes where pallets are likely in a different order.
  3. Just generally to be consistent with RootError :)

Comment on lines 106 to 113
/// Returns a reference to [`PalletMetadata`].
pub fn pallet_by_index(&self, pallet_index: u8) -> Result<&PalletMetadata, MetadataError> {
self.inner
.pallets
.values()
.find(|pallet_metadata| pallet_metadata.index == pallet_index)
.ok_or(MetadataError::PalletNotFound)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe won't be needed when moving to using names over indexes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, I removed the function.

@tadeohepperle
Copy link
Contributor Author

I was not able to find a way to produce a ModuleError in an integration test, if one of you @jsdw @niklasad1 @lexnv knows how I can get a ModuleError returned from a substrate node, please let me know, then I add a test. Otherwise we can merge without test, it compiles at least.

Ok(quote! {
#docs
pub type Error = #error_type;

Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change

Nit: no need for this extra line

@@ -159,6 +163,12 @@ impl ModuleError {
pub fn raw(&self) -> RawModuleError {
self.raw
}

/// attempts to decode the ModuleError into a value implementing the trait `RootError`
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/// attempts to decode the ModuleError into a value implementing the trait `RootError`
/// Attempts to decode the ModuleError into a value implementing the trait `RootError`

Nit: we should start public documentation with upper case, for code comments this is fine tho

pub trait RootError: Sized {
/// Given details of the pallet error we want to decode
fn root_error(
pallet_bytes: &[u8; 4],
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: Not a big deal at all but I'd either go for [u8; 4] (no need for reference since it's only 4 bytes) or &[u8] (gives a little room for growth if the error type grows from [u8;4] for some reason) :)

Copy link
Collaborator

@jsdw jsdw left a comment

Choose a reason for hiding this comment

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

Added a test and it all looks to work well to me; nice job!

@@ -320,10 +321,13 @@ impl RuntimeGenerator {
should_gen_docs,
)?;

let errors = errors::generate_error_type_alias(&type_gen, pallet, should_gen_docs)?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: We could name this something like error_alias to be a bit more explicit, however you prefer here

Comment on lines +378 to +396
let outer_error_variants = self.metadata.pallets.iter().filter_map(|p| {
let variant_name = format_ident!("{}", p.name);
let mod_name = format_ident!("{}", p.name.to_string().to_snake_case());
let index = proc_macro2::Literal::u8_unsuffixed(p.index);

p.error.as_ref().map(|_| {
quote! {
#[codec(index = #index)]
#variant_name(#mod_name::Error),
}
})
});

let outer_error = quote! {
#default_derives
pub enum Error {
#( #outer_error_variants )*
}
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Leaving here my thoughts, not necessarily to be done for this PR:

This part of the code seems highly similar to the generation of the outer Event enum.
There might be a few things we could do here to reduce code duplicate, improve reliability and maintenance:

  • add a function generate_outer_enum
  • take in the name of the enum (either Event / Error or other names we might add in the future)
#[derive(ToTokens)]
enum OuterEnumName {
   Error,
   Event,
}

Copy link
Collaborator

@lexnv lexnv left a comment

Choose a reason for hiding this comment

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

Nicely done!

The PR looks clean, I've left a few ideas that we could tackle in other PRs 👍

@tadeohepperle tadeohepperle merged commit 265f16f into master May 2, 2023
@tadeohepperle tadeohepperle deleted the tadeo-hepperle-root-level-error-codegen branch May 2, 2023 15:33
@jsdw jsdw mentioned this pull request Jun 1, 2023
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.

Add pallet name as constant
3 participants