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

Support decoding signed extensions #1209

Merged
merged 16 commits into from
Oct 31, 2023

Conversation

tadeohepperle
Copy link
Contributor

@tadeohepperle tadeohepperle commented Oct 16, 2023

Fixes #1124
This PR adds an option to dynamically decode signed extensions from extrinsics.
Until now we just skipped over any signed extensions (Signed Extra bytes).
There are two convenience functions for tip() and nonce(). When you have ExtrinsicDetails you can not do the following:

let extrinsic: ExtrinsicDetails;
let signed_extensions = extrinsic.signed_extensions()?;
// tip and nonce
let tip: u128 = signed_extensions.tip()?;
let nonce: u64 = signed_extensions.nonce()?;
// iterate over signed extensions
for s in signed_extensions.iter() {
     let s = s.unwrap(); // error if decoding failed. Ends iteration.
     let name = s.name();    
     let scale_value = s.value();
     let bytes = s.bytes();
}

// or find a specific one:
let mortality = signed_extensions.find("CheckMortality")?;
...

subxt/src/config/polkadot.rs Outdated Show resolved Hide resolved
subxt/src/config/substrate.rs Outdated Show resolved Hide resolved
@tadeohepperle tadeohepperle marked this pull request as ready for review October 24, 2023 12:07
@tadeohepperle tadeohepperle requested a review from a team as a code owner October 24, 2023 12:07
/// # Note
///
/// Returns `None` if the extrinsic is not signed.
pub fn extra_bytes(&self) -> Option<&[u8]> {
Copy link
Member

Choose a reason for hiding this comment

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

dq: does this include just extra or extra | additional_signed?

See https://docs.rs/sp-runtime/latest/src/sp_runtime/generic/unchecked_extrinsic.rs.html#197 for context

Regardless, I think it would be good to document that

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, since we have signed_extensions below to get the signed extensions, let's call this signed_extensions_bytes so it's clear it's getting the bytes for the same thing.

/// The tip of an extrinsic, extracted from the ChargeTransactionPayment signed extension.
pub fn tip(&self) -> Option<u128> {
let tip = self
.find("ChargeTransactionPayment")
Copy link
Member

@niklasad1 niklasad1 Oct 25, 2023

Choose a reason for hiding this comment

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

Would be cool have these in the metadata, I'm not sure whether some chains overrides the transaction_payment pallet and add their own impl probably not.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah; the idea of these "quick" helpers is that they work in the overwhelmingly common case, but may not always work (and should fail gracefully eg returning None here if the extension isn't available)

}

/// The nonce of the account that submitted the extrinsic, extracted from the CheckNonce signed extension.
pub fn nonce(&self) -> Option<u64> {
Copy link
Member

Choose a reason for hiding this comment

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

This may fail on certain runtimes if the CheckNonce isn't part of the SignedExtra.

Might worth a comment for it and advise to use the dynamic API then.

Copy link
Member

Choose a reason for hiding this comment

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

Should be nice to have for the general usecase...

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, it's just for a common case that will most likely work, and should fail gracefully and return None if the relevant extension isn't found :)

@@ -227,3 +229,61 @@ async fn fetch_block_and_decode_extrinsic_details() {
assert_eq!(ext.value, 10_000);
assert!(tx.is_signed());
}

