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

runtime API: Substitute UncheckedExtrinsic with custom encoding #1076

Merged
merged 20 commits into from
Jul 21, 2023

Conversation

lexnv
Copy link
Collaborator

@lexnv lexnv commented Jul 19, 2023

The substrate::UncheckedExtrinsic type is substituted in codgen with a local type that implements custom encoding.

The old UncheckedExtrinsic(pub Vec<u8>) would re-encode the inner bytes of the extrinsic leading to a length prefix byte. This payload is then rejected by substrate WASM code, since the TransactionPaymentApi expects the bytes without the length prefix.

The following type was generated by the codegen:

pub struct UncheckedExtrinsic<_0, _1, _2, _3>(
    pub ::std::vec::Vec<::core::primitive::u8>,
    #[codec(skip)] pub ::core::marker::PhantomData<(_1, _0, _2, _3)>,
);

The type could not be passed to the runtime API because the encoding would expect only a sequence of data.
In contrast, this type is deduced to be a tuple.

Error: Encode(Error { context: Context { path: [Location { inner: Field("uxt") }] }, kind: WrongShape { actual: Tuple, expected: 13 } })
  • Add a new type for UncheckedExtrinsic that is capable of passing through the provided bytes without any further encoding
  • Substitute substrate::UncheckedExtrinsic with our custom UncheckedExtrinsic
  • Validate a raw runtime API call with the statically generated API from codegen

Closes: paritytech/substrate#14584

@lexnv lexnv requested a review from a team as a code owner July 19, 2023 11:11
@lexnv lexnv self-assigned this Jul 19, 2023
Signed-off-by: Alexandru Vasile <[email protected]>
Comment on lines 19 to 22
pub struct UncheckedExtrinsic<Address, Call, Signature, Extra>(
pub Vec<u8>,
#[codec(skip)] pub PhantomData<(Address, Call, Signature, Extra)>,
);
Copy link
Collaborator

@jsdw jsdw Jul 19, 2023

Choose a reason for hiding this comment

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

Potentially we could reuse some other code and avoid some duplicated logic by having this be:

pub struct UncheckedExtrinsic<Address, Call, Signature, Extra>{
    encoded: Static<Encoded>,
    #[codec(skip)]  _marker: PhantomData<(Address, Call, Signature, Extra)>,
};

Then, all of the impls could delegate to the Static<Encoded> type, which I think already does what we want it to.

In any case though, let's give this a nicer interface since users need to call it and shouldn't care about PhantomData etc, something like:

// make a new one:
UncheckedExtrinsic::new(bytes)
// can convert bytes into this type easily:
impl From<Vec<u8>> for UncheckedExtrinsic
// get the bytes:
unchecked_ext.bytes() -> &[u8]
// maybe convert back into bytes?:
impl From<UncheckedExtrinsic> for Vec<u8>

Hopefully type inference will sort out the generic params when it's used, ie

let runtime_api_call = node_runtime::apis()
        .transaction_payment_api()
        .query_fee_details(tx_bytes.into(), len);

Will hopefully Just Work, and infer all of the types properly!

@@ -47,3 +49,57 @@ async fn account_nonce() -> Result<(), subxt::Error> {

Ok(())
}

#[tokio::test]
async fn unchecked_extrinsic_encoding() -> Result<(), subxt::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 like the test; great to check that this works :)

Comment on lines 54 to 75
pub struct UncheckedExtrinsicDecodeAsTypeVisitor<Address, Call, Signature, Extra>(
PhantomData<(Address, Call, Signature, Extra)>,
);

impl<Address, Call, Signature, Extra> Visitor
for UncheckedExtrinsicDecodeAsTypeVisitor<Address, Call, Signature, Extra>
{
type Value<'scale, 'info> = UncheckedExtrinsic<Address, Call, Signature, Extra>;
type Error = scale_decode::Error;

fn unchecked_decode_as_type<'scale, 'info>(
self,
input: &mut &'scale [u8],
_type_id: scale_decode::visitor::TypeId,
_types: &'info scale_info::PortableRegistry,
) -> DecodeAsTypeResult<Self, Result<Self::Value<'scale, 'info>, Self::Error>> {
use scale_decode::{visitor::DecodeError, Error};
let decoded = UncheckedExtrinsic::decode(input)
.map_err(|e| Error::new(DecodeError::CodecError(e).into()));
DecodeAsTypeResult::Decoded(decoded)
}
}
Copy link
Collaborator

@jsdw jsdw Jul 19, 2023

Choose a reason for hiding this comment

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

