-
Notifications
You must be signed in to change notification settings - Fork 248
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
Conversation
Signed-off-by: Alexandru Vasile <[email protected]>
Signed-off-by: Alexandru Vasile <[email protected]>
Signed-off-by: Alexandru Vasile <[email protected]>
Signed-off-by: Alexandru Vasile <[email protected]>
Signed-off-by: Alexandru Vasile <[email protected]>
pub struct UncheckedExtrinsic<Address, Call, Signature, Extra>( | ||
pub Vec<u8>, | ||
#[codec(skip)] pub PhantomData<(Address, Call, Signature, Extra)>, | ||
); |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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 :)
Signed-off-by: Alexandru Vasile <[email protected]>
Signed-off-by: Alexandru Vasile <[email protected]>
Signed-off-by: Alexandru Vasile <[email protected]>
Signed-off-by: Alexandru Vasile <[email protected]>
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) | ||
} | ||
} |
There was a problem hiding this comment.
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)] |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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)? 🤔
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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]>
Signed-off-by: Alexandru Vasile <[email protected]>
…xtrinsic_encoding
There was a problem hiding this 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() |
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)] |
There was a problem hiding this comment.
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.
All looks great; I think we just need to put back a manual impl of 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? |
Signed-off-by: Alexandru Vasile <[email protected]>
Co-authored-by: James Wilson <[email protected]>
Signed-off-by: Alexandru Vasile <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work!
Signed-off-by: Alexandru Vasile <[email protected]>
…ding' into lexnv/runtime_api_extrinsic_encoding
Signed-off-by: Alexandru Vasile <[email protected]>
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 theTransactionPaymentApi
expects the bytes without the length prefix.The following type was generated by the codegen:
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 } })
UncheckedExtrinsic
that is capable of passing through the provided bytes without any further encodingsubstrate::UncheckedExtrinsic
with our customUncheckedExtrinsic
Closes: paritytech/substrate#14584