#[tokio::test]
async fn decode_signed_extensions_from_blocks() {
Copy link
Member

Choose a reason for hiding this comment

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

Would be cool to test with different runtimes so both ChargeAssetTxPayment and ChargeTransactionPayment works but probably annoying to support here.

Could be done in another PR

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah; I pondered this too but not sure how I'd test this?! It'd be a pain introducing the ability to spawn up a different node just for this

Comment on lines 638 to 640
if index == num_signed_extensions {
None
} else {
Copy link
Collaborator

@jsdw jsdw Oct 25, 2023

Choose a reason for hiding this comment

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

Nit; perhaps avoid the indentation in the else by doing return None here instead; an early exit when done :)


impl<'a> ExtrinsicSignedExtensions<'a> {
/// Get a certain signed extension by its name.
pub fn find(&self, signed_extension: impl AsRef<str>) -> Option<ExtrinsicSignedExtension> {
Copy link
Collaborator

@jsdw jsdw Oct 25, 2023

Choose a reason for hiding this comment

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

To be consistent with how Extrinsics/Events are decoded, I think find should look more like:

fn find<S: SignedExtension>() -> Result<Option<S>, Error>

For "dynamically finding things", one can .iter().find(..) anyway.

I'd suggest just removing this for now until we have a nice way to find and decode statically based on the subxt::tx::signed_extensions::* types (which should def be a separate PR).

}

impl<'a> ExtrinsicSignedExtension<'a> {
/// The extra bytes associated with the signed extension.
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
/// The extra bytes associated with the signed extension.
/// The bytes representing this signed extension.

}

/// DecodedValueThunk representing the type of the extra of this signed extension.
pub fn decoded(&self) -> Result<DecodedValueThunk, Error> {
Copy link
Collaborator

@jsdw jsdw Oct 25, 2023

Choose a reason for hiding this comment

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

Let's remove this decoded fn; it doesn't add much on top of value() below imo :)

The value fn can do a more "direct" decoding into a Value type them too; something like Value::decode_as_type(...)

assert_eq!(nonce1, 0);
assert_eq!(tip1, 1234);
assert_eq!(nonce2, 1);
assert_eq!(tip2, 5678);
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 like us to also test that iterating over signed extensions works; right now we have no way to really know if it's actually decoding them as we expect :)

The set of signed extensions on a node is fairly fixed, so we can just sanity test that each of the expected ones is decoded and that the names are right or something like that (in addition to the above).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added code for testing this to the end of the test :)

@jsdw
Copy link
Collaborator

jsdw commented Oct 25, 2023

Looking good; I resolved all of the comments from myself that you've addressed so just worth looking over the others; nothing major from me now! Will be lovely to have this!

On testing: one way to unit test this would be crafting some custom metadata with whatever signed extensions we want, and then creating an OfflineClient (note the offline), setting the metadata on it to be what we want, and then building an extrinsic (could be dynamically constructed). Then, one can manually make the bytes into an ExtrinsicDetails and try to decode each of the signed extensions from it.

This way, we could test a few different combinations of signed extensions (eg if Nonce/Tip ones don't exist, or if both the tip ones exist etc).

But yes, I don't mind this being a follow-up PR because it'll take a little work to do nicely!

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.

Great job, looks good to me!

I wondered why missing docs on a function weren't being picked up in CI, and noticed that the new structs weren't exposed in the module, so I did that and extended a few lifetimes while in the area :)

let alice = dev::alice();
let bob = dev::bob();

macro_rules! submit_transfer_extrinsic_and_get_it_back {
Copy link
Collaborator

Choose a reason for hiding this comment

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

dq: What are the benefits of this macro over a plain function? Looks good otherwise!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There were just some really stupid lifetime issues when using a Closure.
And I cannot use a normal function easily because then I need to specify all types.
And the type of api here depends on the unstable-light-client feature flag. And the type of api would be a generic type of the return value of that function as well. So I would have to provide two implementations of such a function depending on the feature flag. Also I cannot put the api a trait object easily, because OnlineClientT is not object safe. So in the end I just went with this macro.

@lexnv
Copy link
Collaborator

lexnv commented Oct 27, 2023

LGTM!

What do you think about extending the blocks_subscribing to also decode the extensions? 🤔

@jsdw
Copy link
Collaborator

jsdw commented Oct 30, 2023

What do you think about extending the blocks_subscribing to also decode the extensions? 🤔

I don't think that this is a blocker for merging, but would be nice to show this stuff in an example somewhere!


/// Return only the bytes of the signature for this signed extrinsic.
///
/// # Note
Copy link
Member

@niklasad1 niklasad1 Oct 30, 2023

Choose a reason for hiding this comment

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

This note can be removed and change the documentation to something like:

Returns Some(signature_bytes) if the extrinsic was signed otherwise None is returned.

Copy link
Member

@niklasad1 niklasad1 left a comment

Choose a reason for hiding this comment

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

nice

Comment on lines 37 to 47
if let Some(signed_extensions) = ext.signed_extensions() {
println!(" Signed Extensions:");
for signed_extension in signed_extensions.iter() {
let signed_extension = signed_extension?;
let name = signed_extension.name();
let value = signed_extension.value()?.to_string();
println!(" {name}: {value}");
}
} else {
println!(" No Signed Extensions");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just because this is an example, I wonder whether we could make this a bit less ugly, ie removing the else branch and moving it after the other println's or something?

@tadeohepperle tadeohepperle merged commit 1b88ebf into master Oct 31, 2023
10 checks passed
@tadeohepperle tadeohepperle deleted the tadeohepperle/support-decoding-signed-extensions branch October 31, 2023 09:40
@jsdw jsdw mentioned this pull request Dec 7, 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.

Support decoding signed extensions from extrinsics in blocks
4 participants