config: Add SkipCheckIfFeeless signed extension (#1264)

* config: Add `SkipCheckIfFeeless` signed extension

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* config: Add extra extension to the default params

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* examples: Adjust signed extension example

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* config: Extend SkipCheckIfFeeless with inner signed extension

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* config: Configure SkipCheck with inner signed extension params

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* config: Implement Deafult for SkipCheckIfFeelessParams with Option

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* examples: Fix example with proper extension

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* config: Extend <T as Config>::AssetId with EncodeAsType and Clone

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* config: Add SkipCheck with AssetTx

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* config: Encode as type from metadata the inner signed extensions

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Adjust examples

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* blocks: Use `SkipCheckIfFeeless` for decoding the tip of extensions

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* config: Decode `SkipCheckIfFeeless` with `Self`

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* tests: Adjust testing

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* config: Descriptive errors for building `SkipCheckIfFeeless`

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* config: Add docs for extra error types

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Add extra derives to signed extensions

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* config: Use `Default::default` to simplify type init

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

---------

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>
This commit is contained in:
Alexandru Vasile
2023-11-16 16:22:18 +02:00
committed by GitHub
parent eeacf4b041
commit 25e107cc0c
9 changed files with 301 additions and 19 deletions
+13 -1
View File
@@ -3,7 +3,19 @@ use subxt::client::OfflineClientT;
use subxt::config::{Config, ExtrinsicParams, ExtrinsicParamsEncoder};
use subxt_signer::sr25519::dev;
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale")]
#[subxt::subxt(
runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
derive_for_type(path = "xcm::v2::multilocation::MultiLocation", derive = "Clone"),
derive_for_type(path = "xcm::v2::multilocation::Junctions", derive = "Clone"),
derive_for_type(path = "xcm::v2::junction::Junction", derive = "Clone"),
derive_for_type(path = "xcm::v2::NetworkId", derive = "Clone"),
derive_for_type(path = "xcm::v2::BodyId", derive = "Clone"),
derive_for_type(path = "xcm::v2::BodyPart", derive = "Clone"),
derive_for_type(
path = "bounded_collections::weak_bounded_vec::WeakBoundedVec",
derive = "Clone"
)
)]
pub mod runtime {}
use runtime::runtime_types::xcm::v2::multilocation::MultiLocation;
@@ -1,4 +1,5 @@
use codec::Encode;
use scale_encode::EncodeAsType;
use subxt::client::OfflineClientT;
use subxt::config::signed_extensions;
use subxt::config::{
@@ -11,6 +12,7 @@ pub mod runtime {}
// We don't need to construct this at runtime,
// so an empty enum is appropriate:
#[derive(EncodeAsType)]
pub enum CustomConfig {}
impl Config for CustomConfig {
@@ -32,6 +34,10 @@ impl Config for CustomConfig {
signed_extensions::CheckMortality<Self>,
signed_extensions::ChargeAssetTxPayment<Self>,
signed_extensions::ChargeTransactionPayment,
signed_extensions::SkipCheckIfFeeless<
Self,
signed_extensions::ChargeAssetTxPayment<Self>,
>,
// And add a new one of our own:
CustomSignedExtension,
),
@@ -81,8 +87,8 @@ impl ExtrinsicParamsEncoder for CustomSignedExtension {
pub fn custom(
params: DefaultExtrinsicParamsBuilder<CustomConfig>,
) -> <<CustomConfig as Config>::ExtrinsicParams as ExtrinsicParams<CustomConfig>>::OtherParams {
let (a, b, c, d, e, f, g) = params.build();
(a, b, c, d, e, f, g, ())
let (a, b, c, d, e, f, g, h) = params.build();
(a, b, c, d, e, f, g, h, ())
}
#[tokio::main]
+8 -2
View File
@@ -13,7 +13,7 @@ use crate::{
};
use crate::config::signed_extensions::{
ChargeAssetTxPayment, ChargeTransactionPayment, CheckNonce,
ChargeAssetTxPayment, ChargeTransactionPayment, CheckNonce, SkipCheckIfFeeless,
};
use crate::config::SignedExtension;
use crate::dynamic::DecodedValue;
@@ -685,7 +685,7 @@ impl<'a, T: Config> ExtrinsicSignedExtensions<'a, T> {
///
/// Returns `None` if `tip` was not found or decoding failed.
pub fn tip(&self) -> Option<u128> {
// Note: the overhead of iterating twice should be negligible.
// Note: the overhead of iterating multiple time should be negligible.
self.find::<ChargeTransactionPayment>()
.ok()
.flatten()
@@ -696,6 +696,12 @@ impl<'a, T: Config> ExtrinsicSignedExtensions<'a, T> {
.flatten()
.map(|e| e.tip())
})
.or_else(|| {
self.find::<SkipCheckIfFeeless<T, ChargeAssetTxPayment<T>>>()
.ok()
.flatten()
.map(|skip_check| skip_check.inner_signed_extension().tip())
})
}
/// The nonce of the account that submitted the extrinsic, extracted from the CheckNonce signed extension.
@@ -17,6 +17,7 @@ pub type DefaultExtrinsicParams<T> = signed_extensions::AnyOf<
signed_extensions::CheckMortality<T>,
signed_extensions::ChargeAssetTxPayment<T>,
signed_extensions::ChargeTransactionPayment,
signed_extensions::SkipCheckIfFeeless<T, signed_extensions::ChargeAssetTxPayment<T>>,
),
>;
@@ -131,6 +132,9 @@ impl<T: Config> DefaultExtrinsicParamsBuilder<T> {
let charge_transaction_params =
signed_extensions::ChargeTransactionPaymentParams::tip(self.tip);
let skip_check_params =
signed_extensions::SkipCheckIfFeelessParams::from(charge_asset_tx_params.clone());
(
(),
(),
@@ -139,6 +143,7 @@ impl<T: Config> DefaultExtrinsicParamsBuilder<T> {
check_mortality_params,
charge_asset_tx_params,
charge_transaction_params,
skip_check_params,
)
}
}
+13 -1
View File
@@ -18,7 +18,19 @@ pub enum ExtrinsicParamsError {
/// A signed extension was encountered that we don't know about.
#[error("Error constructing extrinsic parameters: Unknown signed extension '{0}'")]
UnknownSignedExtension(String),
/// Some custom error.
/// Cannot find the type id of a signed extension in the metadata.
#[error("Cannot find extension's '{0}' type id '{1} in the metadata")]
MissingTypeId(String, u32),
/// User provided a different signed extension than the one expected.
#[error("Provided a different signed extension for '{0}', the metadata expect '{1}'")]
ExpectedAnotherExtension(String, String),
/// The inner type of a signed extension is not present in the metadata.
#[error("The inner type of the signed extension '{0}' is not present in the metadata")]
MissingInnerSignedExtension(String),
/// The inner type of the signed extension is not named.
#[error("The signed extension's '{0}' type id '{1}' does not have a name in the metadata")]
ExpectedNamedTypeId(String, u32),
/// Some custom error.s
#[error("Error constructing extrinsic parameters: {0}")]
Custom(CustomError),
}
+2 -1
View File
@@ -18,6 +18,7 @@ pub mod substrate;
use codec::{Decode, Encode};
use core::fmt::Debug;
use scale_decode::DecodeAsType;
use scale_encode::EncodeAsType;
use serde::{de::DeserializeOwned, Serialize};
pub use default_extrinsic_params::{DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder};
@@ -54,7 +55,7 @@ pub trait Config: Sized + Send + Sync + 'static {
type ExtrinsicParams: ExtrinsicParams<Self>;
/// This is used to identify an asset in the `ChargeAssetTxPayment` signed extension.
type AssetId: Debug + Encode + DecodeAsType;
type AssetId: Debug + Clone + Encode + DecodeAsType + EncodeAsType;
}
/// given some [`Config`], this return the other params needed for its `ExtrinsicParams`.
+232 -7
View File
@@ -9,11 +9,12 @@
use super::extrinsic_params::{ExtrinsicParams, ExtrinsicParamsEncoder, ExtrinsicParamsError};
use crate::utils::Era;
use crate::{client::OfflineClientT, Config};
use crate::{client::OfflineClientT, Config, Metadata};
use codec::{Compact, Encode};
use core::fmt::Debug;
use scale_decode::DecodeAsType;
use scale_encode::EncodeAsType;
use std::marker::PhantomData;
use std::collections::HashMap;
@@ -32,7 +33,7 @@ pub trait SignedExtension<T: Config>: ExtrinsicParams<T> {
}
/// The [`CheckSpecVersion`] signed extension.
#[derive(Debug)]
#[derive(Clone, Debug, EncodeAsType, DecodeAsType)]
pub struct CheckSpecVersion(u32);
impl<T: Config> ExtrinsicParams<T> for CheckSpecVersion {
@@ -60,7 +61,7 @@ impl<T: Config> SignedExtension<T> for CheckSpecVersion {
}
/// The [`CheckNonce`] signed extension.
#[derive(Debug)]
#[derive(Clone, Debug, EncodeAsType, DecodeAsType)]
pub struct CheckNonce(Compact<u64>);
impl<T: Config> ExtrinsicParams<T> for CheckNonce {
@@ -88,7 +89,7 @@ impl<T: Config> SignedExtension<T> for CheckNonce {
}
/// The [`CheckTxVersion`] signed extension.
#[derive(Debug)]
#[derive(Clone, Debug, EncodeAsType, DecodeAsType)]
pub struct CheckTxVersion(u32);
impl<T: Config> ExtrinsicParams<T> for CheckTxVersion {
@@ -116,6 +117,9 @@ impl<T: Config> SignedExtension<T> for CheckTxVersion {
}
/// The [`CheckGenesis`] signed extension.
#[derive(Clone, EncodeAsType, DecodeAsType)]
#[decode_as_type(trait_bounds = "T::Hash: DecodeAsType")]
#[encode_as_type(trait_bounds = "T::Hash: EncodeAsType")]
pub struct CheckGenesis<T: Config>(T::Hash);
impl<T: Config> std::fmt::Debug for CheckGenesis<T> {
@@ -149,12 +153,16 @@ impl<T: Config> SignedExtension<T> for CheckGenesis<T> {
}
/// The [`CheckMortality`] signed extension.
#[derive(Clone, EncodeAsType, DecodeAsType)]
#[decode_as_type(trait_bounds = "T::Hash: DecodeAsType")]
#[encode_as_type(trait_bounds = "T::Hash: EncodeAsType")]
pub struct CheckMortality<T: Config> {
era: Era,
checkpoint: T::Hash,
}
/// Parameters to configure the [`CheckMortality`] signed extension.
#[derive(Clone, Debug)]
pub struct CheckMortalityParams<T: Config> {
era: Era,
checkpoint: Option<T::Hash>,
@@ -229,8 +237,9 @@ impl<T: Config> SignedExtension<T> for CheckMortality<T> {
}
/// The [`ChargeAssetTxPayment`] signed extension.
#[derive(Debug, DecodeAsType)]
#[derive(Clone, Debug, DecodeAsType, EncodeAsType)]
#[decode_as_type(trait_bounds = "T::AssetId: DecodeAsType")]
#[encode_as_type(trait_bounds = "T::AssetId: EncodeAsType")]
pub struct ChargeAssetTxPayment<T: Config> {
tip: Compact<u128>,
asset_id: Option<T::AssetId>,
@@ -249,11 +258,22 @@ impl<T: Config> ChargeAssetTxPayment<T> {
}
/// Parameters to configure the [`ChargeAssetTxPayment`] signed extension.
#[derive(Debug)]
pub struct ChargeAssetTxPaymentParams<T: Config> {
tip: u128,
asset_id: Option<T::AssetId>,
}
// Dev note: `#[derive(Clone)]` implies `T: Clone` instead of `T::AssetId: Clone`.
impl<T: Config> Clone for ChargeAssetTxPaymentParams<T> {
fn clone(&self) -> Self {
Self {
tip: self.tip,
asset_id: self.asset_id.clone(),
}
}
}
impl<T: Config> Default for ChargeAssetTxPaymentParams<T> {
fn default() -> Self {
ChargeAssetTxPaymentParams {
@@ -315,7 +335,7 @@ impl<T: Config> SignedExtension<T> for ChargeAssetTxPayment<T> {
}
/// The [`ChargeTransactionPayment`] signed extension.
#[derive(Debug, DecodeAsType)]
#[derive(Clone, Debug, DecodeAsType, EncodeAsType)]
pub struct ChargeTransactionPayment {
tip: Compact<u128>,
}
@@ -370,6 +390,211 @@ impl<T: Config> SignedExtension<T> for ChargeTransactionPayment {
type Decoded = Self;
}
/// Information needed to encode the [`SkipCheckIfFeeless`] signed extension.
#[derive(Debug)]
struct SkipCheckIfFeelessEncodingData {
metadata: Metadata,
type_id: u32,
}
impl SkipCheckIfFeelessEncodingData {
/// Construct [`SkipCheckIfFeelessEncodingData`].
fn new(
metadata: Metadata,
extension: &str,
inner_extension: &str,
) -> Result<Self, ExtrinsicParamsError> {
let skip_check_type_id = metadata
.extrinsic()
.signed_extensions()
.iter()
.find_map(|ext| {
if ext.identifier() == extension {
Some(ext.extra_ty())
} else {
None
}
});
let Some(skip_check_type_id) = skip_check_type_id else {
return Err(ExtrinsicParamsError::UnknownSignedExtension(
inner_extension.to_owned(),
));
};
// Ensure that the `SkipCheckIfFeeless` type has the same inner signed extension as provided.
let Some(skip_check_ty) = metadata.types().resolve(skip_check_type_id) else {
return Err(ExtrinsicParamsError::MissingTypeId(
inner_extension.to_owned(),
skip_check_type_id,
));
};
// The substrate's `SkipCheckIfFeeless` contains 2 types: the inner signed extension and a phantom data.
// Phantom data does not have a type associated, so we need to find the inner signed extension.
let Some(inner_type_id) = skip_check_ty
.type_params
.iter()
.find_map(|param| param.ty.map(|ty| ty.id))
else {
return Err(ExtrinsicParamsError::MissingInnerSignedExtension(
inner_extension.to_owned(),
));
};
// Get the inner type of the `SkipCheckIfFeeless` extension to check if the naming matches the provided parameters.
let Some(inner_extension_ty) = metadata.types().resolve(inner_type_id) else {
return Err(ExtrinsicParamsError::MissingTypeId(
inner_extension.to_owned(),
inner_type_id,
));
};
let Some(inner_extension_name) = inner_extension_ty.path.segments.last() else {
return Err(ExtrinsicParamsError::ExpectedNamedTypeId(
inner_extension.to_owned(),
inner_type_id,
));
};
if inner_extension_name != inner_extension {
return Err(ExtrinsicParamsError::ExpectedAnotherExtension(
inner_extension.to_owned(),
inner_extension_name.to_owned(),
));
}
Ok(SkipCheckIfFeelessEncodingData {
metadata,
type_id: inner_type_id,
})
}
}
/// The [`SkipCheckIfFeeless`] signed extension.
#[derive(Debug, DecodeAsType, EncodeAsType)]
#[decode_as_type(trait_bounds = "S: DecodeAsType")]
#[encode_as_type(trait_bounds = "S: EncodeAsType")]
pub struct SkipCheckIfFeeless<T, S>
where
T: Config,
S: SignedExtension<T> + DecodeAsType + EncodeAsType,
{
inner: S,
// Dev note: This is `Option` because `#[derive(DecodeAsType)]` requires the
// `Default` bound on skipped parameters.
// This field is populated when the [`SkipCheckIfFeeless`] is constructed from
// [`ExtrinsicParams`] (ie, when subxt submits extrinsics). However, it is not
// populated when decoding signed extensions from the node.
#[decode_as_type(skip)]
#[encode_as_type(skip)]
encoding_data: Option<SkipCheckIfFeelessEncodingData>,
#[decode_as_type(skip)]
#[encode_as_type(skip)]
_phantom: PhantomData<T>,
}
impl<T, S> SkipCheckIfFeeless<T, S>
where
T: Config,
S: SignedExtension<T> + DecodeAsType + EncodeAsType,
{
/// The inner signed extension.
pub fn inner_signed_extension(&self) -> &S {
&self.inner
}
}
impl<T, S> ExtrinsicParams<T> for SkipCheckIfFeeless<T, S>
where
T: Config,
S: SignedExtension<T> + DecodeAsType + EncodeAsType,
<S as ExtrinsicParams<T>>::OtherParams: Default,
{
type OtherParams = SkipCheckIfFeelessParams<T, S>;
type Error = ExtrinsicParamsError;
fn new<Client: OfflineClientT<T>>(
nonce: u64,
client: Client,
other_params: Self::OtherParams,
) -> Result<Self, Self::Error> {
let other_params = other_params.0.unwrap_or_default();
let metadata = client.metadata();
let encoding_data = SkipCheckIfFeelessEncodingData::new(metadata, Self::NAME, S::NAME)?;
let inner_extension = S::new(nonce, client, other_params).map_err(Into::into)?;
Ok(SkipCheckIfFeeless {
inner: inner_extension,
encoding_data: Some(encoding_data),
_phantom: PhantomData,
})
}
}
impl<T, S> ExtrinsicParamsEncoder for SkipCheckIfFeeless<T, S>
where
T: Config,
S: SignedExtension<T> + DecodeAsType + EncodeAsType,
{
fn encode_extra_to(&self, v: &mut Vec<u8>) {
if let Some(encoding_data) = &self.encoding_data {
let _ = self.inner.encode_as_type_to(
encoding_data.type_id,
encoding_data.metadata.types(),
v,
);
}
}
}
impl<T, S> SignedExtension<T> for SkipCheckIfFeeless<T, S>
where
T: Config,
S: SignedExtension<T> + DecodeAsType + EncodeAsType,
<S as ExtrinsicParams<T>>::OtherParams: Default,
{
const NAME: &'static str = "SkipCheckIfFeeless";
type Decoded = Self;
}
/// Parameters to configure the [`SkipCheckIfFeeless`] signed extension.
pub struct SkipCheckIfFeelessParams<T, S>(Option<<S as ExtrinsicParams<T>>::OtherParams>)
where
T: Config,
S: SignedExtension<T>;
impl<T, S> std::fmt::Debug for SkipCheckIfFeelessParams<T, S>
where
T: Config,
S: SignedExtension<T>,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("SkipCheckIfFeelessParams").finish()
}
}
impl<T: Config, S: SignedExtension<T>> Default for SkipCheckIfFeelessParams<T, S>
where
T: Config,
S: SignedExtension<T>,
{
fn default() -> Self {
SkipCheckIfFeelessParams(None)
}
}
impl<T, S> SkipCheckIfFeelessParams<T, S>
where
T: Config,
S: SignedExtension<T>,
{
/// Skip the check if the transaction is feeless.
pub fn from(extrinsic_params: <S as ExtrinsicParams<T>>::OtherParams) -> Self {
SkipCheckIfFeelessParams(Some(extrinsic_params))
}
}
/// This accepts a tuple of [`SignedExtension`]s, and will dynamically make use of whichever
/// ones are actually required for the chain in the correct order, ignoring the rest. This
/// is a sensible default, and allows for a single configuration to work across multiple chains.
+11 -1
View File
@@ -3,11 +3,21 @@
// see LICENSE for license details.
use scale_decode::DecodeAsType;
use scale_encode::EncodeAsType;
// 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, DecodeAsType,
PartialEq,
Default,
Eq,
Clone,
Copy,
Debug,
serde::Serialize,
serde::Deserialize,
DecodeAsType,
EncodeAsType,
)]
pub enum Era {
/// The transaction is valid forever. The genesis hash must be present in the signed content.
@@ -5,7 +5,9 @@
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::signed_extensions::{
ChargeAssetTxPayment, CheckMortality, CheckNonce, SkipCheckIfFeeless,
};
use subxt::config::DefaultExtrinsicParamsBuilder;
use subxt::config::SubstrateConfig;
use subxt::utils::Era;
@@ -276,13 +278,15 @@ async fn decode_signed_extensions_from_blocks() {
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::<CheckNonce>().unwrap().unwrap().0;
let tip1 = extensions1.tip().unwrap();
let tip1_static: u128 = extensions1
.find::<ChargeAssetTxPayment<SubstrateConfig>>()
.find::<SkipCheckIfFeeless<SubstrateConfig, ChargeAssetTxPayment<SubstrateConfig>>>()
.unwrap()
.unwrap()
.inner_signed_extension()
.tip();
let transaction2 = submit_transfer_extrinsic_and_get_it_back!(5678);
@@ -291,9 +295,10 @@ async fn decode_signed_extensions_from_blocks() {
let nonce2_static = extensions2.find::<CheckNonce>().unwrap().unwrap().0;
let tip2 = extensions2.tip().unwrap();
let tip2_static: u128 = extensions2
.find::<ChargeAssetTxPayment<SubstrateConfig>>()
.find::<SkipCheckIfFeeless<SubstrateConfig, ChargeAssetTxPayment<SubstrateConfig>>>()
.unwrap()
.unwrap()
.inner_signed_extension()
.tip();
assert_eq!(nonce1, 0);
@@ -313,7 +318,7 @@ async fn decode_signed_extensions_from_blocks() {
"CheckMortality",
"CheckNonce",
"CheckWeight",
"ChargeAssetTxPayment",
"SkipCheckIfFeeless",
];
assert_eq!(extensions1.iter().count(), expected_signed_extensions.len());