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

Parse signed payload and expose call data type information #66

Merged
merged 13 commits into from
Nov 4, 2021

Conversation

jsdw
Copy link
Collaborator

@jsdw jsdw commented Nov 3, 2021

This PR builds on @Noch3's work to add decoding for the signer payload. It also exposes the Variant type in the call data, so that you can access the name and details about each field, and it exposes scale_info more directly, and a means of getting hold of the type registry from our metadata.

Noch3's PR exposed the cursor approach (using input like &mut &[u8]); I felt like this made it easier to decide how to handle remaining bytes, and decode bits from a stream, so I consistified the rest of the decode API on this approach too.

I'm also experimenting here with returning references wherever possible when decoding to save some allocations when they may not be needed.

A couple of thoughts/questions:

  • Is it worth having the Owned variants of these structs around or should it be left to users to clone/own whatever they want from them?
  • I'm exposing the Variant type in the call data info. I'm not super confortable about mingling types and values in the output (at least, w.r.t converting to owned structs). Curious how others feel on this!

@jsdw
Copy link
Collaborator Author

jsdw commented Nov 3, 2021

@Noch3 I'd be keen on gathering your feedback on this one, since it builds on your previous PRs and hopefully exposes the pieces of information you wanted :)

@jsdw jsdw requested a review from insipx November 3, 2021 11:23
@elliot-u410
Copy link

Wow thank you so much! I'll take a look today!

Copy link

@elliot-u410 elliot-u410 left a comment

Choose a reason for hiding this comment

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

This is awesome! Thank you!

I think this helps #57 but #60 would require keeping the call data in each layer of Value. That would be a larger refactor and can be saved for later but I think that would be amazing for nested call-data things like batch/proxy.

I'm gonna do a bit of testing with this.

core_v14/src/decoder/mod.rs Outdated Show resolved Hide resolved
core_v14/src/decoder/mod.rs Show resolved Hide resolved
@jsdw
Copy link
Collaborator Author

jsdw commented Nov 3, 2021

I think this helps #57 but #60 would require keeping the call data in each layer of Value. That would be a larger refactor and can be saved for later but I think that would be amazing for nested call-data things like batch/proxy.

I wonder whether this PR providing a separate tree of type information that can be aligned to the Value types that come back would be sufficient for #60 (it's a little more work perhaps to line up and compare the type tree with the values, but I believe still possible?

@elliot-u410
Copy link

Yes the design space there is large so it makes sense to solve that separately.

pub fn args(&self) -> &[TypeId] {
&self.args
}
struct MetadataCalls {
Copy link
Member

Choose a reason for hiding this comment

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

👍

unnamed_value(vec![])
}

fn singleton_value(x: Value) -> Value {
Copy link
Member

Choose a reason for hiding this comment

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

Whats the idea behind storing prim_u<int>_value() in a singleton for the tests?

Copy link
Contributor

Choose a reason for hiding this comment

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

had the same question!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

A singleton here is just a Composite value with only a single member. Some of the metadata types decode into such a thing (eg newtype wrapper structs), and so we need to do the same wrapping when comparing the Value we get back.

@@ -21,3 +21,6 @@ pub mod value;
pub use decoder::Decoder;
pub use metadata::Metadata;
pub use value::Value;

/// A re-export of the [`scale_info`] crate, since we delegate much of the type inspection to it.
Copy link
Member

Choose a reason for hiding this comment

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

What does re-export mean, as in where else is this getting exported? Trying to get more familiar with whats going on here :)

Copy link
Contributor

Choose a reason for hiding this comment

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

It's common for libraries that manipulate data expressed in types stemming from another crate (like here, much of what is going on in desub relies on scale-info types) to "adopt" or "forward" that other crate's types as if they were its own.
One instance of this is how the rust standard library re-exports types and code modules from the core:: namespace as std::. The core:: set of types and modules do not require a std environment but that's not very important to std-using applications, so for convenience the core::* modules are also available through std::*.
In this case James is re-exporting all of scale-info, allowing users to access them through the desub path, e.g. the use desub::TypeInfo, rather than use scale_info::TypeInfo. This is convenient and perfectly fine because it is not a feature of desub to have "swappable" scale type info backends.

Clear as mud? :)

Copy link
Collaborator Author

@jsdw jsdw Nov 4, 2021

Choose a reason for hiding this comment

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