Aah, I was hoping that you could get rid of more of this by using Static<Encoded> instead! This visitor trait is quite verbose alas.

I wonder whether something like this would work at least, to lean on the inner impl better (while not making any assumptions about it):

impl<Address, Call, Signature, Extra> Visitor
    for UncheckedExtrinsicDecodeAsTypeVisitor<Address, Call, Signature, Extra>
{
    type Value<'scale, 'info> = UncheckedExtrinsic<Address, Call, Signature, Extra>;
    type Error = scale_decode::Error;

    fn unchecked_decode_as_type<'scale, 'info>(
        self,
        input: &mut &'scale [u8],
        type_id: scale_decode::visitor::TypeId,
        types: &'info scale_info::PortableRegistry,
    ) -> DecodeAsTypeResult<Self, Result<Self::Value<'scale, 'info>, Self::Error>> {
        DecodeAsTypeResult::Decoded(self.0.decode_as_type(input, type_id.0, types))
    }
}

Maybe this is a sign that we can do something better with the DecodeAsType macro to allow things to be decoded based on some inner type or something.

use super::{Encoded, Static};

/// The unchecked extrinsic from substrate.
#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, scale_encode::EncodeAsType)]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah alas we need to decalre it ourselves because the derive EncodeAsType impl will try to be smarter and make sure it's encoding this whole thing as a tuple, where we just want to defer to the Static impl and ignore this surrounding type entirely (this is why the test is failing)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep exactly, we could at least remove the manual Encode part. I do wonder, could we make the EncodeAsType somehow ignore any fields that have #[codec(skip)] on them (similar to the codec)? 🤔

Copy link
Collaborator

Choose a reason for hiding this comment

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

It already automatically ignores any fields that the metadata isn't saying need to be encoded; the issue is more that it sees that it's on a tuple type and so expects the target type to be tuple-ish too (so that it knows how to encode the inner fields).

We'd want something like a #[transparent] flag or something to delegate to the inner impl :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

And good idea on using Encode and Decode derives; that helps!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks that makes sense!

Signed-off-by: Alexandru Vasile <[email protected]>
Signed-off-by: Alexandru Vasile <[email protected]>
Copy link
Contributor

@tadeohepperle tadeohepperle left a comment

Choose a reason for hiding this comment

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

Looks good to me!


/// Get the bytes of the encoded extrinsic.
pub fn bytes(&self) -> &[u8] {
self.0 .0 .0.as_slice()
Copy link
Contributor

Choose a reason for hiding this comment

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

This formatting looks so weird, never seen that before :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Same, I used to write it without spaces but I've changed my editor to run rustfmt for each file save which changed it like so :D

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think rustfmt is just being a bit naff here, but ah well :)

@@ -26,7 +28,7 @@ pub use primitive_types::{H160, H256, H512};

/// Wraps an already encoded byte vector, prevents being encoded as a raw byte vector as part of
/// the transaction payload
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq, Decode)]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah hmmm, I'm wondering about this automatic Decode, because:

  • decoding like this will first decode a Compact length for the vec length and then put the relevant number of butes into the vec.
  • encoding is not the opposite of this; it will just encode the bytes in the vec to the target; no compact length or whatever at the beginning.

Having a look, an extrinsic is actually just a compact encoded length and then that number of bytes.

So probably you were right in a previous iteration, and UncheckedExtrinsic should have a custom Decode impl that will decode the compact length and then the relevant number of bytes following it all into the Static<Encoded> struct, which will then encode (from the impl below) precisely those bytes back.

@jsdw
Copy link
Collaborator

jsdw commented Jul 20, 2023

All looks great; I think we just need to put back a manual impl of Decode on UncheckedExtrinsic which decodes the compact length and then following bytes and puts all of those bytes in a vec.

Conceptually something like:

fn decode(bytes) {
    // the exttrinsic is basically identical to the encoded form of this:
    let xt_vec = <Vec<u8>>:decode(&mut bytes)?;
    UncheckedExtrinsic(Static(Encoded(xt_vec.encode())))
}

We could save the extra vec allocation by manually decoding the compact length etc, but maybe it's easier just to do the above; we don't expect this to be a common op?

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.

Nice work!

@lexnv lexnv merged commit fd8f60c into master Jul 21, 2023
@lexnv lexnv deleted the lexnv/runtime_api_extrinsic_encoding branch July 21, 2023 10:34
@jsdw jsdw mentioned this pull request Jul 24, 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.

Bad metadata for TransactionPaymentApi.query_info call in Metadata v15.
3 participants