From 56d0cdae785ef863ac282d6bc9c01f7e075cb2c4 Mon Sep 17 00:00:00 2001 From: Tadeo Hepperle <62739623+tadeohepperle@users.noreply.github.com> Date: Fri, 10 Nov 2023 18:32:58 +0100 Subject: [PATCH] Static Decoding of Signed Extensions: Simple Approach (#1235) * skeleton commit * signed extension decoding * fix some minor things * make api more similar to Extrinsics * defer decoding of signed extensions * fix byte slices * add test for nonce signed extension * adjust test and extend for tip * clippy * support both ChargeTransactionPayment and ChargeAssetTxPayment * address PR comments * Extend lifetimes, expose pub structs, remove as_type * add signed extensions to block subscribing example * add Decoded type * fix merging bug and tests * add decoded type in CustomSignedExtension * fix minor issues, extend test * cargo fmt differences * remove the `decoded` function * new as_signed_extra fn, do not expose as_type anymore * fix Result-Option order, simplify obtaining Nonce * tx: Remove `wait_for_in_block` helper method (#1237) Signed-off-by: Alexandru Vasile * Update smoldot to 0.12 (#1212) * Update lightclient Signed-off-by: Alexandru Vasile * testing: Fix typo Signed-off-by: Alexandru Vasile * testing: Update cargo.toml Signed-off-by: Alexandru Vasile * lightclient: Add tracing logs to improve debugging Signed-off-by: Alexandru Vasile * lightclient: Add socket buffers module for `PlatformRef` Signed-off-by: Alexandru Vasile * lightclient: Update `SubxtPlatform` Signed-off-by: Alexandru Vasile * cargo: Add lightclient dependencies Signed-off-by: Alexandru Vasile * Update cargo.lock of wasm tests Signed-off-by: Alexandru Vasile * lightclient: Add constant for with-buffer module Signed-off-by: Alexandru Vasile * lightclient: Replace rand crate with getrandom Signed-off-by: Alexandru Vasile * example: Update cargo lock file Signed-off-by: Alexandru Vasile * examples: Update deps Signed-off-by: Alexandru Vasile --------- Signed-off-by: Alexandru Vasile Co-authored-by: Tadeo Hepperle <62739623+tadeohepperle@users.noreply.github.com> * ChargeAssetTxPayment: support providing u32 or MultiLocation in default impl (#1227) * Asset Id in Config trait * add example configuring the config * fmt * fix Default trait bound * merge examples, fix default again * adjust config in examples * Update subxt/src/config/mod.rs Co-authored-by: James Wilson --------- Co-authored-by: James Wilson * generic AssetId * fix generics * fmt --------- Signed-off-by: Alexandru Vasile Co-authored-by: James Wilson Co-authored-by: Alexandru Vasile <60601340+lexnv@users.noreply.github.com> --- subxt/examples/blocks_subscribing.rs | 1 - .../examples/setup_config_signed_extension.rs | 1 + subxt/src/blocks/extrinsic_types.rs | 84 ++++++++++++++----- subxt/src/config/mod.rs | 3 +- subxt/src/config/signed_extensions.rs | 39 ++++++++- subxt/src/utils/era.rs | 6 +- .../src/full_client/blocks/mod.rs | 52 +++++++++--- 7 files changed, 146 insertions(+), 40 deletions(-) diff --git a/subxt/examples/blocks_subscribing.rs b/subxt/examples/blocks_subscribing.rs index 54ab2818e6..109a81bf5f 100644 --- a/subxt/examples/blocks_subscribing.rs +++ b/subxt/examples/blocks_subscribing.rs @@ -40,7 +40,6 @@ async fn main() -> Result<(), Box> { for evt in events.iter() { let evt = evt?; - let pallet_name = evt.pallet_name(); let event_name = evt.variant_name(); let event_values = evt.field_values()?; diff --git a/subxt/examples/setup_config_signed_extension.rs b/subxt/examples/setup_config_signed_extension.rs index 42c278dfbd..c74e5f655b 100644 --- a/subxt/examples/setup_config_signed_extension.rs +++ b/subxt/examples/setup_config_signed_extension.rs @@ -46,6 +46,7 @@ pub struct CustomSignedExtension; // up in the chain metadata in order to know when and if to use it. impl signed_extensions::SignedExtension for CustomSignedExtension { const NAME: &'static str = "CustomSignedExtension"; + type Decoded = (); } // Gather together any params we need for our signed extension, here none. diff --git a/subxt/src/blocks/extrinsic_types.rs b/subxt/src/blocks/extrinsic_types.rs index 6f9d1fbf39..5d7549e739 100644 --- a/subxt/src/blocks/extrinsic_types.rs +++ b/subxt/src/blocks/extrinsic_types.rs @@ -12,9 +12,13 @@ use crate::{ Metadata, }; +use crate::config::signed_extensions::{ + ChargeAssetTxPayment, ChargeTransactionPayment, CheckNonce, +}; +use crate::config::SignedExtension; use crate::dynamic::DecodedValue; use crate::utils::strip_compact_prefix; -use codec::{Compact, Decode}; +use codec::Decode; use derivative::Derivative; use scale_decode::{DecodeAsFields, DecodeAsType}; @@ -366,12 +370,13 @@ where } /// Returns `None` if the extrinsic is not signed. - pub fn signed_extensions(&self) -> Option> { + pub fn signed_extensions(&self) -> Option> { let signed = self.signed_details.as_ref()?; let extra_bytes = &self.bytes[signed.signature_end_idx..signed.extra_end_idx]; Some(ExtrinsicSignedExtensions { bytes: extra_bytes, metadata: &self.metadata, + _marker: std::marker::PhantomData, }) } @@ -605,21 +610,22 @@ impl ExtrinsicEvents { /// The signed extensions of an extrinsic. #[derive(Debug, Clone)] -pub struct ExtrinsicSignedExtensions<'a> { +pub struct ExtrinsicSignedExtensions<'a, T: Config> { bytes: &'a [u8], metadata: &'a Metadata, + _marker: std::marker::PhantomData, } -impl<'a> ExtrinsicSignedExtensions<'a> { +impl<'a, T: Config> ExtrinsicSignedExtensions<'a, T> { /// Returns an iterator over each of the signed extension details of the extrinsic. /// If the decoding of any signed extension fails, an error item is yielded and the iterator stops. - pub fn iter(&self) -> impl Iterator, Error>> { + pub fn iter(&self) -> impl Iterator, Error>> { let signed_extension_types = self.metadata.extrinsic().signed_extensions(); let num_signed_extensions = signed_extension_types.len(); let bytes = self.bytes; - let metadata = self.metadata; let mut index = 0; let mut byte_start_idx = 0; + let metadata = &self.metadata; std::iter::from_fn(move || { if index == num_signed_extensions { @@ -649,49 +655,69 @@ impl<'a> ExtrinsicSignedExtensions<'a> { ty_id, identifier: extension.identifier(), metadata, + _marker: std::marker::PhantomData, })) }) } + fn find_by_name(&self, name: &str) -> Option> { + let signed_extension = self + .iter() + .find_map(|e| e.ok().filter(|e| e.name() == name))?; + Some(signed_extension) + } + + /// Searches through all signed extensions to find a specific one. + /// If the Signed Extension is not found `Ok(None)` is returned. + /// If the Signed Extension is found but decoding failed `Err(_)` is returned. + pub fn find>(&self) -> Result, Error> { + self.find_by_name(S::NAME) + .map(|s| { + s.as_signed_extra::().map(|e| { + e.expect("signed extra name is correct, because it was found before; qed.") + }) + }) + .transpose() + } + /// The tip of an extrinsic, extracted from the ChargeTransactionPayment or ChargeAssetTxPayment /// signed extension, depending on which is present. /// /// Returns `None` if `tip` was not found or decoding failed. pub fn tip(&self) -> Option { - let tip = self.iter().find_map(|e| { - e.ok().filter(|e| { - e.name() == "ChargeTransactionPayment" || e.name() == "ChargeAssetTxPayment" + // Note: the overhead of iterating twice should be negligible. + self.find::() + .ok() + .flatten() + .map(|e| e.tip()) + .or_else(|| { + self.find::>() + .ok() + .flatten() + .map(|e| e.tip()) }) - })?; - - // Note: ChargeAssetTxPayment might have addition information in it (asset_id). - // But both should start with a compact encoded u128, so this decoding is fine. - let tip = Compact::::decode(&mut tip.bytes()).ok()?.0; - Some(tip) } /// The nonce of the account that submitted the extrinsic, extracted from the CheckNonce signed extension. /// /// Returns `None` if `nonce` was not found or decoding failed. pub fn nonce(&self) -> Option { - let nonce = self - .iter() - .find_map(|e| e.ok().filter(|e| e.name() == "CheckNonce"))?; - let nonce = Compact::::decode(&mut nonce.bytes()).ok()?.0; + let nonce = self.find::().ok()??.0; Some(nonce) } } /// A single signed extension #[derive(Debug, Clone)] -pub struct ExtrinsicSignedExtension<'a> { +pub struct ExtrinsicSignedExtension<'a, T: Config> { bytes: &'a [u8], ty_id: u32, identifier: &'a str, metadata: &'a Metadata, + _marker: std::marker::PhantomData, } -impl<'a> ExtrinsicSignedExtension<'a> { +impl<'a, T: Config> ExtrinsicSignedExtension<'a, T> { /// The bytes representing this signed extension. pub fn bytes(&self) -> &'a [u8] { self.bytes @@ -709,11 +735,23 @@ impl<'a> ExtrinsicSignedExtension<'a> { /// Signed Extension as a [`scale_value::Value`] pub fn value(&self) -> Result { - let value = - DecodedValue::decode_as_type(&mut &self.bytes[..], self.ty_id, self.metadata.types())?; + self.as_type() + } + /// Decodes the `extra` bytes of this Signed Extension into a static type. + fn as_type(&self) -> Result { + let value = E::decode_as_type(&mut &self.bytes[..], self.ty_id, self.metadata.types())?; Ok(value) } + + /// Decodes the `extra` bytes of this Signed Extension into its associated `Decoded` type. + /// Returns `Ok(None)` if the identitfier of this Signed Extension object does not line up with the `NAME` constant of the provided Signed Extension type. + pub fn as_signed_extra>(&self) -> Result, Error> { + if self.identifier != S::NAME { + return Ok(None); + } + self.as_type::().map(Some) + } } #[cfg(test)] diff --git a/subxt/src/config/mod.rs b/subxt/src/config/mod.rs index c52d1a029a..542ea9874d 100644 --- a/subxt/src/config/mod.rs +++ b/subxt/src/config/mod.rs @@ -17,6 +17,7 @@ pub mod substrate; use codec::{Decode, Encode}; use core::fmt::Debug; +use scale_decode::DecodeAsType; use serde::{de::DeserializeOwned, Serialize}; pub use default_extrinsic_params::{DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder}; @@ -53,7 +54,7 @@ pub trait Config: Sized + Send + Sync + 'static { type ExtrinsicParams: ExtrinsicParams; /// This is used to identify an asset in the `ChargeAssetTxPayment` signed extension. - type AssetId: Debug + Encode; + type AssetId: Debug + Encode + DecodeAsType; } /// given some [`Config`], this return the other params needed for its `ExtrinsicParams`. diff --git a/subxt/src/config/signed_extensions.rs b/subxt/src/config/signed_extensions.rs index 13c87a6a1d..b8a197bca4 100644 --- a/subxt/src/config/signed_extensions.rs +++ b/subxt/src/config/signed_extensions.rs @@ -12,6 +12,9 @@ use crate::utils::Era; use crate::{client::OfflineClientT, Config}; use codec::{Compact, Encode}; use core::fmt::Debug; + +use scale_decode::DecodeAsType; + use std::collections::HashMap; /// A single [`SignedExtension`] has a unique name, but is otherwise the @@ -21,6 +24,11 @@ pub trait SignedExtension: ExtrinsicParams { /// The name of the signed extension. This is used to associate it /// with the signed extensions that the node is making use of. const NAME: &'static str; + + /// The type representing the `extra` bytes of a signed extension. + /// Decoding from this type should be symmetrical to the respective + /// `ExtrinsicParamsEncoder::encode_extra_to()` implementation of this signed extension. + type Decoded: DecodeAsType; } /// The [`CheckSpecVersion`] signed extension. @@ -48,6 +56,7 @@ impl ExtrinsicParamsEncoder for CheckSpecVersion { impl SignedExtension for CheckSpecVersion { const NAME: &'static str = "CheckSpecVersion"; + type Decoded = (); } /// The [`CheckNonce`] signed extension. @@ -75,6 +84,7 @@ impl ExtrinsicParamsEncoder for CheckNonce { impl SignedExtension for CheckNonce { const NAME: &'static str = "CheckNonce"; + type Decoded = Compact; } /// The [`CheckTxVersion`] signed extension. @@ -102,6 +112,7 @@ impl ExtrinsicParamsEncoder for CheckTxVersion { impl SignedExtension for CheckTxVersion { const NAME: &'static str = "CheckTxVersion"; + type Decoded = (); } /// The [`CheckGenesis`] signed extension. @@ -134,6 +145,7 @@ impl ExtrinsicParamsEncoder for CheckGenesis { impl SignedExtension for CheckGenesis { const NAME: &'static str = "CheckGenesis"; + type Decoded = (); } /// The [`CheckMortality`] signed extension. @@ -213,15 +225,29 @@ impl ExtrinsicParamsEncoder for CheckMortality { impl SignedExtension for CheckMortality { const NAME: &'static str = "CheckMortality"; + type Decoded = Era; } /// The [`ChargeAssetTxPayment`] signed extension. -#[derive(Debug)] +#[derive(Debug, DecodeAsType)] +#[decode_as_type(trait_bounds = "T::AssetId: DecodeAsType")] pub struct ChargeAssetTxPayment { tip: Compact, asset_id: Option, } +impl ChargeAssetTxPayment { + /// Tip to the extrinsic author in the native chain token. + pub fn tip(&self) -> u128 { + self.tip.0 + } + + /// Tip to the extrinsic author using the asset ID given. + pub fn asset_id(&self) -> Option<&T::AssetId> { + self.asset_id.as_ref() + } +} + /// Parameters to configure the [`ChargeAssetTxPayment`] signed extension. pub struct ChargeAssetTxPaymentParams { tip: u128, @@ -285,14 +311,22 @@ impl ExtrinsicParamsEncoder for ChargeAssetTxPayment { impl SignedExtension for ChargeAssetTxPayment { const NAME: &'static str = "ChargeAssetTxPayment"; + type Decoded = Self; } /// The [`ChargeTransactionPayment`] signed extension. -#[derive(Debug)] +#[derive(Debug, DecodeAsType)] pub struct ChargeTransactionPayment { tip: Compact, } +impl ChargeTransactionPayment { + /// Tip to the extrinsic author in the native chain token. + pub fn tip(&self) -> u128 { + self.tip.0 + } +} + /// Parameters to configure the [`ChargeTransactionPayment`] signed extension. #[derive(Default)] pub struct ChargeTransactionPaymentParams { @@ -333,6 +367,7 @@ impl ExtrinsicParamsEncoder for ChargeTransactionPayment { impl SignedExtension for ChargeTransactionPayment { const NAME: &'static str = "ChargeTransactionPayment"; + type Decoded = Self; } /// This accepts a tuple of [`SignedExtension`]s, and will dynamically make use of whichever diff --git a/subxt/src/utils/era.rs b/subxt/src/utils/era.rs index 63bede4432..6bea303de4 100644 --- a/subxt/src/utils/era.rs +++ b/subxt/src/utils/era.rs @@ -2,9 +2,13 @@ // This file is dual-licensed as Apache-2.0 or GPL-3.0. // see LICENSE for license details. +use scale_decode::DecodeAsType; + // Dev note: This and related bits taken from `sp_runtime::generic::Era` /// An era to describe the longevity of a transaction. -#[derive(PartialEq, Default, Eq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] +#[derive( + PartialEq, Default, Eq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize, DecodeAsType, +)] pub enum Era { /// The transaction is valid forever. The genesis hash must be present in the signed content. #[default] diff --git a/testing/integration-tests/src/full_client/blocks/mod.rs b/testing/integration-tests/src/full_client/blocks/mod.rs index ae2373f035..53c6d4d9a2 100644 --- a/testing/integration-tests/src/full_client/blocks/mod.rs +++ b/testing/integration-tests/src/full_client/blocks/mod.rs @@ -5,8 +5,10 @@ use crate::{test_context, utils::node_runtime}; use codec::{Compact, Encode}; use futures::StreamExt; - +use subxt::config::signed_extensions::{ChargeAssetTxPayment, CheckMortality, CheckNonce}; use subxt::config::DefaultExtrinsicParamsBuilder; +use subxt::config::SubstrateConfig; +use subxt::utils::Era; use subxt_metadata::Metadata; use subxt_signer::sr25519::dev; @@ -272,38 +274,64 @@ async fn decode_signed_extensions_from_blocks() { }}; } - let expected_signed_extensions = [ - "CheckNonZeroSender", - "CheckSpecVersion", - "CheckTxVersion", - "CheckGenesis", - "CheckMortality", - "CheckNonce", - "CheckWeight", - "ChargeAssetTxPayment", - ]; - let transaction1 = submit_transfer_extrinsic_and_get_it_back!(1234); let extensions1 = transaction1.signed_extensions().unwrap(); let nonce1 = extensions1.nonce().unwrap(); + let nonce1_static = extensions1.find::().unwrap().unwrap().0; let tip1 = extensions1.tip().unwrap(); + let tip1_static: u128 = extensions1 + .find::>() + .unwrap() + .unwrap() + .tip(); let transaction2 = submit_transfer_extrinsic_and_get_it_back!(5678); let extensions2 = transaction2.signed_extensions().unwrap(); let nonce2 = extensions2.nonce().unwrap(); + let nonce2_static = extensions2.find::().unwrap().unwrap().0; let tip2 = extensions2.tip().unwrap(); + let tip2_static: u128 = extensions2 + .find::>() + .unwrap() + .unwrap() + .tip(); assert_eq!(nonce1, 0); + assert_eq!(nonce1, nonce1_static); assert_eq!(tip1, 1234); + assert_eq!(tip1, tip1_static); assert_eq!(nonce2, 1); + assert_eq!(nonce2, nonce2_static); assert_eq!(tip2, 5678); + assert_eq!(tip2, tip2_static); + + let expected_signed_extensions = [ + "CheckNonZeroSender", + "CheckSpecVersion", + "CheckTxVersion", + "CheckGenesis", + "CheckMortality", + "CheckNonce", + "CheckWeight", + "ChargeAssetTxPayment", + ]; assert_eq!(extensions1.iter().count(), expected_signed_extensions.len()); for (e, expected_name) in extensions1.iter().zip(expected_signed_extensions.iter()) { assert_eq!(e.unwrap().name(), *expected_name); } + assert_eq!(extensions2.iter().count(), expected_signed_extensions.len()); for (e, expected_name) in extensions2.iter().zip(expected_signed_extensions.iter()) { assert_eq!(e.unwrap().name(), *expected_name); } + + // check that era decodes: + for extensions in [&extensions1, &extensions2] { + let era: Era = extensions + .find::>() + .unwrap() + .unwrap(); + assert_eq!(era, Era::Immortal) + } }