(Thank you! Nit: they'd have access via desub::scale_info::TypeInfo as it stands. The main benefit just being that you don't need to also add/keep-up-to-date scale_info to yoru Cargo.toml; you can just access the version used by desub)

calls: HashMap<u8, MetadataCall>,
/// Metadata may not contain call information. If it does,
/// it'll be here.
calls: Option<MetadataCalls>,
Copy link
Member

Choose a reason for hiding this comment

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

Might be a silly dumb question, but do you know any cases off the top of your head where this would unwrap to None?

Copy link
Contributor

Choose a reason for hiding this comment

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

Just guessing here, but maybe something like the shell parachain could have zero calls? (Well, hmm, that one has exactly one call right?). It'd be something pretty exotic, but unless it's illegal to have a pallet with zero calls we need something here that can represent such a thing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I wasn't sure either, but since the frame-metadata type allows for None I thought that instead of erroring immediately, I'd allow for the possibility and, if needed, handle errors closer to the calls that would be broken by it.

Copy link
Member

@TarikGul TarikGul left a comment

Choose a reason for hiding this comment

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

lgtm. I really like the inline docs as it allowed me to follow along pretty seamlessly.

@Noch3 Thanks for bringing this to light!

core_v14/src/decoder/extrinsic_bytes.rs Outdated Show resolved Hide resolved
core_v14/src/decoder/extrinsic_bytes.rs Outdated Show resolved Hide resolved
core_v14/src/decoder/mod.rs Outdated Show resolved Hide resolved
@@ -127,17 +129,14 @@ pub enum DecodeError {
DecodeTypeError(#[from] DecodeTypeError),
#[error("Failed to decode: expected more data")]
EarlyEof(&'static str),
#[error("Failed to decode extrinsics: expected {0} more bytes to be consumed from the input")]
DidntConsumeBytes(usize),
Copy link
Contributor

Choose a reason for hiding this comment

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

UndrainedBytes maybe?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I had trouble with this one! Maybe BytesNotConsumed or ExcessBytes?

Copy link
Contributor

Choose a reason for hiding this comment

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

I like ExcessBytes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ExcessBytes it is!

/// of the extrinsic we think we decoded, and the number of bytes left in the slice provided (which we
/// expected to entirely consume, but did not).
#[error("Decoding an extrinsic should consume all bytes, but {0} bytes remain")]
LateEof(usize, Extrinsic),
Copy link
Contributor

Choose a reason for hiding this comment

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

good riddance! That "late end of file" really messed with my brain. :)

Comment on lines 53 to 55
for (idx, variant) in calls_variant.variants().iter().enumerate() {
call_variant_indexes.insert(variant.index(), idx);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't we use FromIterator here somehow, dunno, call_variant_indexes = calls_variant.variants().iter().enumerate().collect()?

Copy link
Contributor

Choose a reason for hiding this comment

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

random thought: perhaps call_variant_indexes should be an IndexMap? I guess these collections are small enough that it doesn't matter.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, yup we can now collect straight into a HashMap; good call!

I looked at IndexMap briefly; it's all about preserving insertion order by the sounds of it. What was your thinking around using it?

(since the index is a u8 I could see that a [usize; 256] or something could be used if we wanted)

Copy link
Contributor

Choose a reason for hiding this comment

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

What was your thinking around using it?

It was a random thought, something like this: given the purpose of this hashmap is to map from one index to another index, we could sort of use insertion order (idx here) instead of a hashed key. So IndexMap's IndexSet type. Or, as you suggest, a fixed sized array.
I don't think it's a good optimization to make at this point though so, "no action required"!

core_v14/src/metadata/version_14.rs Outdated Show resolved Hide resolved
core_v14/src/decoder/mod.rs Outdated Show resolved Hide resolved
core_v14/src/decoder/mod.rs Outdated Show resolved Hide resolved
unnamed_value(vec![])
}

fn singleton_value(x: Value) -> Value {
Copy link
Contributor

Choose a reason for hiding this comment

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

had the same question!

core_v14/src/decoder/mod.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@insipx insipx left a comment

Choose a reason for hiding this comment

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

lgtm!

@jsdw jsdw merged commit 87377e7 into master Nov 4, 2021
@jsdw jsdw deleted the jsdw-decode-flexibility branch November 4, 2021 13:55
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.

5 participants