diff --git a/Cargo.lock b/Cargo.lock index 4cedda8314ef6..266bdcbbeb7c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24102,6 +24102,7 @@ dependencies = [ name = "sp-metadata-ir" version = "0.6.0" dependencies = [ + "derive-where", "frame-metadata 23.0.1", "parity-scale-codec", "scale-info", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs index 405f85720d7fc..303593ce831ed 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -1515,9 +1515,10 @@ pub struct EthExtraImpl; impl EthExtra for EthExtraImpl { type Config = Runtime; - type Extension = TxExtension; + type ExtensionV0 = TxExtension; + type ExtensionOtherVersions = sp_runtime::traits::InvalidVersion; - fn get_eth_extension(nonce: u32, tip: Balance) -> Self::Extension { + fn get_eth_extension(nonce: u32, tip: Balance) -> Self::ExtensionV0 { ( frame_system::AuthorizeCall::::new(), frame_system::CheckNonZeroSender::::new(), diff --git a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs index 21dbe58ac884e..ea25a47bdeadc 100644 --- a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs +++ b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs @@ -153,9 +153,10 @@ pub struct EthExtraImpl; impl EthExtra for EthExtraImpl { type Config = Runtime; - type Extension = TxExtension; + type ExtensionV0 = TxExtension; + type ExtensionOtherVersions = sp_runtime::traits::InvalidVersion; - fn get_eth_extension(nonce: u32, tip: Balance) -> Self::Extension { + fn get_eth_extension(nonce: u32, tip: Balance) -> Self::ExtensionV0 { ( frame_system::AuthorizeCall::::new(), frame_system::CheckNonZeroSender::::new(), diff --git a/docs/mermaid/extrinsics.mmd b/docs/mermaid/extrinsics.mmd index 4afd4ab8f755d..06be32ecf9e39 100644 --- a/docs/mermaid/extrinsics.mmd +++ b/docs/mermaid/extrinsics.mmd @@ -1,5 +1,6 @@ flowchart TD - E(Extrinsic) ---> I(Inherent); - E --> T(Transaction) - T --> ST("Signed (aka. Transaction)") - T --> UT(Unsigned) + E(Extrinsic) ---> B(Bare); + E --> S(Signed Transaction) + E --> G(General Transaction) + B --> I(Inherent); + B --> U(Unsigned Transaction (deprecated)) diff --git a/prdoc/pr_7035.prdoc b/prdoc/pr_7035.prdoc new file mode 100644 index 0000000000000..87770c83e0fd0 --- /dev/null +++ b/prdoc/pr_7035.prdoc @@ -0,0 +1,57 @@ +title: Allow declaration and usage of multiple transaction extension versions in FRAME and primitives + +doc: + - audience: Runtime Dev + description: | + This PR enhance `UncheckedExtrinsic` type with a new optional generic: `ExtensionOtherVersions`. + This generic defaults to `InvalidVersion` meaning there is not other version than the regular version 0. This is the same behavior as before this PR. + + # Breaking change + + The types `Preamble`, `CheckedExtrinsic` and `ExtrinsicFormat` also have this new optional generic. Their type definitions also have changed a bit: the `General` variant was 2 fields, the version and the extension, it is now only one field, the extension, and the version can be retrieve by calling `extension.version()` + + Some trait such as `ExtrinsicMetadata` and `EthExtraImpl` changed their associated type named `Extension` to `ExtensionV0` and have new associated type `ExtensionOtherVersions`. This is because multiple version are now supported. + You can always use `InvalidVersion` for `ExtensionOtherVersions` and keep the old `Extension` for `ExtensionV0` to keep the same behavior as before this PR. + + The type inference for those types may fail because of this PR, to update the code by writing the concrete types. + + # New feature + + To use this new feature, you can use the new types `PipelineAtVers` and `MultiVersion` to define a transaction extension with multiple version: + + ```rust + pub type TransactionExtensionV0 = (); + pub type TransactionExtensionV4 = (); + pub type TransactionExtensionV7 = (); + + pub type OtherVersions = MultiVersion< + PipelineAtVers<4, TransactionExtensionV4>; + PipelineAtVers<7, TransactionExtensionV7>; + >; + + pub type UncheckedExtrinsic = generic::UncheckedExtrinsic< + AccountId, + RuntimeCall, + UintAuthorityId, + TransactionExtensionV0, // The version 0, same as before + OtherVersions, // The other versions. + >; + ``` + +crates: + - name: node-testing + bump: major + - name: pallet-revive + bump: major + - name: frame-support-procedural + bump: major + - name: frame-support + bump: major + - name: frame-support-test + bump: major + - name: sp-runtime + bump: major + - name: sp-metadata-ir + bump: major + - name: pallet-transaction-payment + bump: major diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index c819f35a8e764..835b56f82394b 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -2932,9 +2932,10 @@ pub struct EthExtraImpl; impl EthExtra for EthExtraImpl { type Config = Runtime; - type Extension = TxExtension; + type ExtensionV0 = TxExtension; + type ExtensionOtherVersions = sp_runtime::traits::InvalidVersion; - fn get_eth_extension(nonce: u32, tip: Balance) -> Self::Extension { + fn get_eth_extension(nonce: u32, tip: Balance) -> Self::ExtensionV0 { ( frame_system::AuthorizeCall::::new(), frame_system::CheckNonZeroSender::::new(), diff --git a/substrate/bin/node/testing/src/bench.rs b/substrate/bin/node/testing/src/bench.rs index 6602d5a8488ae..da067c2d6cbac 100644 --- a/substrate/bin/node/testing/src/bench.rs +++ b/substrate/bin/node/testing/src/bench.rs @@ -603,12 +603,9 @@ impl BenchKeyring { .into() }, ExtrinsicFormat::Bare => generic::UncheckedExtrinsic::new_bare(xt.function).into(), - ExtrinsicFormat::General(ext_version, tx_ext) => { - generic::UncheckedExtrinsic::from_parts( - xt.function, - Preamble::General(ext_version, tx_ext), - ) - .into() + ExtrinsicFormat::General(tx_ext) => { + generic::UncheckedExtrinsic::from_parts(xt.function, Preamble::General(tx_ext)) + .into() }, } } diff --git a/substrate/bin/node/testing/src/keyring.rs b/substrate/bin/node/testing/src/keyring.rs index 485323bf6132b..e4e3313d703e8 100644 --- a/substrate/bin/node/testing/src/keyring.rs +++ b/substrate/bin/node/testing/src/keyring.rs @@ -131,10 +131,9 @@ pub fn sign( .into() }, ExtrinsicFormat::Bare => generic::UncheckedExtrinsic::new_bare(xt.function).into(), - ExtrinsicFormat::General(ext_version, tx_ext) => generic::UncheckedExtrinsic::from_parts( - xt.function, - generic::Preamble::General(ext_version, tx_ext), - ) - .into(), + ExtrinsicFormat::General(tx_ext) => { + generic::UncheckedExtrinsic::from_parts(xt.function, generic::Preamble::General(tx_ext)) + .into() + }, } } diff --git a/substrate/frame/revive/dev-node/runtime/src/lib.rs b/substrate/frame/revive/dev-node/runtime/src/lib.rs index b4ac9bc93d197..f2a40f2569787 100644 --- a/substrate/frame/revive/dev-node/runtime/src/lib.rs +++ b/substrate/frame/revive/dev-node/runtime/src/lib.rs @@ -199,9 +199,10 @@ pub struct EthExtraImpl; impl EthExtra for EthExtraImpl { type Config = Runtime; - type Extension = TxExtension; + type ExtensionV0 = TxExtension; + type ExtensionOtherVersions = sp_runtime::traits::InvalidVersion; - fn get_eth_extension(nonce: u32, tip: Balance) -> Self::Extension { + fn get_eth_extension(nonce: u32, tip: Balance) -> Self::ExtensionV0 { ( frame_system::CheckNonZeroSender::::new(), frame_system::CheckSpecVersion::::new(), diff --git a/substrate/frame/revive/src/evm/fees.rs b/substrate/frame/revive/src/evm/fees.rs index 8d586d226eeb1..971da73276928 100644 --- a/substrate/frame/revive/src/evm/fees.rs +++ b/substrate/frame/revive/src/evm/fees.rs @@ -213,8 +213,15 @@ where ::RuntimeCall: Dispatchable, CallOf: SetWeightLimit, - <::Block as BlockT>::Extrinsic: - From, Signature, E::Extension>>, + <::Block as BlockT>::Extrinsic: From< + UncheckedExtrinsic< + Address, + CallOf, + Signature, + E::ExtensionV0, + E::ExtensionOtherVersions, + >, + >, <::OnChargeTransaction as TxCreditHold>::Credit: SuppressedDrop>, { diff --git a/substrate/frame/revive/src/evm/runtime.rs b/substrate/frame/revive/src/evm/runtime.rs index ce38c255dd344..337a435abf36d 100644 --- a/substrate/frame/revive/src/evm/runtime.rs +++ b/substrate/frame/revive/src/evm/runtime.rs @@ -39,7 +39,7 @@ use sp_runtime::{ Debug, OpaqueExtrinsic, Weight, generic::{self, CheckedExtrinsic, ExtrinsicFormat}, traits::{ - Checkable, ExtrinsicCall, ExtrinsicLike, ExtrinsicMetadata, LazyExtrinsic, + Checkable, ExtrinsicCall, ExtrinsicLike, ExtrinsicMetadata, LazyExtrinsic, Pipeline, TransactionExtension, }, transaction_validity::{InvalidTransaction, TransactionValidityError}, @@ -57,28 +57,58 @@ pub trait SetWeightLimit { /// [`crate::Call::eth_transact`] extrinsic. #[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug)] pub struct UncheckedExtrinsic( - pub generic::UncheckedExtrinsic, Signature, E::Extension>, + pub generic::UncheckedExtrinsic< + Address, + CallOf, + Signature, + E::ExtensionV0, + E::ExtensionOtherVersions, + >, ); impl TypeInfo for UncheckedExtrinsic where Address: StaticTypeInfo, Signature: StaticTypeInfo, - E::Extension: StaticTypeInfo, + E::ExtensionV0: StaticTypeInfo, { - type Identity = - generic::UncheckedExtrinsic, Signature, E::Extension>; + type Identity = generic::UncheckedExtrinsic< + Address, + CallOf, + Signature, + E::ExtensionV0, + E::ExtensionOtherVersions, + >; fn type_info() -> scale_info::Type { - generic::UncheckedExtrinsic::, Signature, E::Extension>::type_info() + generic::UncheckedExtrinsic::< + Address, + CallOf, + Signature, + E::ExtensionV0, + E::ExtensionOtherVersions, + >::type_info() } } impl - From, Signature, E::Extension>> - for UncheckedExtrinsic + From< + generic::UncheckedExtrinsic< + Address, + CallOf, + Signature, + E::ExtensionV0, + E::ExtensionOtherVersions, + >, + > for UncheckedExtrinsic { fn from( - utx: generic::UncheckedExtrinsic, Signature, E::Extension>, + utx: generic::UncheckedExtrinsic< + Address, + CallOf, + Signature, + E::ExtensionV0, + E::ExtensionOtherVersions, + >, ) -> Self { Self(utx) } @@ -99,9 +129,16 @@ impl ExtrinsicMetadata Address, CallOf, Signature, - E::Extension, + E::ExtensionV0, + E::ExtensionOtherVersions, >::VERSIONS; - type TransactionExtensions = E::Extension; + type TransactionExtensionPipelines = , + Signature, + E::ExtensionV0, + E::ExtensionOtherVersions, + > as ExtrinsicMetadata>::TransactionExtensionPipelines; } impl ExtrinsicCall @@ -126,13 +163,28 @@ where ::Nonce: TryFrom, CallOf: SetWeightLimit, // required by Checkable for `generic::UncheckedExtrinsic` - generic::UncheckedExtrinsic, Signature, E::Extension>: - Checkable< - Lookup, - Checked = CheckedExtrinsic, CallOf, E::Extension>, + generic::UncheckedExtrinsic< + LookupSource, + CallOf, + Signature, + E::ExtensionV0, + E::ExtensionOtherVersions, + >: Checkable< + Lookup, + Checked = CheckedExtrinsic< + AccountIdOf, + CallOf, + E::ExtensionV0, + E::ExtensionOtherVersions, >, + >, { - type Checked = CheckedExtrinsic, CallOf, E::Extension>; + type Checked = CheckedExtrinsic< + AccountIdOf, + CallOf, + E::ExtensionV0, + E::ExtensionOtherVersions, + >; fn check(self, lookup: &Lookup) -> Result { if !self.0.is_signed() { @@ -195,17 +247,17 @@ impl SignedTransactionBuilder where Address: TypeInfo, Signature: TypeInfo, - E::Extension: TypeInfo, + E::ExtensionV0: TypeInfo, { type Address = Address; type Signature = Signature; - type Extension = E::Extension; + type Extension = E::ExtensionV0; fn new_signed_transaction( call: Self::Call, signed: Address, signature: Signature, - tx_ext: E::Extension, + tx_ext: E::ExtensionV0, ) -> Self { generic::UncheckedExtrinsic::new_signed(call, signed, signature, tx_ext).into() } @@ -215,7 +267,7 @@ impl InherentBuilder for UncheckedExtrinsic Self { generic::UncheckedExtrinsic::new_bare(call).into() @@ -227,7 +279,7 @@ impl From) -> Self { extrinsic.0.into() @@ -236,7 +288,13 @@ where impl LazyExtrinsic for UncheckedExtrinsic where - generic::UncheckedExtrinsic, Signature, E::Extension>: LazyExtrinsic, + generic::UncheckedExtrinsic< + Address, + CallOf, + Signature, + E::ExtensionV0, + E::ExtensionOtherVersions, + >: LazyExtrinsic, { fn decode_unprefixed(data: &[u8]) -> Result { Ok(Self(LazyExtrinsic::decode_unprefixed(data)?)) @@ -248,11 +306,16 @@ pub trait EthExtra { /// The Runtime configuration. type Config: Config + TxConfig; - /// The Runtime's transaction extension. + /// The Runtime's transaction extension version 0. /// It should include at least: /// - [`frame_system::CheckNonce`] to ensure that the nonce from the Ethereum transaction is /// correct. - type Extension: TransactionExtension>; + type ExtensionV0: TransactionExtension>; + + /// The Runtime's transaction extension versions other than 0. + /// + /// Use [`sp_runtime::traits::InvalidVersion`] if no other versions should be supported. + type ExtensionOtherVersions: Pipeline>; /// Get the transaction extension to apply to an unsigned [`crate::Call::eth_transact`] /// extrinsic. @@ -263,7 +326,7 @@ pub trait EthExtra { fn get_eth_extension( nonce: ::Nonce, tip: BalanceOf, - ) -> Self::Extension; + ) -> Self::ExtensionV0; /// Convert the unsigned [`crate::Call::eth_transact`] into a [`CheckedExtrinsic`]. /// and ensure that the fees from the Ethereum transaction correspond to the fees computed from @@ -276,7 +339,12 @@ pub trait EthExtra { payload: &[u8], encoded_len: usize, ) -> Result< - CheckedExtrinsic, CallOf, Self::Extension>, + CheckedExtrinsic< + AccountIdOf, + CallOf, + Self::ExtensionV0, + Self::ExtensionOtherVersions, + >, InvalidTransaction, > where diff --git a/substrate/frame/revive/src/tests.rs b/substrate/frame/revive/src/tests.rs index 0c8cbcdb5a1e0..584c5b22eaaad 100644 --- a/substrate/frame/revive/src/tests.rs +++ b/substrate/frame/revive/src/tests.rs @@ -71,9 +71,10 @@ pub struct EthExtraImpl; impl EthExtra for EthExtraImpl { type Config = Test; - type Extension = SignedExtra; + type ExtensionV0 = SignedExtra; + type ExtensionOtherVersions = sp_runtime::traits::InvalidVersion; - fn get_eth_extension(nonce: u32, tip: BalanceOf) -> Self::Extension { + fn get_eth_extension(nonce: u32, tip: BalanceOf) -> Self::ExtensionV0 { ( frame_system::CheckNonce::from(nonce), ChargeTransactionPayment::from(tip), diff --git a/substrate/frame/support/procedural/src/construct_runtime/expand/metadata.rs b/substrate/frame/support/procedural/src/construct_runtime/expand/metadata.rs index 054b9d9da3306..3afaf0b961f53 100644 --- a/substrate/frame/support/procedural/src/construct_runtime/expand/metadata.rs +++ b/substrate/frame/support/procedural/src/construct_runtime/expand/metadata.rs @@ -110,6 +110,20 @@ pub fn expand_runtime_metadata( use #scrate::__private::metadata_ir::InternalImplRuntimeApis; + + let mut versioned_extensions_metadata = + #scrate::sp_runtime::traits::PipelineMetadataBuilder::new(); + + < + < + #extrinsic as #scrate::sp_runtime::traits::ExtrinsicMetadata + >::TransactionExtensionPipelines + as + #scrate::sp_runtime::traits::Pipeline::< + <#runtime as #system_path::Config>::RuntimeCall + > + >::build_metadata(&mut versioned_extensions_metadata); + #scrate::__private::metadata_ir::MetadataIR { pallets: #scrate::__private::vec![ #(#pallets),* ], extrinsic: #scrate::__private::metadata_ir::ExtrinsicMetadataIR { @@ -119,15 +133,8 @@ pub fn expand_runtime_metadata( call_ty, signature_ty, extra_ty, - extensions: < - < - #extrinsic as #scrate::sp_runtime::traits::ExtrinsicMetadata - >::TransactionExtensions - as - #scrate::sp_runtime::traits::TransactionExtension::< - <#runtime as #system_path::Config>::RuntimeCall - > - >::metadata() + extensions_by_version: versioned_extensions_metadata.by_version, + extensions_in_versions: versioned_extensions_metadata.in_versions .into_iter() .map(|meta| #scrate::__private::metadata_ir::TransactionExtensionMetadataIR { identifier: meta.identifier, diff --git a/substrate/frame/support/src/dispatch.rs b/substrate/frame/support/src/dispatch.rs index e0febaa21ec07..0f86b4cb71d78 100644 --- a/substrate/frame/support/src/dispatch.rs +++ b/substrate/frame/support/src/dispatch.rs @@ -407,10 +407,13 @@ where } /// Implementation for unchecked extrinsic. -impl> GetDispatchInfo - for UncheckedExtrinsic +impl GetDispatchInfo + for UncheckedExtrinsic where - Call: GetDispatchInfo + Dispatchable, + Call: GetDispatchInfo + Dispatchable + Encode, + ExtensionV0: TransactionExtension, + ExtensionOtherVersions: sp_runtime::traits::Pipeline, + ::RuntimeOrigin: sp_runtime::traits::AsTransactionAuthorizedOrigin, { fn get_dispatch_info(&self) -> DispatchInfo { let mut info = self.function.get_dispatch_info(); @@ -420,10 +423,13 @@ where } /// Implementation for checked extrinsic. -impl> GetDispatchInfo - for CheckedExtrinsic +impl GetDispatchInfo + for CheckedExtrinsic where - Call: GetDispatchInfo, + Call: GetDispatchInfo + Dispatchable + Encode, + ExtensionV0: TransactionExtension, + ExtensionOtherVersions: sp_runtime::traits::Pipeline, + ::RuntimeOrigin: sp_runtime::traits::AsTransactionAuthorizedOrigin, { fn get_dispatch_info(&self) -> DispatchInfo { let mut info = self.function.get_dispatch_info(); @@ -1563,7 +1569,7 @@ mod extension_weight_tests { // First testcase let ext: TxExtension = (HalfCostIf(false), FreeIfUnder(2000), ActualWeightIs(0)); let xt = CheckedExtrinsic { - format: ExtrinsicFormat::Signed(0, ext.clone()), + format: ExtrinsicFormat::<_, _>::Signed(0, ext.clone()), function: call.clone(), }; assert_eq!(xt.extension_weight(), Weight::from_parts(600, 0)); @@ -1578,7 +1584,7 @@ mod extension_weight_tests { // Second testcase let ext: TxExtension = (HalfCostIf(false), FreeIfUnder(1100), ActualWeightIs(200)); let xt = CheckedExtrinsic { - format: ExtrinsicFormat::Signed(0, ext), + format: ExtrinsicFormat::<_, _>::Signed(0, ext), function: call.clone(), }; let post_info = xt.apply::(&info, 0).unwrap().unwrap(); @@ -1588,7 +1594,7 @@ mod extension_weight_tests { // Third testcase let ext: TxExtension = (HalfCostIf(true), FreeIfUnder(1060), ActualWeightIs(200)); let xt = CheckedExtrinsic { - format: ExtrinsicFormat::Signed(0, ext), + format: ExtrinsicFormat::<_, _>::Signed(0, ext), function: call.clone(), }; let post_info = xt.apply::(&info, 0).unwrap().unwrap(); @@ -1598,7 +1604,7 @@ mod extension_weight_tests { // Fourth testcase let ext: TxExtension = (HalfCostIf(false), FreeIfUnder(100), ActualWeightIs(300)); let xt = CheckedExtrinsic { - format: ExtrinsicFormat::Signed(0, ext), + format: ExtrinsicFormat::<_, _>::Signed(0, ext), function: call.clone(), }; let post_info = xt.apply::(&info, 0).unwrap().unwrap(); diff --git a/substrate/frame/support/src/traits/misc.rs b/substrate/frame/support/src/traits/misc.rs index 3b808ec5cdf18..99fc3c44800f6 100644 --- a/substrate/frame/support/src/traits/misc.rs +++ b/substrate/frame/support/src/traits/misc.rs @@ -948,13 +948,14 @@ pub trait InherentBuilder: ExtrinsicCall { fn new_inherent(call: Self::Call) -> Self; } -impl InherentBuilder - for sp_runtime::generic::UncheckedExtrinsic -where - Address: TypeInfo, - Call: TypeInfo, - Signature: TypeInfo, - Extra: TypeInfo, +impl InherentBuilder + for sp_runtime::generic::UncheckedExtrinsic< + Address, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + > { fn new_inherent(call: Self::Call) -> Self { Self::new_bare(call) @@ -977,23 +978,24 @@ pub trait SignedTransactionBuilder: ExtrinsicCall { ) -> Self; } -impl SignedTransactionBuilder - for sp_runtime::generic::UncheckedExtrinsic -where - Address: TypeInfo, - Call: TypeInfo, - Signature: TypeInfo, - Extension: TypeInfo, +impl SignedTransactionBuilder + for sp_runtime::generic::UncheckedExtrinsic< + Address, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + > { type Address = Address; type Signature = Signature; - type Extension = Extension; + type Extension = ExtensionV0; fn new_signed_transaction( call: Self::Call, signed: Address, signature: Signature, - tx_ext: Extension, + tx_ext: ExtensionV0, ) -> Self { Self::new_signed(call, signed, signature, tx_ext) } diff --git a/substrate/frame/support/test/tests/tx_ext_multi_version.rs b/substrate/frame/support/test/tests/tx_ext_multi_version.rs new file mode 100644 index 0000000000000..bde7935002b56 --- /dev/null +++ b/substrate/frame/support/test/tests/tx_ext_multi_version.rs @@ -0,0 +1,239 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test that FRAME support multiple versions for transaction extensions. + +use codec::{Decode, DecodeWithMemTracking, Encode}; +use core::fmt::Debug; +use frame_support::{ + derive_impl, + dispatch::{DispatchInfo, GetDispatchInfo, PostDispatchInfo}, + pallet_prelude::Weight, +}; +use scale_info::TypeInfo; +use sp_runtime::{ + generic, + generic::Preamble, + testing::UintAuthorityId, + traits::{ + Applyable, BlakeTwo256, Checkable, ExtensionVariant, IdentityLookup, TransactionExtension, + }, + transaction_validity::{ + InvalidTransaction, TransactionSource, TransactionValidityError, ValidTransaction, + }, + DispatchResult, +}; + +// This transaction extension is valid iff `token != 0`, when valid the resulting origin is +// `frame_system::Origin::Signed(100)`. +#[derive(Clone, Debug, Encode, Decode, DecodeWithMemTracking, PartialEq, Eq, TypeInfo)] +pub struct SimpleExt { + pub token: u8, +} + +const SIMPLE_EXT_IDS: [&str; 8] = [ + "SimpleExt0", + "SimpleExt1", + "SimpleExt2", + "SimpleExt3", + "SimpleExt4", + "SimpleExt5", + "SimpleExt6", + "SimpleExt7", +]; + +impl TransactionExtension for SimpleExt { + const IDENTIFIER: &'static str = SIMPLE_EXT_IDS[N as usize]; + type Implicit = (); + type Val = (); + type Pre = (); + + fn implicit(&self) -> Result { + Ok(()) + } + + fn weight(&self, _call: &RuntimeCall) -> Weight { + Weight::from_parts(N as u64 + 10, 0) + } + + fn validate( + &self, + _origin: RuntimeOrigin, + _call: &RuntimeCall, + _info: &DispatchInfo, + _len: usize, + _self_implicit: Self::Implicit, + _inherited_implication: &impl sp_runtime::traits::Implication, + _source: TransactionSource, + ) -> sp_runtime::traits::ValidateResult { + if self.token == 0 { + return Err(InvalidTransaction::Custom(N as u8).into()); + } + Ok((ValidTransaction::default(), (), frame_system::Origin::::Signed(100).into())) + } + + fn prepare( + self, + _val: Self::Val, + _origin: &RuntimeOrigin, + _call: &RuntimeCall, + _info: &DispatchInfo, + _len: usize, + ) -> Result { + Ok(()) + } + + fn post_dispatch_details( + _pre: Self::Pre, + _info: &DispatchInfo, + _post_info: &PostDispatchInfo, + _len: usize, + _result: &DispatchResult, + ) -> Result { + Ok(Weight::zero()) + } +} + +pub type SimpleExtV0 = SimpleExt<0>; +pub type SimpleExtV4 = SimpleExt<4>; +pub type SimpleExtV7 = SimpleExt<7>; + +pub type Ext4 = sp_runtime::traits::PipelineAtVers<4, SimpleExtV4>; +pub type Ext7 = sp_runtime::traits::PipelineAtVers<7, SimpleExtV7>; + +pub type OtherVersions = sp_runtime::traits::MultiVersion; + +pub type AccountId = u64; +pub type UncheckedExtrinsic = generic::UncheckedExtrinsic< + AccountId, + RuntimeCall, + UintAuthorityId, + SimpleExtV0, + OtherVersions, +>; + +pub type BlockNumber = u32; +pub type Block = generic::Block, UncheckedExtrinsic>; + +#[frame_support::runtime] +mod runtime { + #[runtime::runtime] + #[runtime::derive( + RuntimeCall, + RuntimeEvent, + RuntimeError, + RuntimeOrigin, + RuntimeFreezeReason, + RuntimeHoldReason, + RuntimeSlashReason, + RuntimeLockId, + RuntimeTask + )] + pub struct Runtime; + + #[runtime::pallet_index(30)] + pub type System = frame_system; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Runtime { + type AccountId = AccountId; + type Lookup = sp_runtime::traits::IdentityLookup; + type BaseCallFilter = frame_support::traits::Everything; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type PalletInfo = PalletInfo; + type OnSetCode = (); + type Block = Block; +} + +#[test] +fn test_metadata() { + let metadata = Runtime::metadata_ir(); + + assert_eq!( + metadata.extrinsic.extensions_by_version, + [(0, vec![0]), (4, vec![1]), (7, vec![2]),].into_iter().collect() + ); + + assert_eq!( + metadata + .extrinsic + .extensions_in_versions + .iter() + .map(|ext| ext.identifier) + .collect::>(), + vec!["SimpleExt0", "SimpleExt4", "SimpleExt7"] + ); +} + +#[test] +fn dispatch_of_valid_extrinsic_succeeds() { + let mut ext = sp_io::TestExternalities::new(Default::default()); + ext.execute_with(|| { + // Create an unchecked extrinsic with transaction extension: token=7, + // This is valid and will result in a signed origin with account id `100`. + let xt = UncheckedExtrinsic::from_parts( + RuntimeCall::System(frame_system::Call::remark { remark: vec![1, 2] }), + Preamble::General(ExtensionVariant::Other(OtherVersions::A(Ext4::new(SimpleExtV4 { + token: 7, + })))), + ); + + let len = xt.using_encoded(|e| e.len()); + let info = xt.get_dispatch_info(); + + let checked = xt.check(&IdentityLookup::default()); + assert!(checked.is_ok(), "Should produce a valid checked extrinsic"); + let checked = checked.unwrap(); + + checked + .validate::(TransactionSource::External, &info, len) + .expect("valid"); + + checked.apply::(&info, len).expect("valid").expect("success"); + }); +} + +#[test] +fn dispatch_of_invalid_extrinsic_fails() { + let mut ext = sp_io::TestExternalities::new(Default::default()); + ext.execute_with(|| { + // Create an unchecked extrinsic with transaction extension: token=0, + // This is invalid as `SimpleExt` validation fails for token=0, + let xt = UncheckedExtrinsic::from_parts( + RuntimeCall::System(frame_system::Call::remark { remark: vec![1, 2] }), + Preamble::General(ExtensionVariant::Other(OtherVersions::A(Ext4::new(SimpleExtV4 { + token: 0, + })))), + ); + + let len = xt.using_encoded(|e| e.len()); + let info = xt.get_dispatch_info(); + + let checked = xt.check(&IdentityLookup::default()); + assert!(checked.is_ok(), "Should produce a valid checked extrinsic"); + let checked = checked.unwrap(); + + checked + .validate::(TransactionSource::External, &info, len) + .expect_err("invalid, `SimpleExt` is invalid if token == 0"); + + checked.apply::(&info, len).expect_err("invalid"); + }); +} diff --git a/substrate/frame/transaction-payment/src/tests.rs b/substrate/frame/transaction-payment/src/tests.rs index 8349df306675e..3371aca4f75d6 100644 --- a/substrate/frame/transaction-payment/src/tests.rs +++ b/substrate/frame/transaction-payment/src/tests.rs @@ -282,7 +282,7 @@ fn query_info_and_fee_details_works() { let call = RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 2, value: 69 }); let origin = 111111; let extra = (); - let xt = UncheckedExtrinsic::new_signed(call.clone(), origin, (), extra); + let xt = UncheckedExtrinsic::<_, _, _, _>::new_signed(call.clone(), origin, (), extra); let info = xt.get_dispatch_info(); let ext = xt.encode(); let len = ext.len() as u32; diff --git a/substrate/primitives/metadata-ir/Cargo.toml b/substrate/primitives/metadata-ir/Cargo.toml index d7786347dd028..852ded865d7c5 100644 --- a/substrate/primitives/metadata-ir/Cargo.toml +++ b/substrate/primitives/metadata-ir/Cargo.toml @@ -17,6 +17,7 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] codec = { workspace = true } +derive-where = { workspace = true } frame-metadata = { features = ["current"], workspace = true } scale-info = { features = ["derive"], workspace = true } diff --git a/substrate/primitives/metadata-ir/src/lib.rs b/substrate/primitives/metadata-ir/src/lib.rs index 3ad2dd6203f38..433b383f04245 100644 --- a/substrate/primitives/metadata-ir/src/lib.rs +++ b/substrate/primitives/metadata-ir/src/lib.rs @@ -119,7 +119,8 @@ mod test { call_ty: meta_type::<()>(), signature_ty: meta_type::<()>(), extra_ty: meta_type::<()>(), - extensions: vec![], + extensions_by_version: [(0, vec![])].into(), + extensions_in_versions: vec![], }, ty: meta_type::<()>(), apis: vec![], diff --git a/substrate/primitives/metadata-ir/src/types.rs b/substrate/primitives/metadata-ir/src/types.rs index a56c927bee7a3..6e8f8757249b0 100644 --- a/substrate/primitives/metadata-ir/src/types.rs +++ b/substrate/primitives/metadata-ir/src/types.rs @@ -16,6 +16,7 @@ // limitations under the License. use codec::{Compact, Decode, Encode}; +use derive_where::derive_where; use scale_info::{ form::{Form, MetaForm, PortableForm}, prelude::{collections::BTreeMap, vec::Vec}, @@ -44,7 +45,8 @@ pub struct MetadataIR { } /// Metadata of a runtime trait. -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub struct RuntimeApiMetadataIR { /// Trait name. pub name: T::String, @@ -73,7 +75,8 @@ impl IntoPortable for RuntimeApiMetadataIR { } /// Metadata of a runtime method. -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub struct RuntimeApiMethodMetadataIR { /// Method name. pub name: T::String, @@ -102,7 +105,8 @@ impl IntoPortable for RuntimeApiMethodMetadataIR { } /// Metadata of a runtime method parameter. -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub struct RuntimeApiMethodParamMetadataIR { /// Parameter name. pub name: T::String, @@ -122,7 +126,8 @@ impl IntoPortable for RuntimeApiMethodParamMetadataIR { } /// Metadata of a pallet view function method. -#[derive(Clone, PartialEq, Eq, Encode, Decode, Debug)] +#[derive(Encode, Decode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub struct PalletViewFunctionMetadataIR { /// Method name. pub name: T::String, @@ -154,7 +159,8 @@ impl IntoPortable for PalletViewFunctionMetadataIR { } /// Metadata of a pallet view function method argument. -#[derive(Clone, PartialEq, Eq, Encode, Decode, Debug)] +#[derive(Encode, Decode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub struct PalletViewFunctionParamMetadataIR { /// Parameter name. pub name: T::String, @@ -174,7 +180,8 @@ impl IntoPortable for PalletViewFunctionParamMetadataIR { } /// The intermediate representation for a pallet metadata. -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub struct PalletMetadataIR { /// Pallet name. pub name: T::String, @@ -226,7 +233,8 @@ impl IntoPortable for PalletMetadataIR { } /// Metadata of the extrinsic used by the runtime. -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub struct ExtrinsicMetadataIR { /// The type of the extrinsic. /// @@ -243,8 +251,24 @@ pub struct ExtrinsicMetadataIR { /// The type of the outermost Extra/Extensions enum. // TODO: metadata-v16: remove this, the `implicit` type can be found in `extensions::implicit`. pub extra_ty: T::Type, - /// The transaction extensions in the order they appear in the extrinsic. - pub extensions: Vec>, + /// The transaction extensions for each version as a list of index in reference to items in + /// `extensions_in_versions` field. + pub extensions_by_version: BTreeMap>, + /// The list of all transaction extensions used in `extensions_by_version`. + pub extensions_in_versions: Vec>, +} + +impl ExtrinsicMetadataIR { + /// The transaction extensions in the order they appear in the extrinsic for the version 0 if + /// defined. + pub fn extensions_v0(&self) -> Option>> { + self.extensions_by_version.get(&0).map(|indices| { + indices + .iter() + .map(|i| self.extensions_in_versions[*i as usize].clone()) + .collect() + }) + } } impl IntoPortable for ExtrinsicMetadataIR { @@ -258,13 +282,15 @@ impl IntoPortable for ExtrinsicMetadataIR { call_ty: registry.register_type(&self.call_ty), signature_ty: registry.register_type(&self.signature_ty), extra_ty: registry.register_type(&self.extra_ty), - extensions: registry.map_into_portable(self.extensions), + extensions_by_version: self.extensions_by_version, + extensions_in_versions: registry.map_into_portable(self.extensions_in_versions), } } } /// Metadata of a pallet's associated type. -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub struct PalletAssociatedTypeMetadataIR { /// The name of the associated type. pub name: T::String, @@ -287,7 +313,8 @@ impl IntoPortable for PalletAssociatedTypeMetadataIR { } /// Metadata of an extrinsic's signed extension. -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub struct TransactionExtensionMetadataIR { /// The unique signed extension identifier, which may be different from the type name. pub identifier: T::String, @@ -310,8 +337,10 @@ impl IntoPortable for TransactionExtensionMetadataIR { } /// All metadata of the pallet's storage. -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +/// /// The common prefix used by all storage entries. +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub struct PalletStorageMetadataIR { /// The common prefix used by all storage entries. pub prefix: T::String, @@ -331,7 +360,8 @@ impl IntoPortable for PalletStorageMetadataIR { } /// Metadata about one storage entry. -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub struct StorageEntryMetadataIR { /// Variable name of the storage entry. pub name: T::String, @@ -397,7 +427,8 @@ pub enum StorageHasherIR { } /// A type of storage value. -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub enum StorageEntryTypeIR { /// Plain storage entry (just the value). Plain(T::Type), @@ -428,7 +459,8 @@ impl IntoPortable for StorageEntryTypeIR { } /// Metadata for all calls in a pallet -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub struct PalletCallMetadataIR { /// The corresponding enum type for the pallet call. pub ty: T::Type, @@ -448,7 +480,8 @@ impl IntoPortable for PalletCallMetadataIR { } /// Metadata about the pallet Event type. -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub struct PalletEventMetadataIR { /// The Event type. pub ty: T::Type, @@ -468,7 +501,8 @@ impl IntoPortable for PalletEventMetadataIR { } /// Metadata about one pallet constant. -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub struct PalletConstantMetadataIR { /// Name of the pallet constant. pub name: T::String, @@ -497,7 +531,8 @@ impl IntoPortable for PalletConstantMetadataIR { } /// Metadata about a pallet error. -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub struct PalletErrorMetadataIR { /// The error type information. pub ty: T::Type, @@ -517,7 +552,8 @@ impl IntoPortable for PalletErrorMetadataIR { } /// The type of the outer enums. -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub struct OuterEnumsIR { /// The type of the outer `RuntimeCall` enum. pub call_enum_ty: T::Type, @@ -554,7 +590,8 @@ impl IntoPortable for OuterEnumsIR { } /// Deprecation information for generic items. -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub enum ItemDeprecationInfoIR { /// Item is not deprecated. NotDeprecated, @@ -587,7 +624,8 @@ impl IntoPortable for ItemDeprecationInfoIR { /// Deprecation information for enums in which specific variants can be deprecated. /// If the map is empty, then nothing is deprecated. -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub struct EnumDeprecationInfoIR(pub BTreeMap>); impl EnumDeprecationInfoIR { @@ -617,7 +655,8 @@ impl IntoPortable for EnumDeprecationInfoIR { } /// Deprecation information for an item or variant in the metadata. -#[derive(Clone, PartialEq, Eq, Encode, Debug)] +#[derive(Encode)] +#[derive_where(Clone, PartialEq, Eq, Debug;)] pub enum VariantDeprecationInfoIR { /// Variant is deprecated without a note. DeprecatedWithoutNote, diff --git a/substrate/primitives/metadata-ir/src/v14.rs b/substrate/primitives/metadata-ir/src/v14.rs index f3cb5973f5bdc..a8a528e8bf101 100644 --- a/substrate/primitives/metadata-ir/src/v14.rs +++ b/substrate/primitives/metadata-ir/src/v14.rs @@ -155,7 +155,12 @@ impl From for ExtrinsicMetadata { ExtrinsicMetadata { ty: ir.ty, version: *lowest_supported_version, - signed_extensions: ir.extensions.into_iter().map(Into::into).collect(), + signed_extensions: ir + .extensions_v0() + .expect("Metadata V14 expect a defined transaction extenstion pipeline version 0") + .into_iter() + .map(Into::into) + .collect(), } } } diff --git a/substrate/primitives/metadata-ir/src/v15.rs b/substrate/primitives/metadata-ir/src/v15.rs index 1bfad0bd355f1..371b1b1fcd820 100644 --- a/substrate/primitives/metadata-ir/src/v15.rs +++ b/substrate/primitives/metadata-ir/src/v15.rs @@ -101,7 +101,12 @@ impl From for ExtrinsicMetadata { call_ty: ir.call_ty, signature_ty: ir.signature_ty, extra_ty: ir.extra_ty, - signed_extensions: ir.extensions.into_iter().map(Into::into).collect(), + signed_extensions: ir + .extensions_v0() + .expect("Metadata V15 expect a defined transaction extenstion pipeline version 0") + .into_iter() + .map(Into::into) + .collect(), } } } diff --git a/substrate/primitives/metadata-ir/src/v16.rs b/substrate/primitives/metadata-ir/src/v16.rs index 3ba9f093939f2..babbbc437f0d6 100644 --- a/substrate/primitives/metadata-ir/src/v16.rs +++ b/substrate/primitives/metadata-ir/src/v16.rs @@ -37,7 +37,6 @@ use frame_metadata::v16::{ StorageEntryMetadata, TransactionExtensionMetadata, VariantDeprecationInfo, }; -use codec::Compact; use scale_info::form::MetaForm; impl From for RuntimeMetadataV16 { @@ -181,17 +180,23 @@ impl From for TransactionExtensionMetadata { impl ExtrinsicMetadataIR { fn into_v16_with_call_ty(self, call_ty: scale_info::MetaType) -> ExtrinsicMetadata { - // Assume version 0 for all extensions. - let indexes = (0..self.extensions.len()).map(|index| Compact(index as u32)).collect(); - let transaction_extensions_by_version = [(0, indexes)].iter().cloned().collect(); - ExtrinsicMetadata { versions: self.versions, address_ty: self.address_ty, call_ty, signature_ty: self.signature_ty, - transaction_extensions_by_version, - transaction_extensions: self.extensions.into_iter().map(Into::into).collect(), + transaction_extensions_by_version: self + .extensions_by_version + .into_iter() + .map(|(version, extensions)| { + (version, extensions.into_iter().map(Into::into).collect()) + }) + .collect(), + transaction_extensions: self + .extensions_in_versions + .into_iter() + .map(Into::into) + .collect(), } } } diff --git a/substrate/primitives/runtime/src/generic/checked_extrinsic.rs b/substrate/primitives/runtime/src/generic/checked_extrinsic.rs index 816c5248d6e64..62b738559ed5d 100644 --- a/substrate/primitives/runtime/src/generic/checked_extrinsic.rs +++ b/substrate/primitives/runtime/src/generic/checked_extrinsic.rs @@ -18,37 +18,42 @@ //! Generic implementation of an extrinsic that has passed the verification //! stage. -use codec::Encode; -use sp_weights::Weight; - +use super::unchecked_extrinsic::ExtensionVersion; use crate::{ traits::{ - self, transaction_extension::TransactionExtension, AsTransactionAuthorizedOrigin, - DispatchInfoOf, DispatchTransaction, Dispatchable, MaybeDisplay, Member, - PostDispatchInfoOf, ValidateUnsigned, + self, AsTransactionAuthorizedOrigin, DispatchInfoOf, DispatchTransaction, Dispatchable, + ExtensionVariant, InvalidVersion, MaybeDisplay, Member, Pipeline, PostDispatchInfoOf, + TransactionExtension, ValidateUnsigned, }, transaction_validity::{TransactionSource, TransactionValidity}, }; +use codec::Encode; +use sp_weights::Weight; -use super::unchecked_extrinsic::ExtensionVersion; - -/// Default version of the [Extension](TransactionExtension) used to construct the inherited -/// implication for legacy transactions. -const DEFAULT_EXTENSION_VERSION: ExtensionVersion = 0; +/// Version 0 of the transaction extension version. +const EXTENSION_V0_VERSION: ExtensionVersion = 0; /// The kind of extrinsic this is, including any fields required of that kind. This is basically /// the full extrinsic except the `Call`. +/// +/// Bare extrinsics and signed extrinsics are extended with the transaction extension version 0, +/// specified by the generic parameter `ExtensionV0`. +/// +/// General extrinsics support multiple transaction extension version, specified by both +/// `ExtensionV0` and `ExtensionOtherVersions`, by default `ExtensionOtherVersions` is set to +/// invalid version, making `ExtensionV0` the only supported version. If you want to support more +/// versions, you need to specify the `ExtensionOtherVersions` parameter. #[derive(PartialEq, Eq, Clone, Debug)] -pub enum ExtrinsicFormat { +pub enum ExtrinsicFormat { /// Extrinsic is bare; it must pass either the bare forms of `TransactionExtension` or /// `ValidateUnsigned`, both deprecated, or alternatively a `ProvideInherent`. Bare, /// Extrinsic has a default `Origin` of `Signed(AccountId)` and must pass all /// `TransactionExtension`s regular checks and includes all extension data. - Signed(AccountId, Extension), + Signed(AccountId, ExtensionV0), /// Extrinsic has a default `Origin` of `None` and must pass all `TransactionExtension`s. /// regular checks and includes all extension data. - General(ExtensionVersion, Extension), + General(ExtensionVariant), } /// Definition of something that the external world might want to say; its existence implies that it @@ -56,22 +61,31 @@ pub enum ExtrinsicFormat { /// /// This is typically passed into [`traits::Applyable::apply`], which should execute /// [`CheckedExtrinsic::function`], alongside all other bits and bobs. +/// +/// Bare extrinsics and signed extrinsics are extended with the transaction extension version 0, +/// specified by the generic parameter `ExtensionV0`. +/// +/// General extrinsics support multiple transaction extension versions, specified by both +/// `ExtensionV0` and `ExtensionOtherVersions`, by default `ExtensionOtherVersions` is set to +/// invalid version, making `ExtensionV0` the only supported version. If you want to support more +/// versions, you need to specify the `ExtensionOtherVersions` parameter. #[derive(PartialEq, Eq, Clone, Debug)] -pub struct CheckedExtrinsic { +pub struct CheckedExtrinsic { /// Who this purports to be from and the number of extrinsics have come before /// from the same signer, if anyone (note this is not a signature). - pub format: ExtrinsicFormat, + pub format: ExtrinsicFormat, /// The function that should be called. pub function: Call, } -impl traits::Applyable - for CheckedExtrinsic +impl traits::Applyable + for CheckedExtrinsic where AccountId: Member + MaybeDisplay, Call: Member + Dispatchable + Encode, - Extension: TransactionExtension, + ExtensionV0: TransactionExtension, + ExtensionOtherVersions: Pipeline, RuntimeOrigin: From> + AsTransactionAuthorizedOrigin, { type Call = Call; @@ -85,25 +99,18 @@ where match self.format { ExtrinsicFormat::Bare => { let inherent_validation = I::validate_unsigned(source, &self.function)?; - let legacy_validation = Extension::bare_validate(&self.function, info, len)?; + let legacy_validation = ExtensionV0::bare_validate(&self.function, info, len)?; Ok(legacy_validation.combine_with(inherent_validation)) }, ExtrinsicFormat::Signed(ref signer, ref extension) => { let origin = Some(signer.clone()).into(); extension - .validate_only( - origin, - &self.function, - info, - len, - source, - DEFAULT_EXTENSION_VERSION, - ) + .validate_only(origin, &self.function, info, len, source, EXTENSION_V0_VERSION) .map(|x| x.0) }, - ExtrinsicFormat::General(extension_version, ref extension) => extension - .validate_only(None.into(), &self.function, info, len, source, extension_version) - .map(|x| x.0), + ExtrinsicFormat::General(ref extension) => { + extension.validate_only(None.into(), &self.function, info, len, source) + }, } } @@ -117,13 +124,13 @@ where I::pre_dispatch(&self.function)?; // TODO: Separate logic from `TransactionExtension` into a new `InherentExtension` // interface. - Extension::bare_validate_and_prepare(&self.function, info, len)?; + ExtensionV0::bare_validate_and_prepare(&self.function, info, len)?; let res = self.function.dispatch(None.into()); let mut post_info = res.unwrap_or_else(|err| err.post_info); let pd_res = res.map(|_| ()).map_err(|e| e.error); // TODO: Separate logic from `TransactionExtension` into a new `InherentExtension` // interface. - Extension::bare_post_dispatch(info, &mut post_info, len, &pd_res)?; + ExtensionV0::bare_post_dispatch(info, &mut post_info, len, &pd_res)?; Ok(res) }, ExtrinsicFormat::Signed(signer, extension) => extension.dispatch_transaction( @@ -131,25 +138,30 @@ where self.function, info, len, - DEFAULT_EXTENSION_VERSION, + EXTENSION_V0_VERSION, ), - ExtrinsicFormat::General(extension_version, extension) => extension - .dispatch_transaction(None.into(), self.function, info, len, extension_version), + ExtrinsicFormat::General(extension) => { + extension.dispatch_transaction(None.into(), self.function, info, len) + }, } } } -impl> - CheckedExtrinsic +impl + CheckedExtrinsic +where + Call: Dispatchable + Encode, + ExtensionV0: TransactionExtension, + ExtensionOtherVersions: Pipeline, + ::RuntimeOrigin: AsTransactionAuthorizedOrigin, { /// Returns the weight of the extension of this transaction, if present. If the transaction /// doesn't use any extension, the weight returned is equal to zero. pub fn extension_weight(&self) -> Weight { match &self.format { ExtrinsicFormat::Bare => Weight::zero(), - ExtrinsicFormat::Signed(_, ext) | ExtrinsicFormat::General(_, ext) => { - ext.weight(&self.function) - }, + ExtrinsicFormat::Signed(_, ext) => ext.weight(&self.function), + ExtrinsicFormat::General(ext) => ext.weight(&self.function), } } } diff --git a/substrate/primitives/runtime/src/generic/unchecked_extrinsic.rs b/substrate/primitives/runtime/src/generic/unchecked_extrinsic.rs index bd1b93bbd0fd5..5205d377772e5 100644 --- a/substrate/primitives/runtime/src/generic/unchecked_extrinsic.rs +++ b/substrate/primitives/runtime/src/generic/unchecked_extrinsic.rs @@ -20,9 +20,10 @@ use crate::{ generic::{CheckedExtrinsic, ExtrinsicFormat}, traits::{ - self, transaction_extension::TransactionExtension, Checkable, Dispatchable, ExtrinsicCall, - ExtrinsicLike, ExtrinsicMetadata, IdentifyAccount, LazyExtrinsic, MaybeDisplay, Member, - SignaturePayload, + self, AsTransactionAuthorizedOrigin, Checkable, DecodeWithVersion, + DecodeWithVersionWithMemTracking, Dispatchable, ExtensionVariant, ExtrinsicCall, + ExtrinsicLike, ExtrinsicMetadata, IdentifyAccount, InvalidVersion, LazyExtrinsic, + MaybeDisplay, Member, Pipeline, PipelineVersion, SignaturePayload, TransactionExtension, }, transaction_validity::{InvalidTransaction, TransactionValidityError}, OpaqueExtrinsic, @@ -58,12 +59,6 @@ pub const EXTRINSIC_FORMAT_VERSION: ExtrinsicVersion = 5; /// compatibility reasons. It will be deprecated in favor of v5 extrinsics and an inherent/general /// transaction model. pub const LEGACY_EXTRINSIC_FORMAT_VERSION: ExtrinsicVersion = 4; -/// Current version of the [Extension](TransactionExtension) used in this -/// [extrinsic](UncheckedExtrinsic). -/// -/// This version needs to be bumped if there are breaking changes to the extension used in the -/// [UncheckedExtrinsic] implementation. -const EXTENSION_VERSION: ExtensionVersion = 0; /// Maximum decoded heap size for a runtime call (in bytes). pub const DEFAULT_MAX_CALL_SIZE: usize = 16 * 1024 * 1024; // 16 MiB @@ -81,8 +76,16 @@ impl SignaturePaylo /// A "header" for extrinsics leading up to the call itself. Determines the type of extrinsic and /// holds any necessary specialized data. -#[derive(DecodeWithMemTracking, Eq, PartialEq, Clone)] -pub enum Preamble { +/// +/// Bare extrinsics and signed extrinsics are extended with the transaction extension version 0, +/// specified by the generic parameter `ExtensionV0`. +/// +/// General extrinsics support multiple transaction extension version, specified by both +/// `ExtensionV0` and `ExtensionOtherVersions`, by default `ExtensionOtherVersions` is set to +/// invalid version, making `ExtensionV0` the only supported version. If you want to support more +/// versions, you need to specify the `ExtensionOtherVersions` parameter. +#[derive(Eq, PartialEq, Clone)] +pub enum Preamble { /// An extrinsic without a signature or any extension. This means it's either an inherent or /// an old-school "Unsigned" (we don't use that terminology any more since it's confusable with /// the general transaction which is without a signature but does have an extension). @@ -92,11 +95,11 @@ pub enum Preamble { Bare(ExtrinsicVersion), /// An old-school transaction extrinsic which includes a signature of some hard-coded crypto. /// Available only on extrinsic version 4. - Signed(Address, Signature, Extension), + Signed(Address, Signature, ExtensionV0), /// A new-school transaction extrinsic which does not include a signature by default. The /// origin authorization, through signatures or other means, is performed by the transaction /// extension in this extrinsic. Available starting with extrinsic version 5. - General(ExtensionVersion, Extension), + General(ExtensionVariant), } const VERSION_MASK: u8 = 0b0011_1111; @@ -105,11 +108,13 @@ const BARE_EXTRINSIC: u8 = 0b0000_0000; const SIGNED_EXTRINSIC: u8 = 0b1000_0000; const GENERAL_EXTRINSIC: u8 = 0b0100_0000; -impl Decode for Preamble +impl Decode + for Preamble where Address: Decode, Signature: Decode, - Extension: Decode, + ExtensionV0: Decode, + ExtensionOtherVersions: DecodeWithVersion, { fn decode(input: &mut I) -> Result { let version_and_type = input.read_byte()?; @@ -125,13 +130,17 @@ where (LEGACY_EXTRINSIC_FORMAT_VERSION, SIGNED_EXTRINSIC) => { let address = Address::decode(input)?; let signature = Signature::decode(input)?; - let ext = Extension::decode(input)?; + let ext = ExtensionV0::decode(input)?; Self::Signed(address, signature, ext) }, (EXTRINSIC_FORMAT_VERSION, GENERAL_EXTRINSIC) => { let ext_version = ExtensionVersion::decode(input)?; - let ext = Extension::decode(input)?; - Self::General(ext_version, ext) + let ext = + ExtensionVariant::::decode_with_version( + ext_version, + input, + )?; + Self::General(ext) }, (_, _) => return Err("Invalid transaction version".into()), }; @@ -140,11 +149,23 @@ where } } -impl Encode for Preamble +impl DecodeWithMemTracking + for Preamble +where + Address: DecodeWithMemTracking, + Signature: DecodeWithMemTracking, + ExtensionV0: DecodeWithMemTracking, + ExtensionOtherVersions: DecodeWithVersionWithMemTracking, +{ +} + +impl Encode + for Preamble where Address: Encode, Signature: Encode, - Extension: Encode, + ExtensionV0: Encode, + ExtensionOtherVersions: Encode + PipelineVersion, { fn size_hint(&self) -> usize { match &self { @@ -154,9 +175,9 @@ where .saturating_add(address.size_hint()) .saturating_add(signature.size_hint()) .saturating_add(ext.size_hint()), - Preamble::General(ext_version, ext) => EXTRINSIC_FORMAT_VERSION + Preamble::General(ext) => EXTRINSIC_FORMAT_VERSION .size_hint() - .saturating_add(ext_version.size_hint()) + .saturating_add(0u8.size_hint()) // version .saturating_add(ext.size_hint()), } } @@ -172,18 +193,20 @@ where signature.encode_to(dest); ext.encode_to(dest); }, - Preamble::General(ext_version, ext) => { + Preamble::General(ext) => { (EXTRINSIC_FORMAT_VERSION | GENERAL_EXTRINSIC).encode_to(dest); - ext_version.encode_to(dest); + ext.version().encode_to(dest); ext.encode_to(dest); }, } } } -impl Preamble { +impl + Preamble +{ /// Returns `Some` if this is a signed extrinsic, together with the relevant inner fields. - pub fn to_signed(self) -> Option<(Address, Signature, Extension)> { + pub fn to_signed(self) -> Option<(Address, Signature, ExtensionV0)> { match self { Self::Signed(a, s, e) => Some((a, s, e)), _ => None, @@ -191,18 +214,18 @@ impl Preamble { } } -impl fmt::Debug for Preamble +impl fmt::Debug + for Preamble where Address: fmt::Debug, - Extension: fmt::Debug, + ExtensionV0: fmt::Debug, + ExtensionOtherVersions: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::Bare(_) => write!(f, "Bare"), Self::Signed(address, _, tx_ext) => write!(f, "Signed({:?}, {:?})", address, tx_ext), - Self::General(ext_version, tx_ext) => { - write!(f, "General({:?}, {:?})", ext_version, tx_ext) - }, + Self::General(tx_ext) => write!(f, "General({:?})", tx_ext), } } } @@ -212,16 +235,32 @@ where /// An extrinsic is formally described as any external data that is originating from the outside of /// the runtime and fed into the runtime as a part of the block-body. /// -/// Inherents are special types of extrinsics that are placed into the block by the block-builder. -/// They are unsigned because the assertion is that they are "inherently true" by virtue of getting -/// past all validators. +/// Prerequisite concepts: Inherents vs Transactions +/// * Inherents are special types of extrinsics that are placed into the block by the block-builder. +/// They are unsigned because the assertion is that they are "inherently true" by virtue of +/// getting past all validators. +/// * Transactions are all other statements provided by external entities that the chain deems +/// values and decided to include in the block. This value is typically in the form of fee +/// payment, but it could in principle be any other interaction. Transactions are either signed or +/// unsigned. A sensible transaction pool should ensure that only transactions that are worthwhile +/// are considered for block-building. +/// +/// Types of extrinsics: +/// - **Bare**: An extrinsic without a signature or any additional data. After being checked, bare +/// extrinsics can be applied, the apply logic is extended with `ExtensionV0`. +/// Bare extrinsics are typically used for inherent extrinsics. +/// Bare extrinsics are also used for some unsigned transactions, this feature will be deprecated, +/// see [transaction horizon](https://github.com/paritytech/polkadot-sdk/issues/2415). +/// - **Signed**: An extrinsic with a signature and extended with transaction extension +/// `ExtensionV0`. (transaction extension: [`TransactionExtension`]). +/// - **General**: An extrinsic extended with a versioned transaction extension. The transaction +/// extension is specified by both `ExtensionV0` and `ExtensionOtherVersions`. By default, +/// `ExtensionOtherVersions` is set to invalid version, making `ExtensionV0` the only supported +/// version. General transaction is a generalization of signed transaction that doesn't hardcode a +/// signature, instead signature is to be set and checked by a transaction extension. +/// +/// #[cfg_attr(all(feature = "std", not(windows)), doc = simple_mermaid::mermaid!("../../docs/mermaid/extrinsics.mmd"))] /// -/// Transactions are all other statements provided by external entities that the chain deems values -/// and decided to include in the block. This value is typically in the form of fee payment, but it -/// could in principle be any other interaction. Transactions are either signed or unsigned. A -/// sensible transaction pool should ensure that only transactions that are worthwhile are -/// considered for block-building. -#[cfg_attr(all(feature = "std", not(windows)), doc = simple_mermaid::mermaid!("../../docs/mermaid/extrinsics.mmd"))] /// This type is by no means enforced within Substrate, but given its genericness, it is highly /// likely that for most use-cases it will suffice. Thus, the encoding of this type will dictate /// exactly what bytes should be sent to a runtime to transact with it. @@ -229,23 +268,61 @@ where /// This can be checked using [`Checkable`], yielding a [`CheckedExtrinsic`], which is the /// counterpart of this type after its signature (and other non-negotiable validity checks) have /// passed. +/// +/// # Usage: +/// +/// Usage with multiple versions for general transaction. +/// ``` +/// use sp_runtime::{ +/// generic::UncheckedExtrinsic, +/// traits::{MultiVersion, PipelineAtVers}, +/// }; +/// +/// struct Signature; // Some signature scheme. +/// struct Call; // Some call type. +/// struct AccountId; // Type for account identifier that can sign a transaction. +/// +/// // Some implementation of `TransactionExtension`. +/// struct PaymentExt; +/// struct PaymentV2Ext; +/// struct SigV2Ext; +/// struct NonceExt; +/// struct NonceV2Ext; +/// +/// // Definition of the extrinsic. +/// type ExtensionV0 = (SigV2Ext, NonceExt, PaymentExt); +/// type ExtensionV1 = PipelineAtVers<1, (SigV2Ext, NonceExt, PaymentV2Ext)>; +/// type ExtensionV2 = PipelineAtVers<2, (SigV2Ext, NonceV2Ext, PaymentV2Ext)>; +/// +/// type OtherVersions = MultiVersion; +/// +/// type Extrinsic = UncheckedExtrinsic< +/// AccountId, +/// Call, +/// Signature, +/// ExtensionV0, +/// OtherVersions, +/// >; +/// ``` #[derive(DecodeWithMemTracking, Eq, Clone)] #[codec(decode_with_mem_tracking_bound( Address: DecodeWithMemTracking, Call: DecodeWithMemTracking, Signature: DecodeWithMemTracking, - Extension: DecodeWithMemTracking) -)] + ExtensionV0: DecodeWithMemTracking, + ExtensionOtherVersions: DecodeWithVersionWithMemTracking, +))] pub struct UncheckedExtrinsic< Address, Call, Signature, - Extension, + ExtensionV0, + ExtensionOtherVersions = InvalidVersion, const MAX_CALL_SIZE: usize = DEFAULT_MAX_CALL_SIZE, > { /// Information regarding the type of extrinsic this is (inherent or transaction) as well as /// associated extension (`Extension`) data if it's a transaction and a possible signature. - pub preamble: Preamble, + pub preamble: Preamble, /// The function that should be called. pub function: Call, /// Stores the raw encoded call. @@ -262,9 +339,18 @@ impl< Address: Debug, Call: Debug, Signature: Debug, - Extension: Debug, + ExtensionV0: Debug, + ExtensionOtherVersions: Debug, const MAX_CALL_SIZE: usize, - > Debug for UncheckedExtrinsic + > Debug + for UncheckedExtrinsic< + Address, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + MAX_CALL_SIZE, + > { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("UncheckedExtrinsic") @@ -278,9 +364,18 @@ impl< Address: PartialEq, Call: PartialEq, Signature: PartialEq, - Extension: PartialEq, + ExtensionV0: PartialEq, + ExtensionOtherVersions: PartialEq, const MAX_CALL_SIZE: usize, - > PartialEq for UncheckedExtrinsic + > PartialEq + for UncheckedExtrinsic< + Address, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + MAX_CALL_SIZE, + > { fn eq(&self, other: &Self) -> bool { self.preamble == other.preamble && self.function == other.function @@ -291,15 +386,31 @@ impl< /// `Vec`, but requires some logic to extract the signature and payload. /// /// See [`UncheckedExtrinsic::encode`] and [`UncheckedExtrinsic::decode`]. -impl TypeInfo - for UncheckedExtrinsic +impl + TypeInfo + for UncheckedExtrinsic< + Address, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + MAX_CALL_SIZE, + > where Address: StaticTypeInfo, Call: StaticTypeInfo, Signature: StaticTypeInfo, - Extension: StaticTypeInfo, + ExtensionV0: StaticTypeInfo, + ExtensionOtherVersions: StaticTypeInfo, { - type Identity = UncheckedExtrinsic; + type Identity = UncheckedExtrinsic< + Address, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + MAX_CALL_SIZE, + >; fn type_info() -> Type { Type::builder() @@ -311,7 +422,12 @@ where TypeParameter::new("Address", Some(meta_type::
())), TypeParameter::new("Call", Some(meta_type::())), TypeParameter::new("Signature", Some(meta_type::())), - TypeParameter::new("Extra", Some(meta_type::())), + TypeParameter::new("Extra", Some(meta_type::())), + // NOTE: We avoid adding `ExtensionOtherVersions` in the type parameter type info + // because they break tools like subxt that hard-coded the number of type + // parameters. Also in general type parameters are not useful, information related + // to transaction extension are available in the metadata like for v16 the fields + // `transaction_extensions_by_version` and `transaction_extensions`. ]) .docs(&["UncheckedExtrinsic raw bytes, requires custom decoding routine"]) // Because of the custom encoding, we can only accurately describe the encoding as an @@ -321,8 +437,8 @@ where } } -impl - UncheckedExtrinsic +impl + UncheckedExtrinsic { /// New instance of a bare (ne unsigned) extrinsic. This could be used for an inherent or an /// old-school "unsigned transaction" (which are new being deprecated in favour of general @@ -344,7 +460,10 @@ impl } /// Create an `UncheckedExtrinsic` from a `Preamble` and the actual `Call`. - pub fn from_parts(function: Call, preamble: Preamble) -> Self { + pub fn from_parts( + function: Call, + preamble: Preamble, + ) -> Self { Self { preamble, function, encoded_call: None } } @@ -363,19 +482,19 @@ impl function: Call, signed: Address, signature: Signature, - tx_ext: Extension, + tx_ext: ExtensionV0, ) -> Self { Self::from_parts(function, Preamble::Signed(signed, signature, tx_ext)) } /// New instance of a new-school unsigned transaction. - pub fn new_transaction(function: Call, tx_ext: Extension) -> Self { - Self::from_parts(function, Preamble::General(EXTENSION_VERSION, tx_ext)) + pub fn new_transaction(function: Call, tx_ext: ExtensionV0) -> Self { + Self::from_parts(function, Preamble::General(ExtensionVariant::V0(tx_ext))) } fn decode_with_len(input: &mut I, len: usize) -> Result where - Preamble: Decode, + Preamble: Decode, Call: DecodeWithMemTracking, { let mut input = CountedInput::new(input); @@ -426,7 +545,7 @@ impl fn encode_without_prefix(&self) -> Vec where - Preamble: Encode, + Preamble: Encode, Call: Encode, { let mut encoded = self.preamble.encode(); @@ -444,8 +563,16 @@ impl } } -impl ExtrinsicLike - for UncheckedExtrinsic +impl + ExtrinsicLike + for UncheckedExtrinsic< + Address, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + MAX_CALL_SIZE, + > { fn is_signed(&self) -> Option { Some(matches!(self.preamble, Preamble::Signed(..))) @@ -456,8 +583,16 @@ impl ExtrinsicLike } } -impl ExtrinsicCall - for UncheckedExtrinsic +impl + ExtrinsicCall + for UncheckedExtrinsic< + Address, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + MAX_CALL_SIZE, + > { type Call = Call; @@ -474,18 +609,34 @@ impl ExtrinsicCall // transactions by adding an extension to validate signatures, as they are currently validated in // the `Checkable` implementation for `Signed` transactions. -impl Checkable - for UncheckedExtrinsic +impl< + LookupSource, + AccountId, + Call, + Signature, + ExtensionV0, + const MAX_CALL_SIZE: usize, + ExtensionOtherVersions, + Lookup, + > Checkable + for UncheckedExtrinsic< + LookupSource, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + MAX_CALL_SIZE, + > where LookupSource: Member + MaybeDisplay, Call: Encode + Member + Dispatchable, Signature: Member + traits::Verify, ::Signer: IdentifyAccount, - Extension: Encode + TransactionExtension, + ExtensionV0: Encode + TransactionExtension, AccountId: Member + MaybeDisplay, Lookup: traits::Lookup, { - type Checked = CheckedExtrinsic; + type Checked = CheckedExtrinsic; fn check(self, lookup: &Lookup) -> Result { Ok(match self.preamble { @@ -502,8 +653,8 @@ where let (function, tx_ext, _) = raw_payload.deconstruct(); CheckedExtrinsic { format: ExtrinsicFormat::Signed(signed, tx_ext), function } }, - Preamble::General(extension_version, tx_ext) => CheckedExtrinsic { - format: ExtrinsicFormat::General(extension_version, tx_ext), + Preamble::General(tx_ext) => CheckedExtrinsic { + format: ExtrinsicFormat::General(tx_ext), function: self.function, }, Preamble::Bare(_) => { @@ -525,8 +676,8 @@ where function: self.function, } }, - Preamble::General(extension_version, tx_ext) => CheckedExtrinsic { - format: ExtrinsicFormat::General(extension_version, tx_ext), + Preamble::General(tx_ext) => CheckedExtrinsic { + format: ExtrinsicFormat::General(tx_ext), function: self.function, }, Preamble::Bare(_) => { @@ -536,33 +687,62 @@ where } } -impl> - ExtrinsicMetadata for UncheckedExtrinsic +impl< + Address, + Call: Dispatchable, + Signature, + ExtensionV0, + const MAX_CALL_SIZE: usize, + ExtensionOtherVersions, + > ExtrinsicMetadata + for UncheckedExtrinsic< + Address, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + MAX_CALL_SIZE, + > { const VERSIONS: &'static [u8] = &[LEGACY_EXTRINSIC_FORMAT_VERSION, EXTRINSIC_FORMAT_VERSION]; - type TransactionExtensions = Extension; + type TransactionExtensionPipelines = ExtensionVariant; } -impl> - UncheckedExtrinsic +impl + UncheckedExtrinsic +where + Call: Dispatchable + Encode, + ExtensionV0: TransactionExtension, + ExtensionOtherVersions: Pipeline, + ::RuntimeOrigin: AsTransactionAuthorizedOrigin, { /// Returns the weight of the extension of this transaction, if present. If the transaction /// doesn't use any extension, the weight returned is equal to zero. pub fn extension_weight(&self) -> Weight { match &self.preamble { Preamble::Bare(_) => Weight::zero(), - Preamble::Signed(_, _, ext) | Preamble::General(_, ext) => ext.weight(&self.function), + Preamble::Signed(_, _, ext) => ext.weight(&self.function), + Preamble::General(ext) => ext.weight(&self.function), } } } -impl Decode - for UncheckedExtrinsic +impl + Decode + for UncheckedExtrinsic< + Address, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + MAX_CALL_SIZE, + > where Address: Decode, Signature: Decode, Call: DecodeWithMemTracking, - Extension: Decode, + ExtensionV0: Decode, + ExtensionOtherVersions: DecodeWithVersion, { fn decode(input: &mut I) -> Result { // This is a little more complicated than usual since the binary format must be compatible @@ -575,12 +755,21 @@ where } #[docify::export(unchecked_extrinsic_encode_impl)] -impl Encode - for UncheckedExtrinsic +impl + Encode + for UncheckedExtrinsic< + Address, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + MAX_CALL_SIZE, + > where - Preamble: Encode, + Preamble: Encode, Call: Encode, - Extension: Encode, + ExtensionV0: Encode, + ExtensionOtherVersions: Encode, { fn encode(&self) -> Vec { let tmp = self.encode_without_prefix(); @@ -597,19 +786,38 @@ where } } -impl EncodeLike - for UncheckedExtrinsic +impl + EncodeLike + for UncheckedExtrinsic< + Address, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + MAX_CALL_SIZE, + > where - Address: Encode, - Signature: Encode, - Call: Encode + Dispatchable, - Extension: TransactionExtension, + Self: Encode, { } #[cfg(feature = "serde")] -impl serde::Serialize - for UncheckedExtrinsic +impl + serde::Serialize + for UncheckedExtrinsic< + Address, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + MAX_CALL_SIZE, + > +where + Address: Encode, + Signature: Encode, + Call: Encode, + ExtensionV0: Encode, + ExtensionOtherVersions: Encode + PipelineVersion, { fn serialize(&self, seq: S) -> Result where @@ -620,8 +828,29 @@ impl serde: } #[cfg(feature = "serde")] -impl<'a, Address: Decode, Signature: Decode, Call: DecodeWithMemTracking, Extension: Decode> - serde::Deserialize<'a> for UncheckedExtrinsic +impl< + 'a, + Address, + Signature, + Call, + ExtensionV0, + const MAX_CALL_SIZE: usize, + ExtensionOtherVersions, + > serde::Deserialize<'a> + for UncheckedExtrinsic< + Address, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + MAX_CALL_SIZE, + > +where + Address: Decode, + Signature: Decode, + Call: DecodeWithMemTracking, + ExtensionV0: Decode, + ExtensionOtherVersions: DecodeWithVersion, { fn deserialize(de: D) -> Result where @@ -666,46 +895,46 @@ impl Encode for CallAndMaybeEncoded { /// Note that the payload that we sign to produce unchecked extrinsic signature /// is going to be different than the `SignaturePayload` - so the thing the extrinsic /// actually contains. -pub struct SignedPayload>( - (CallAndMaybeEncoded, Extension, Extension::Implicit), +pub struct SignedPayload>( + (CallAndMaybeEncoded, ExtensionV0, ExtensionV0::Implicit), ); -impl SignedPayload +impl SignedPayload where Call: Encode + Dispatchable, - Extension: TransactionExtension, + ExtensionV0: TransactionExtension, { /// Create new `SignedPayload` for extrinsic format version 4. /// - /// This function may fail if `implicit` of `Extension` is not available. + /// This function may fail if `implicit` of `ExtensionV0` is not available. pub fn new( call: impl Into>, - tx_ext: Extension, + tx_ext: ExtensionV0, ) -> Result { - let implicit = Extension::implicit(&tx_ext)?; + let implicit = ExtensionV0::implicit(&tx_ext)?; Ok(Self((call.into(), tx_ext, implicit))) } /// Create new `SignedPayload` from raw components. pub fn from_raw( call: impl Into>, - tx_ext: Extension, - implicit: Extension::Implicit, + tx_ext: ExtensionV0, + implicit: ExtensionV0::Implicit, ) -> Self { Self((call.into(), tx_ext, implicit)) } /// Deconstruct the payload into it's components. - pub fn deconstruct(self) -> (Call, Extension, Extension::Implicit) { + pub fn deconstruct(self) -> (Call, ExtensionV0, ExtensionV0::Implicit) { let (call, ext, implicit) = self.0; (call.call, ext, implicit) } } -impl Encode for SignedPayload +impl Encode for SignedPayload where Call: Encode + Dispatchable, - Extension: TransactionExtension, + ExtensionV0: TransactionExtension, { /// Get an encoded version of this `blake2_256`-hashed payload. fn using_encoded R>(&self, f: F) -> R { @@ -719,29 +948,59 @@ where } } -impl EncodeLike for SignedPayload +impl EncodeLike for SignedPayload where Call: Encode + Dispatchable, - Extension: TransactionExtension, + ExtensionV0: TransactionExtension, { } -impl - From> for OpaqueExtrinsic +impl + From< + UncheckedExtrinsic< + Address, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + MAX_CALL_SIZE, + >, + > for OpaqueExtrinsic where - Preamble: Encode, + Preamble: Encode, Call: Encode, + ExtensionV0: Encode, + ExtensionOtherVersions: Encode + PipelineVersion, { - fn from(extrinsic: UncheckedExtrinsic) -> Self { + fn from( + extrinsic: UncheckedExtrinsic< + Address, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + MAX_CALL_SIZE, + >, + ) -> Self { Self::from_blob(extrinsic.encode_without_prefix()) } } -impl LazyExtrinsic - for UncheckedExtrinsic +impl + LazyExtrinsic + for UncheckedExtrinsic< + Address, + Call, + Signature, + ExtensionV0, + ExtensionOtherVersions, + MAX_CALL_SIZE, + > where - Preamble: Decode, + Preamble: Decode, Call: DecodeWithMemTracking, + ExtensionV0: Decode, + ExtensionOtherVersions: DecodeWithVersion + PipelineVersion, { fn decode_unprefixed(data: &[u8]) -> Result { Self::decode_with_len(&mut &data[..], data.len()) @@ -1066,7 +1325,7 @@ mod tests { assert_eq!( >::check(ux, &Default::default()), Ok(CEx { - format: ExtrinsicFormat::General(0, DummyExtension), + format: ExtrinsicFormat::General(ExtensionVariant::V0(DummyExtension)), function: vec![0u8; 0].into() }), ); diff --git a/substrate/primitives/runtime/src/traits/mod.rs b/substrate/primitives/runtime/src/traits/mod.rs index 874bc53542e01..3dd4173bf19de 100644 --- a/substrate/primitives/runtime/src/traits/mod.rs +++ b/substrate/primitives/runtime/src/traits/mod.rs @@ -56,10 +56,15 @@ use std::fmt::Display; use std::str::FromStr; pub mod transaction_extension; +pub mod vers_tx_ext; pub use transaction_extension::{ DispatchTransaction, Implication, ImplicationParts, TransactionExtension, TransactionExtensionMetadata, TxBaseImplication, ValidateResult, }; +pub use vers_tx_ext::{ + DecodeWithVersion, DecodeWithVersionWithMemTracking, ExtensionVariant, InvalidVersion, + MultiVersion, Pipeline, PipelineAtVers, PipelineMetadataBuilder, PipelineVersion, +}; /// A lazy value. pub trait Lazy { @@ -1496,8 +1501,13 @@ pub trait ExtrinsicMetadata { /// By format we mean the encoded representation of the `Extrinsic`. const VERSIONS: &'static [u8]; - /// Transaction extensions attached to this `Extrinsic`. - type TransactionExtensions; + /// All version of transaction extensions attached to this `Extrinsic`. + /// + /// For extrinsic version 4, extrinsics don't specify any version, the pipeline version 0 is + /// used. + /// For extrinsic version 5, bare extrinsics don't specify any version, the pipeline version 0 + /// is used. + type TransactionExtensionPipelines; } /// Extract the hashing type for a block. diff --git a/substrate/primitives/runtime/src/traits/transaction_extension/mod.rs b/substrate/primitives/runtime/src/traits/transaction_extension/mod.rs index 24ec6721acf22..bdd4085de5cac 100644 --- a/substrate/primitives/runtime/src/traits/transaction_extension/mod.rs +++ b/substrate/primitives/runtime/src/traits/transaction_extension/mod.rs @@ -17,6 +17,10 @@ //! The transaction extension trait. +use super::{ + DispatchInfoOf, DispatchOriginOf, Dispatchable, ExtensionPostDispatchWeightHandler, + PostDispatchInfoOf, RefundWeight, +}; use crate::{ scale_info::{MetaType, StaticTypeInfo}, transaction_validity::{ @@ -33,11 +37,6 @@ use impl_trait_for_tuples::impl_for_tuples; use sp_weights::Weight; use tuplex::{PopFront, PushBack}; -use super::{ - DispatchInfoOf, DispatchOriginOf, Dispatchable, ExtensionPostDispatchWeightHandler, - PostDispatchInfoOf, RefundWeight, -}; - mod as_transaction_extension; mod dispatch_transaction; #[allow(deprecated)] @@ -528,6 +527,7 @@ macro_rules! impl_tx_ext_default { } /// Information about a [`TransactionExtension`] for the runtime metadata. +#[derive(Clone)] pub struct TransactionExtensionMetadata { /// The unique identifier of the [`TransactionExtension`]. pub identifier: &'static str, diff --git a/substrate/primitives/runtime/src/traits/vers_tx_ext/at_vers.rs b/substrate/primitives/runtime/src/traits/vers_tx_ext/at_vers.rs new file mode 100644 index 0000000000000..ed329f2c04482 --- /dev/null +++ b/substrate/primitives/runtime/src/traits/vers_tx_ext/at_vers.rs @@ -0,0 +1,304 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Type to define a versioned transaction extension pipeline for a specific version. + +use crate::{ + traits::{ + AsTransactionAuthorizedOrigin, DecodeWithVersion, DecodeWithVersionWithMemTracking, + DispatchInfoOf, DispatchOriginOf, DispatchTransaction, Dispatchable, Pipeline, + PipelineMetadataBuilder, PipelineVersion, PostDispatchInfoOf, TransactionExtension, + }, + transaction_validity::{TransactionSource, TransactionValidityError, ValidTransaction}, +}; +use codec::{Decode, DecodeWithMemTracking, Encode}; +use core::fmt::Debug; +use scale_info::TypeInfo; +use sp_weights::Weight; + +/// A transaction extension pipeline defined for a single version. +#[derive(Encode, Clone, Debug, TypeInfo, PartialEq, Eq)] +pub struct PipelineAtVers { + /// The transaction extension pipeline for the version `VERSION`. + pub extension: Extension, +} + +impl PipelineAtVers { + /// Create a new versioned extension. + pub fn new(extension: Extension) -> Self { + Self { extension } + } +} + +impl DecodeWithVersion + for PipelineAtVers +{ + fn decode_with_version( + extension_version: u8, + input: &mut I, + ) -> Result { + if extension_version == VERSION { + Ok(PipelineAtVers { extension: Extension::decode(input)? }) + } else { + Err(codec::Error::from("Invalid extension version")) + } + } +} + +impl DecodeWithVersionWithMemTracking + for PipelineAtVers +{ +} + +impl PipelineVersion for PipelineAtVers { + fn version(&self) -> u8 { + VERSION + } +} + +impl Pipeline for PipelineAtVers +where + Call: Dispatchable + Encode, + Extension: TransactionExtension, +{ + fn build_metadata(builder: &mut PipelineMetadataBuilder) { + builder.push_versioned_extension(VERSION, Extension::metadata()); + } + fn validate_only( + &self, + origin: DispatchOriginOf, + call: &Call, + info: &DispatchInfoOf, + len: usize, + source: TransactionSource, + ) -> Result { + self.extension + .validate_only(origin, call, info, len, source, VERSION) + .map(|x| x.0) + } + fn dispatch_transaction( + self, + origin: DispatchOriginOf, + call: Call, + info: &DispatchInfoOf, + len: usize, + ) -> crate::ApplyExtrinsicResultWithInfo> { + self.extension.dispatch_transaction(origin, call, info, len, VERSION) + } + fn weight(&self, call: &Call) -> Weight { + self.extension.weight(call) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + traits::{ + Dispatchable, Implication, TransactionExtension, TransactionSource, ValidateResult, + }, + transaction_validity::{InvalidTransaction, TransactionValidityError, ValidTransaction}, + DispatchError, + }; + use codec::{Decode, DecodeWithMemTracking, Encode}; + use sp_weights::Weight; + + // --- Mock types --- + + /// A mock call type implementing Dispatchable + #[derive(Clone, Debug, Encode, Decode, PartialEq, Eq)] + pub struct MockCall(pub u64); + #[derive(Debug)] + pub struct MockOrigin(pub u64); + + impl AsTransactionAuthorizedOrigin for MockOrigin { + fn is_transaction_authorized(&self) -> bool { + true + } + } + + impl Dispatchable for MockCall { + type RuntimeOrigin = MockOrigin; + type Config = (); + type Info = (); + type PostInfo = (); + + fn dispatch( + self, + origin: Self::RuntimeOrigin, + ) -> crate::DispatchResultWithInfo { + if origin.0 == 0 { + return Err(DispatchError::Other("origin is 0").into()); + } + Ok(Default::default()) + } + } + + // A trivial extension that sets a known weight and does minimal logic. + // We simply store an integer "token" and do check logic on it. + #[derive(PartialEq, Eq, Clone, Debug, Encode, Decode, DecodeWithMemTracking, TypeInfo)] + pub struct SimpleExtension { + /// The token for validation logic + pub token: u32, + /// The "weight" that this extension claims to cost. + pub w: u64, + } + + impl TransactionExtension for SimpleExtension { + const IDENTIFIER: &'static str = "SimpleExtension"; + + type Implicit = (); + fn implicit(&self) -> Result { + Ok(()) + } + + type Val = (); + type Pre = (); + + fn weight(&self, _call: &MockCall) -> Weight { + Weight::from_parts(self.w, 0) + } + + fn validate( + &self, + origin: MockOrigin, + _call: &MockCall, + _info: &DispatchInfoOf, + _len: usize, + _self_implicit: Self::Implicit, + _inherited_implication: &impl Implication, + _source: TransactionSource, + ) -> ValidateResult { + // any origin is permitted, but if `token == 0` => invalid + if self.token == 0 { + Err(InvalidTransaction::Custom(1).into()) + } else { + Ok((ValidTransaction::default(), (), origin)) + } + } + + fn prepare( + self, + _val: Self::Val, + _origin: &MockOrigin, + _call: &MockCall, + _info: &DispatchInfoOf, + _len: usize, + ) -> Result { + Ok(()) + } + } + + // This type represents the versioned extension pipeline for version=3. + pub type ExtV3 = PipelineAtVers<3, SimpleExtension>; + + // This type represents the versioned extension pipeline for version=10. + pub type ExtV10 = PipelineAtVers<10, SimpleExtension>; + + // --- Tests --- + + #[test] + fn decode_with_correct_version_succeeds() { + let ext_v3 = ExtV3 { extension: SimpleExtension { token: 55, w: 1234 } }; + let encoded = ext_v3.encode(); + + let decoded = ::decode_with_version(3, &mut &encoded[..]) + .expect("should decode fine with matching version"); + assert_eq!(decoded.extension.token, 55); + assert_eq!(decoded.extension.w, 1234); + } + + #[test] + fn decode_with_incorrect_version_fails() { + let ext_v3 = ExtV3 { extension: SimpleExtension { token: 55, w: 1234 } }; + let encoded = ext_v3.encode(); + + // Attempt decode with version=10 + let decode_err = ::decode_with_version(10, &mut &encoded[..]) + .expect_err("should fail decode due to invalid version"); + let decode_err_str = format!("{}", decode_err); + assert!(decode_err_str.contains("Invalid extension version")); + } + + #[test] + fn version_is_correct() { + let ext_v3 = ExtV3 { extension: SimpleExtension { token: 55, w: 1234 } }; + assert_eq!(ext_v3.version(), 3); + + let ext_v10 = ExtV10 { extension: SimpleExtension { token: 1, w: 1 } }; + assert_eq!(ext_v10.version(), 10); + } + + #[test] + fn pipeline_functions_work() { + let ext_v3 = ExtV3 { extension: SimpleExtension { token: 999, w: 50 } }; + + // test "weight" function + let call = MockCall(0x_f00); + assert_eq!(ext_v3.weight(&call).ref_time(), 50); + + // test validating logic + { + // token = 0 => invalid + let invalid_ext_v3 = ExtV3 { extension: SimpleExtension { token: 0, w: 10 } }; + let validity = invalid_ext_v3.validate_only( + MockOrigin(1), + &call, + &Default::default(), + 0, + TransactionSource::External, + ); + assert_eq!( + validity, + Err(TransactionValidityError::Invalid(InvalidTransaction::Custom(1))) + ); + } + + // ok scenario: token != 0 => OK + let validity_ok = ext_v3.validate_only( + MockOrigin(2), + &call, + &Default::default(), + 0, + TransactionSource::Local, + ); + assert!(validity_ok.is_ok()); + let valid = validity_ok.unwrap(); + assert_eq!(valid, ValidTransaction::default()); + } + + #[test] + fn dispatch_transaction_works() { + // This extension is valid => token=1 + let ext_v3 = ExtV3 { extension: SimpleExtension { token: 1, w: 10 } }; + let call = MockCall(123); + let info = Default::default(); + let len = 0usize; + + // dispatch => OK + ext_v3 + .clone() + .dispatch_transaction(MockOrigin(1), call.clone(), &info, len) + .expect("valid dispatch") + .expect("should be OK"); + + // but if origin is None => the underlying call fails + let res_fail = ext_v3.dispatch_transaction(MockOrigin(0), call, &info, len); + let block_err = res_fail.expect("valid").expect_err("should fail"); + assert_eq!(block_err.error, DispatchError::Other("origin is 0")); + } +} diff --git a/substrate/primitives/runtime/src/traits/vers_tx_ext/invalid.rs b/substrate/primitives/runtime/src/traits/vers_tx_ext/invalid.rs new file mode 100644 index 0000000000000..f05cc2cfce06e --- /dev/null +++ b/substrate/primitives/runtime/src/traits/vers_tx_ext/invalid.rs @@ -0,0 +1,101 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Implementation of versioned transaction extension pipeline that is always invalid. + +use crate::{ + traits::{ + DecodeWithVersion, DecodeWithVersionWithMemTracking, DispatchInfoOf, DispatchOriginOf, + Dispatchable, Pipeline, PipelineMetadataBuilder, PipelineVersion, PostDispatchInfoOf, + }, + transaction_validity::{TransactionSource, TransactionValidityError, ValidTransaction}, +}; +use codec::Encode; +use core::fmt::Debug; +use scale_info::TypeInfo; +use sp_weights::Weight; + +/// An implementation of [`Pipeline`] that consider any version invalid. +/// +/// This is mostly used by [`crate::traits::MultiVersion`]. +/// +/// This type cannot be instantiated. +#[derive(Encode, Debug, Clone, Eq, PartialEq, TypeInfo)] +pub enum InvalidVersion {} + +impl DecodeWithVersion for InvalidVersion { + fn decode_with_version( + _extension_version: u8, + _input: &mut I, + ) -> Result { + Err(codec::Error::from("Invalid extension version")) + } +} + +impl DecodeWithVersionWithMemTracking for InvalidVersion {} + +impl Pipeline for InvalidVersion { + fn build_metadata(_builder: &mut PipelineMetadataBuilder) { + // Do nothing. + } + fn validate_only( + &self, + _origin: DispatchOriginOf, + _call: &Call, + _info: &DispatchInfoOf, + _len: usize, + _source: TransactionSource, + ) -> Result { + // The type cannot be instantiated so this method is never called. + unreachable!() + } + fn dispatch_transaction( + self, + _origin: DispatchOriginOf, + _call: Call, + _info: &DispatchInfoOf, + _len: usize, + ) -> crate::ApplyExtrinsicResultWithInfo> { + // The type cannot be instantiated so this method is never called. + unreachable!() + } + fn weight(&self, _call: &Call) -> Weight { + // The type cannot be instantiated so this method is never called. + unreachable!() + } +} + +impl PipelineVersion for InvalidVersion { + fn version(&self) -> u8 { + // The type cannot be instantiated so this method is never called. + unreachable!() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn invalid_version_cannot_be_decoded() { + let mut input = &b""[..]; + assert_eq!( + InvalidVersion::decode_with_version(0, &mut input), + Err(codec::Error::from("Invalid extension version")) + ); + } +} diff --git a/substrate/primitives/runtime/src/traits/vers_tx_ext/mod.rs b/substrate/primitives/runtime/src/traits/vers_tx_ext/mod.rs new file mode 100644 index 0000000000000..705ecfebd35f0 --- /dev/null +++ b/substrate/primitives/runtime/src/traits/vers_tx_ext/mod.rs @@ -0,0 +1,213 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! The traits and primitive types for versioned transaction extension pipelines. + +use crate::{ + scale_info::StaticTypeInfo, + traits::{ + DispatchInfoOf, DispatchOriginOf, Dispatchable, PostDispatchInfoOf, + TransactionExtensionMetadata, + }, + transaction_validity::{TransactionSource, TransactionValidityError, ValidTransaction}, +}; +use alloc::{collections::BTreeMap, vec::Vec}; +use codec::Encode; +use core::fmt::Debug; +use sp_weights::Weight; + +mod at_vers; +mod invalid; +mod multi; +mod variant; +pub use at_vers::PipelineAtVers; +pub use invalid::InvalidVersion; +pub use multi::MultiVersion; +pub use variant::ExtensionVariant; + +/// The version for an instance of a versioned transaction extension pipeline. +/// +/// This trait is part of [`Pipeline`]. It is defined independently to allow implementation to +/// rely only on it without bounding the whole trait [`Pipeline`]. +/// +/// (type name is short for versioned (Vers) Pipeline version (Version)). +pub trait PipelineVersion { + /// Return the version for the given versioned transaction extension pipeline. + fn version(&self) -> u8; +} + +/// A versioned transaction extension pipeline. +/// +/// This defines multiple version of a transaction extensions pipeline. +/// +/// (type name is short for versioned (Vers) Pipeline). +pub trait Pipeline: + Encode + + DecodeWithVersion + + DecodeWithVersionWithMemTracking + + Debug + + StaticTypeInfo + + Send + + Sync + + Clone + + PipelineVersion +{ + /// Build the metadata for the versioned transaction extension pipeline. + fn build_metadata(builder: &mut PipelineMetadataBuilder); + + /// Validate a transaction. + fn validate_only( + &self, + origin: DispatchOriginOf, + call: &Call, + info: &DispatchInfoOf, + len: usize, + source: TransactionSource, + ) -> Result; + + /// Dispatch a transaction. + fn dispatch_transaction( + self, + origin: DispatchOriginOf, + call: Call, + info: &DispatchInfoOf, + len: usize, + ) -> crate::ApplyExtrinsicResultWithInfo>; + + /// Return the pre dispatch weight for the given versioned transaction extension pipeline and + /// call. + fn weight(&self, call: &Call) -> Weight; +} + +/// A type that can be decoded from a specific version and a [`codec::Input`]. +pub trait DecodeWithVersion: Sized { + /// Decode the type from the given version and input. + fn decode_with_version( + extension_version: u8, + input: &mut I, + ) -> Result; +} + +/// A type implements [`DecodeWithVersion`] where inner decoding is implementing +/// [`codec::DecodeWithMemTracking`]. +pub trait DecodeWithVersionWithMemTracking: DecodeWithVersion {} + +/// A type to build the metadata for the versioned transaction extension pipeline. +pub struct PipelineMetadataBuilder { + /// The transaction extension pipeline by version and its list of items as vec of index into + /// the other field `in_versions`. + pub by_version: BTreeMap>, + /// The list of all transaction extension item used. + pub in_versions: Vec, +} + +impl PipelineMetadataBuilder { + /// Create a new empty metadata builder. + pub fn new() -> Self { + Self { by_version: BTreeMap::new(), in_versions: Vec::new() } + } + + /// A function to add a versioned transaction extension pipeline to the metadata builder. + pub fn push_versioned_extension( + &mut self, + ext_version: u8, + ext_items: Vec, + ) { + if self.by_version.contains_key(&ext_version) { + log::warn!("Duplicate definition for transaction extension version: {}", ext_version); + debug_assert!( + false, + "Duplicate definition for transaction extension version: {ext_version}", + ); + return; + } + + let mut ext_item_indices = Vec::with_capacity(ext_items.len()); + for ext_item in ext_items { + let ext_item_index = + match self.in_versions.iter().position(|ext| ext.identifier == ext_item.identifier) + { + Some(index) => index, + None => { + self.in_versions.push(ext_item); + self.in_versions.len() - 1 + }, + }; + ext_item_indices.push(ext_item_index as u32); + } + self.by_version.insert(ext_version, ext_item_indices); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use scale_info::meta_type; + + #[test] + fn test_metadata_builder() { + let mut builder = PipelineMetadataBuilder::new(); + + let ext_item_a = TransactionExtensionMetadata { + identifier: "ExtensionA", + ty: meta_type::(), + implicit: meta_type::<(u32, u8)>(), + }; + let ext_item_b = TransactionExtensionMetadata { + identifier: "ExtensionB", + ty: meta_type::(), + implicit: meta_type::(), + }; + + // Push version 1 with ExtensionA. + builder.push_versioned_extension(1, vec![ext_item_a.clone()]); + // Push version 2 with ExtensionB, then ExtensionA again. + builder.push_versioned_extension(2, vec![ext_item_b.clone(), ext_item_a.clone()]); + + // We now expect: + // - `by_version` to have two entries: {1: [], 2: []}. + // - `in_versions` to contain ExtensionA and ExtensionB in some order. + + // Check that by_version now has 2 distinct versions defined. + assert_eq!(builder.by_version.len(), 2); + + // Verify version 1 entries. + { + let v1_indices = builder.by_version.get(&1).expect("Version 1 must be present"); + assert_eq!(v1_indices.len(), 1, "Version 1 should have exactly one extension"); + // Since we only ever added ExtensionA for version 1, it must match. + assert_eq!(builder.in_versions[v1_indices[0] as usize].identifier, "ExtensionA"); + } + + // Verify version 2 entries. + { + let v2_indices = builder.by_version.get(&2).expect("Version 2 must be present"); + assert_eq!(v2_indices.len(), 2, "Version 2 should have exactly two extensions"); + // For version 2, we pushed B then A, so the index order should reflect that: + // - ExtensionB is new, so it should get appended at the end of `in_versions`. + // - ExtensionA was seen previously, so it should reuse the earlier index. + + // First index for version 2 should point to "ExtensionB". + assert_eq!(builder.in_versions[v2_indices[0] as usize].identifier, "ExtensionB"); + // Second index for version 2 should point back to "ExtensionA". + assert_eq!(builder.in_versions[v2_indices[1] as usize].identifier, "ExtensionA"); + } + + // There should be exactly 2 unique entries in `in_versions`: [ExtensionA, ExtensionB]. + assert_eq!(builder.in_versions.len(), 2); + } +} diff --git a/substrate/primitives/runtime/src/traits/vers_tx_ext/multi.rs b/substrate/primitives/runtime/src/traits/vers_tx_ext/multi.rs new file mode 100644 index 0000000000000..d90990740d981 --- /dev/null +++ b/substrate/primitives/runtime/src/traits/vers_tx_ext/multi.rs @@ -0,0 +1,520 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Types and trait to aggregate multiple versioned transaction extension pipelines. + +use crate::{ + traits::{ + DecodeWithVersion, DecodeWithVersionWithMemTracking, DispatchInfoOf, DispatchOriginOf, + Dispatchable, InvalidVersion, Pipeline, PipelineAtVers, PipelineMetadataBuilder, + PipelineVersion, PostDispatchInfoOf, + }, + transaction_validity::{TransactionSource, TransactionValidityError, ValidTransaction}, +}; +use alloc::vec::Vec; +use codec::Encode; +use core::fmt::Debug; +use scale_info::TypeInfo; +use sp_weights::Weight; + +/// An item in [`MultiVersion`]. It represents a transaction extension pipeline of a specific +/// single version. +pub trait MultiVersionItem { + /// The version of the transaction extension pipeline. + /// + /// `None` means that the item has no version and can't be decoded. + const VERSION: Option; +} + +impl MultiVersionItem for InvalidVersion { + const VERSION: Option = None; +} + +impl MultiVersionItem for PipelineAtVers { + const VERSION: Option = Some(VERSION); +} + +macro_rules! declare_multi_version_enum { + ($( $variant:tt, )*) => { + + /// An implementation of [`Pipeline`] that aggregates multiple versioned transaction + /// extension pipeline. + /// + /// It is an enum where each variant has its own version, duplicated version must be + /// avoided, only the first used version will be effective other duplicated version will be + /// ignored. + /// + /// Versioned transaction extension pipelines are configured using the generic parameters. + /// + /// # Example + /// + /// ``` + /// use sp_runtime::traits::{MultiVersion, PipelineAtVers}; + /// + /// struct PaymentExt; + /// struct PaymentExtV2; + /// struct NonceExt; + /// + /// type ExtV1 = PipelineAtVers<1, (NonceExt, PaymentExt)>; + /// type ExtV4 = PipelineAtVers<4, (NonceExt, PaymentExtV2)>; + /// + /// /// The transaction extension pipeline that supports both version 1 and 4. + /// type TransactionExtension = MultiVersion; + /// ``` + #[allow(private_interfaces)] + #[derive(PartialEq, Eq, Clone, Debug, TypeInfo)] + pub enum MultiVersion< + $( + $variant = InvalidVersion, + )* + > { + $( + /// The transaction extension pipeline of a specific version. + $variant($variant), + )* + } + + impl<$( $variant: PipelineVersion, )*> PipelineVersion for MultiVersion<$( $variant, )*> { + fn version(&self) -> u8 { + match self { + $( + MultiVersion::$variant(v) => v.version(), + )* + } + } + } + + // It encodes without the variant index. + impl<$( $variant: Encode, )*> Encode for MultiVersion<$( $variant, )*> { + fn size_hint(&self) -> usize { + match self { + $( + MultiVersion::$variant(v) => v.size_hint(), + )* + } + } + fn encode(&self) -> Vec { + match self { + $( + MultiVersion::$variant(v) => v.encode(), + )* + } + } + fn encode_to(&self, dest: &mut CodecOutput) { + match self { + $( + MultiVersion::$variant(v) => v.encode_to(dest), + )* + } + } + fn encoded_size(&self) -> usize { + match self { + $( + MultiVersion::$variant(v) => v.encoded_size(), + )* + } + } + fn using_encoded FunctionResult>( + &self, + f: Function + ) -> FunctionResult { + match self { + $( + MultiVersion::$variant(v) => v.using_encoded(f), + )* + } + } + } + + // It decodes from a specified version. + impl<$( $variant: DecodeWithVersion + MultiVersionItem, )*> + DecodeWithVersion for MultiVersion<$( $variant, )*> + { + fn decode_with_version( + extension_version: u8, + input: &mut CodecInput, + ) -> Result { + $( + // Here we could try all variants without checking for the version, + // but the error would be less informative. + // Otherwise we could change the trait `DecodeWithVersion` to return an enum of + // 3 variants: ok, error and invalid_version. + if $variant::VERSION == Some(extension_version) { + return Ok(MultiVersion::$variant($variant::decode_with_version(extension_version, input)?)); + } + )* + + Err(codec::Error::from("Invalid extension version")) + } + } + + impl<$( $variant: DecodeWithVersionWithMemTracking + MultiVersionItem, )*> + DecodeWithVersionWithMemTracking for MultiVersion<$( $variant, )*> + {} + + impl<$( $variant: Pipeline + MultiVersionItem, )* Call: Dispatchable> + Pipeline for MultiVersion<$( $variant, )*> + { + fn build_metadata(builder: &mut PipelineMetadataBuilder) { + $( + $variant::build_metadata(builder); + )* + } + fn validate_only( + &self, + origin: DispatchOriginOf, + call: &Call, + info: &DispatchInfoOf, + len: usize, + source: TransactionSource, + ) -> Result { + match self { + $( + MultiVersion::$variant(v) => v.validate_only(origin, call, info, len, source), + )* + } + } + fn dispatch_transaction( + self, + origin: DispatchOriginOf, + call: Call, + info: &DispatchInfoOf, + len: usize, + ) -> crate::ApplyExtrinsicResultWithInfo> { + match self { + $( + MultiVersion::$variant(v) => v.dispatch_transaction(origin, call, info, len), + )* + } + } + fn weight(&self, call: &Call) -> Weight { + match self { + $( + MultiVersion::$variant(v) => v.weight(call), + )* + } + } + } + }; +} + +declare_multi_version_enum! { + A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + traits::{ + AsTransactionAuthorizedOrigin, DecodeWithVersion, DispatchInfoOf, Dispatchable, + Implication, Pipeline, PipelineVersion, TransactionExtension, TransactionSource, + ValidateResult, + }, + transaction_validity::{InvalidTransaction, TransactionValidityError, ValidTransaction}, + DispatchError, + }; + use codec::{Decode, DecodeWithMemTracking, Encode}; + use core::fmt::Debug; + use scale_info::TypeInfo; + use sp_weights::Weight; + + // -------------------------------------------------------- + // A mock call type and origin used for testing + // -------------------------------------------------------- + #[derive(Clone, Debug, Encode, Decode, PartialEq, Eq, TypeInfo)] + pub struct MockCall(pub u32); + + #[derive(Clone, Debug)] + pub struct MockOrigin(pub u8); + + impl AsTransactionAuthorizedOrigin for MockOrigin { + fn is_transaction_authorized(&self) -> bool { + // Let's say any origin != 0 is authorized + self.0 != 0 + } + } + + impl Dispatchable for MockCall { + type RuntimeOrigin = MockOrigin; + type Config = (); + type Info = (); + type PostInfo = (); + + fn dispatch( + self, + origin: Self::RuntimeOrigin, + ) -> crate::DispatchResultWithInfo { + // If the origin is 0, dispatch fails. + // Also, if the call is 0, dispatch fails. + if origin.0 == 0 { + return Err(DispatchError::Other("Unauthorized origin=0").into()); + } + if self.0 == 0 { + return Err(DispatchError::Other("call=0").into()); + } + Ok(Default::default()) + } + } + + // -------------------------------------------------------- + // Let's define two single-version pipelines with versions 4 and 7 + // -------------------------------------------------------- + + // A single-version extension pipeline that "succeeds" only if token != 0 + #[derive(Clone, Debug, Encode, Decode, DecodeWithMemTracking, PartialEq, Eq, TypeInfo)] + pub struct SimpleExtensionV4 { + pub token: u8, + pub declared_weight: u64, + } + + impl TransactionExtension for SimpleExtensionV4 { + const IDENTIFIER: &'static str = "SimpleExtV4"; + type Implicit = (); + type Val = (); + type Pre = (); + + fn implicit(&self) -> Result { + Ok(()) + } + + fn weight(&self, _call: &MockCall) -> Weight { + Weight::from_parts(self.declared_weight, 0) + } + + fn validate( + &self, + origin: MockOrigin, + _call: &MockCall, + _info: &DispatchInfoOf, + _len: usize, + _self_implicit: Self::Implicit, + _inherited_implication: &impl Implication, + _source: TransactionSource, + ) -> ValidateResult { + if self.token == 0 { + Err(InvalidTransaction::Custom(44).into()) + } else { + Ok((ValidTransaction::default(), (), origin)) + } + } + + fn prepare( + self, + _val: Self::Val, + _origin: &MockOrigin, + _call: &MockCall, + _info: &DispatchInfoOf, + _len: usize, + ) -> Result { + Ok(()) + } + } + + pub type PipelineV4 = PipelineAtVers<4, SimpleExtensionV4>; + + // Another single-version extension pipeline, version=7 + #[derive(Clone, Debug, Encode, Decode, DecodeWithMemTracking, PartialEq, Eq, TypeInfo)] + pub struct SimpleExtensionV7 { + pub token: u8, + pub declared_weight: u64, + } + + impl TransactionExtension for SimpleExtensionV7 { + const IDENTIFIER: &'static str = "SimpleExtV7"; + type Implicit = (); + type Val = (); + type Pre = (); + + fn implicit(&self) -> Result { + Ok(()) + } + + fn weight(&self, _call: &MockCall) -> Weight { + Weight::from_parts(self.declared_weight, 0) + } + + fn validate( + &self, + origin: MockOrigin, + _call: &MockCall, + _info: &DispatchInfoOf, + _len: usize, + _self_implicit: Self::Implicit, + _inherited_implication: &impl Implication, + _source: TransactionSource, + ) -> ValidateResult { + if self.token == 0 { + Err(InvalidTransaction::Custom(77).into()) + } else { + Ok((ValidTransaction::default(), (), origin)) + } + } + + fn prepare( + self, + _val: Self::Val, + _origin: &MockOrigin, + _call: &MockCall, + _info: &DispatchInfoOf, + _len: usize, + ) -> Result { + Ok(()) + } + } + + pub type PipelineV7 = PipelineAtVers<7, SimpleExtensionV7>; + + // -------------------------------------------------------- + // Our MultiVersion definition under test + // -------------------------------------------------------- + + pub type MyMultiExt = MultiVersion; + + // -------------------------------------------------------- + // Actual tests + // -------------------------------------------------------- + + #[test] + fn decode_with_version_works_for_known_versions() { + // Build a pipeline for version=4 + let pipeline_v4 = PipelineV4::new(SimpleExtensionV4 { token: 99, declared_weight: 123 }); + let encoded_v4 = pipeline_v4.encode(); + let decoded_v4 = MyMultiExt::decode_with_version(4, &mut &encoded_v4[..]) + .expect("decode with version=4"); + let expected_v4 = MultiVersion::A(pipeline_v4); + assert_eq!(decoded_v4, expected_v4); + + // Build a pipeline for version=7 + let pipeline_v7 = PipelineV7::new(SimpleExtensionV7 { token: 55, declared_weight: 777 }); + let encoded_v7 = pipeline_v7.encode(); + let decoded_v7 = MyMultiExt::decode_with_version(7, &mut &encoded_v7[..]) + .expect("decode with version=7"); + let expected_v7 = MultiVersion::B(pipeline_v7); + assert_eq!(decoded_v7, expected_v7); + } + + #[test] + fn decode_with_unknown_version_fails() { + let pipeline_v4 = PipelineV4::new(SimpleExtensionV4 { token: 1, declared_weight: 100 }); + let encoded_v4 = pipeline_v4.encode(); + + // Attempt decode with version=123 => fails + let decode_err = MyMultiExt::decode_with_version(123, &mut &encoded_v4[..]) + .expect_err("decode must fail with unknown version=123"); + assert!(format!("{}", decode_err).contains("Invalid extension version")); + } + + #[test] + fn version_is_correct() { + // The variant "A" is always the first in our MultiVersion and is version=4 + let multi_a = + MyMultiExt::A(PipelineV4::new(SimpleExtensionV4 { token: 1, declared_weight: 10 })); + assert_eq!(multi_a.version(), 4); + + // The variant "B" is version=7 + let multi_b = + MyMultiExt::B(PipelineV7::new(SimpleExtensionV7 { token: 2, declared_weight: 20 })); + assert_eq!(multi_b.version(), 7); + } + + #[test] + fn weight_check_works() { + let multi_a = + MyMultiExt::A(PipelineV4::new(SimpleExtensionV4 { token: 1, declared_weight: 500 })); + let multi_b = + MyMultiExt::B(PipelineV7::new(SimpleExtensionV7 { token: 1, declared_weight: 999 })); + + let call = MockCall(0); + assert_eq!(multi_a.weight(&call).ref_time(), 500); + assert_eq!(multi_b.weight(&call).ref_time(), 999); + } + + #[test] + fn validate_only_logic_works() { + // A with token=0 => invalid + let invalid_a = + MyMultiExt::A(PipelineV4::new(SimpleExtensionV4 { token: 0, declared_weight: 123 })); + let call = MockCall(42); + let validity = invalid_a.validate_only( + MockOrigin(42), + &call, + &Default::default(), + 0, + TransactionSource::Local, + ); + assert_eq!( + validity, + Err(TransactionValidityError::Invalid(InvalidTransaction::Custom(44))) + ); + + // B with token=0 => invalid + let invalid_b = + MyMultiExt::B(PipelineV7::new(SimpleExtensionV7 { token: 0, declared_weight: 456 })); + let validity_b = invalid_b.validate_only( + MockOrigin(42), + &call, + &Default::default(), + 0, + TransactionSource::Local, + ); + assert_eq!( + validity_b, + Err(TransactionValidityError::Invalid(InvalidTransaction::Custom(77))) + ); + + // A with token=some => ok + let valid_a = + MyMultiExt::A(PipelineV4::new(SimpleExtensionV4 { token: 55, declared_weight: 10 })); + let result_ok_a = valid_a.validate_only( + MockOrigin(1), + &call, + &Default::default(), + 0, + TransactionSource::External, + ); + assert!(result_ok_a.is_ok(), "valid scenario for pipeline A"); + } + + #[test] + fn dispatch_transaction_works() { + // "A" with token != 0 => valid + let pipeline_a = PipelineV4::new(SimpleExtensionV4 { token: 33, declared_weight: 1 }); + let multi_a = MyMultiExt::A(pipeline_a); + let call_good = MockCall(42); + multi_a + .dispatch_transaction(MockOrigin(9), call_good.clone(), &Default::default(), 0) + .expect("Should not fail validity") + .expect("Success"); + + // but call=0 => dispatch fails + let fail_res = + MyMultiExt::A(PipelineV4::new(SimpleExtensionV4 { token: 1, declared_weight: 10 })) + .dispatch_transaction(MockOrigin(9), MockCall(0), &Default::default(), 0) + .expect("Should be a valid transaction from viewpoint of extension"); + let block_err = fail_res.expect_err("actual dispatch error"); + assert_eq!(block_err.error, DispatchError::Other("call=0")); + + // "B" scenario + let pipeline_b = PipelineV7::new(SimpleExtensionV7 { token: 2, declared_weight: 99 }); + let multi_b = MyMultiExt::B(pipeline_b); + let outcome_ok = multi_b + .dispatch_transaction(MockOrigin(1), call_good, &Default::default(), 0) + .expect("Should pass validity"); + assert!(outcome_ok.is_ok()); + } +} diff --git a/substrate/primitives/runtime/src/traits/vers_tx_ext/variant.rs b/substrate/primitives/runtime/src/traits/vers_tx_ext/variant.rs new file mode 100644 index 0000000000000..5e7eae325175b --- /dev/null +++ b/substrate/primitives/runtime/src/traits/vers_tx_ext/variant.rs @@ -0,0 +1,434 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Implementation of versioned transaction extension pipeline that aggregate a version 0 and +//! other versions. + +use crate::{ + generic::ExtensionVersion, + traits::{ + AsTransactionAuthorizedOrigin, DecodeWithVersion, DecodeWithVersionWithMemTracking, + DispatchInfoOf, DispatchTransaction, Dispatchable, Pipeline, PipelineAtVers, + PipelineMetadataBuilder, PipelineVersion, PostDispatchInfoOf, TransactionExtension, + }, + transaction_validity::TransactionSource, +}; +use alloc::vec::Vec; +use codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_weights::Weight; + +/// Version 0 of the transaction extension version. +const EXTENSION_V0_VERSION: ExtensionVersion = 0; + +/// A versioned transaction extension pipeline defined with 2 variants: one for the version 0 and +/// one for other versions. +/// +/// The generic `ExtensionOtherVersions` should not re-define a transaction extension pipeline for +/// the version 0, it will be ignored. The transaction extension pipeline for the version 0 is +/// defined by the generic `ExtensionV0`. +#[derive(PartialEq, Eq, Clone, Debug, TypeInfo)] +pub enum ExtensionVariant { + /// A transaction extension pipeline for the version 0. + V0(ExtensionV0), + /// A transaction extension pipeline for other versions. + Other(ExtensionOtherVersions), +} + +impl PipelineVersion + for ExtensionVariant +{ + fn version(&self) -> u8 { + match self { + ExtensionVariant::V0(_) => EXTENSION_V0_VERSION, + ExtensionVariant::Other(ext) => ext.version(), + } + } +} + +impl Encode + for ExtensionVariant +{ + fn encode(&self) -> Vec { + match self { + ExtensionVariant::V0(ext) => ext.encode(), + ExtensionVariant::Other(ext) => ext.encode(), + } + } + fn size_hint(&self) -> usize { + match self { + ExtensionVariant::V0(ext) => ext.size_hint(), + ExtensionVariant::Other(ext) => ext.size_hint(), + } + } + fn encode_to(&self, dest: &mut T) { + match self { + ExtensionVariant::V0(ext) => ext.encode_to(dest), + ExtensionVariant::Other(ext) => ext.encode_to(dest), + } + } + fn encoded_size(&self) -> usize { + match self { + ExtensionVariant::V0(ext) => ext.encoded_size(), + ExtensionVariant::Other(ext) => ext.encoded_size(), + } + } + fn using_encoded R>(&self, f: F) -> R { + match self { + ExtensionVariant::V0(ext) => ext.using_encoded(f), + ExtensionVariant::Other(ext) => ext.using_encoded(f), + } + } +} + +impl DecodeWithVersion + for ExtensionVariant +{ + fn decode_with_version( + extension_version: u8, + input: &mut I, + ) -> Result { + match extension_version { + EXTENSION_V0_VERSION => Ok(ExtensionVariant::V0(Decode::decode(input)?)), + _ => Ok(ExtensionVariant::Other(DecodeWithVersion::decode_with_version( + extension_version, + input, + )?)), + } + } +} + +impl + DecodeWithVersionWithMemTracking for ExtensionVariant +{ +} + +impl< + Call: Dispatchable + Encode, + ExtensionV0: TransactionExtension, + ExtensionOtherVersions: Pipeline, + > Pipeline for ExtensionVariant +where + ::RuntimeOrigin: AsTransactionAuthorizedOrigin, +{ + fn build_metadata(builder: &mut PipelineMetadataBuilder) { + PipelineAtVers::::build_metadata(builder); + ExtensionOtherVersions::build_metadata(builder); + } + fn validate_only( + &self, + origin: super::DispatchOriginOf, + call: &Call, + info: &DispatchInfoOf, + len: usize, + source: TransactionSource, + ) -> Result< + crate::transaction_validity::ValidTransaction, + crate::transaction_validity::TransactionValidityError, + > { + match self { + ExtensionVariant::V0(ext) => ext + .validate_only(origin, call, info, len, source, EXTENSION_V0_VERSION) + .map(|x| x.0), + ExtensionVariant::Other(ext) => ext.validate_only(origin, call, info, len, source), + } + } + fn dispatch_transaction( + self, + origin: super::DispatchOriginOf, + call: Call, + info: &DispatchInfoOf, + len: usize, + ) -> crate::ApplyExtrinsicResultWithInfo> { + match self { + ExtensionVariant::V0(ext) => { + ext.dispatch_transaction(origin, call, info, len, EXTENSION_V0_VERSION) + }, + ExtensionVariant::Other(ext) => ext.dispatch_transaction(origin, call, info, len), + } + } + fn weight(&self, call: &Call) -> Weight { + match self { + ExtensionVariant::V0(ext) => ext.weight(call), + ExtensionVariant::Other(ext) => ext.weight(call), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + traits::{ + AsTransactionAuthorizedOrigin, DispatchInfoOf, Dispatchable, Implication, + PipelineAtVers, TransactionExtension, TransactionSource, ValidateResult, + }, + transaction_validity::{InvalidTransaction, TransactionValidityError, ValidTransaction}, + DispatchError, + }; + use codec::{Decode, DecodeWithMemTracking, Encode}; + use core::fmt::Debug; + use sp_weights::Weight; + + // -------------------------------------------------------------------- + // 1. Mock call and "origin" type + // -------------------------------------------------------------------- + + #[derive(Clone, Debug, Encode, Decode, PartialEq, Eq)] + pub struct MockCall(pub u64); + #[derive(Debug)] + pub struct MockOrigin; + + impl Dispatchable for MockCall { + type RuntimeOrigin = MockOrigin; + type Config = (); + type Info = (); + type PostInfo = (); + + fn dispatch( + self, + _origin: Self::RuntimeOrigin, + ) -> crate::DispatchResultWithInfo { + if self.0 == 0 { + return Err(DispatchError::Other("call is 0").into()); + } + Ok(Default::default()) + } + } + + // We'll implement the AsTransactionAuthorizedOrigin for Option: + impl AsTransactionAuthorizedOrigin for MockOrigin { + fn is_transaction_authorized(&self) -> bool { + true + } + } + + // -------------------------------------------------------------------- + // 2. Mock Extension used as "ExtensionV0" + // -------------------------------------------------------------------- + + /// A trivial extension type for "old-school version 0". + #[derive(Clone, Debug, Encode, Decode, DecodeWithMemTracking, PartialEq, Eq, TypeInfo)] + pub struct ExtV0 { + pub success_token: bool, + pub w: u64, + } + + impl TransactionExtension for ExtV0 { + const IDENTIFIER: &'static str = "OldSchoolV0"; + type Implicit = (); + fn implicit(&self) -> Result { + Ok(()) + } + type Val = (); + type Pre = (); + + fn weight(&self, _call: &MockCall) -> Weight { + Weight::from_parts(self.w, 0) + } + + fn validate( + &self, + origin: MockOrigin, + _call: &MockCall, + _info: &DispatchInfoOf, + _len: usize, + _self_implicit: Self::Implicit, + _inherited_implication: &impl Implication, + _source: TransactionSource, + ) -> ValidateResult { + if !self.success_token { + Err(InvalidTransaction::Custom(99).into()) + } else { + Ok((ValidTransaction::default(), (), origin)) + } + } + + fn prepare( + self, + _val: Self::Val, + _origin: &MockOrigin, + _call: &MockCall, + _info: &DispatchInfoOf, + _len: usize, + ) -> Result { + Ok(()) + } + } + + // -------------------------------------------------------------------- + // 3. Another pipeline that is used for "Other" versions: We'll define a minimal versioned + // pipeline with one version + // -------------------------------------------------------------------- + + /// Another extension for "some version" pipeline. + #[derive(Clone, Debug, Encode, Decode, DecodeWithMemTracking, PartialEq, Eq, TypeInfo)] + pub struct OtherExtension { + pub token: u16, + pub w: u64, + } + + impl TransactionExtension for OtherExtension { + const IDENTIFIER: &'static str = "OtherExtension"; + type Implicit = (); + fn implicit(&self) -> Result { + Ok(()) + } + type Val = (); + type Pre = (); + + fn weight(&self, _call: &MockCall) -> Weight { + Weight::from_parts(self.w, 0) + } + + fn validate( + &self, + origin: MockOrigin, + _call: &MockCall, + _info: &DispatchInfoOf, + _len: usize, + _self_implicit: Self::Implicit, + _inherited_implication: &impl Implication, + _source: TransactionSource, + ) -> ValidateResult { + // If 'token' is 0 => invalid. Else ok. + if self.token == 0 { + return Err(InvalidTransaction::Custom(7).into()); + } + Ok((ValidTransaction::default(), (), origin)) + } + + fn prepare( + self, + _val: Self::Val, + _origin: &MockOrigin, + _call: &MockCall, + _info: &DispatchInfoOf, + _len: usize, + ) -> Result { + Ok(()) + } + } + + type ExtV2 = PipelineAtVers<2, OtherExtension>; + + // -------------------------------------------------------------------- + // Actual unit tests + // -------------------------------------------------------------------- + + type Variant = ExtensionVariant; + + #[test] + fn decode_v0() { + // If extension_version == 0 => decode as V0 + let v0_data = ExtV0 { success_token: true, w: 42 }.encode(); + let candidate = Variant::decode_with_version(0, &mut &v0_data[..]) + .expect("decode with v0 must succeed"); + let ExtensionVariant::V0(ext_v0) = candidate else { panic!("Expected V0 variant") }; + assert!(ext_v0.success_token); + assert_eq!(ext_v0.w, 42); + } + + #[test] + fn decode_other() { + // If extension_version == 2 => decode as Other + let pipeline = ExtV2::new(OtherExtension { token: 9, w: 888 }); + let encoded = pipeline.encode(); + let candidate = Variant::decode_with_version(2, &mut &encoded[..]) + .expect("decode with version=2 => 'Other'"); + let ExtensionVariant::Other(p) = candidate else { panic!("Expected Other variant") }; + assert_eq!(p.extension.token, 9); + assert_eq!(p.extension.w, 888); + } + + #[test] + fn version_check() { + let v0_var: Variant = ExtensionVariant::V0(ExtV0 { success_token: true, w: 1 }); + let other_var: Variant = + ExtensionVariant::Other(ExtV2::new(OtherExtension { token: 1, w: 1 })); + assert_eq!(v0_var.version(), 0); + assert_eq!(other_var.version(), 2); + } + + #[test] + fn weight_check() { + let v0_var: Variant = ExtensionVariant::V0(ExtV0 { success_token: true, w: 100 }); + let other_var: Variant = + ExtensionVariant::Other(ExtV2::new(OtherExtension { token: 2, w: 555 })); + let call = MockCall(123); + + assert_eq!(v0_var.weight(&call).ref_time(), 100); + assert_eq!(other_var.weight(&call).ref_time(), 555); + } + + #[test] + fn validate_only_works() { + { + // v0 + success_token => ok + let v0_var: Variant = ExtensionVariant::V0(ExtV0 { success_token: true, w: 100 }); + let call = MockCall(1); + let valid = v0_var.validate_only( + MockOrigin, + &call, + &Default::default(), + 0, + TransactionSource::External, + ); + assert!(valid.is_ok()); + } + { + // other => token=0 => fail + let var_other: Variant = + ExtensionVariant::Other(ExtV2::new(OtherExtension { token: 0, w: 5 })); + let call = MockCall(1); + let fail = var_other.validate_only( + MockOrigin, + &call, + &Default::default(), + 0, + TransactionSource::Local, + ); + assert_eq!(fail, Err(TransactionValidityError::Invalid(InvalidTransaction::Custom(7)))); + } + } + + #[test] + fn dispatch_transaction_works() { + // We'll do "v0" scenario with success_token => true => valid + let v0_var: Variant = ExtensionVariant::V0(ExtV0 { success_token: true, w: 12 }); + let call = MockCall(42); + let result = v0_var.dispatch_transaction(MockOrigin, call.clone(), &Default::default(), 0); + let extrinsic_outcome = result.expect("Ok(ApplyExtrinsicResultWithInfo)"); + assert!(extrinsic_outcome.is_ok(), "call with origin Some => dispatch Ok"); + + // If call is 0 => call fails at dispatch + let err = Variant::V0(ExtV0 { success_token: true, w: 1 }) + .dispatch_transaction(MockOrigin, MockCall(0), &Default::default(), 0) + .expect("valid") + .expect_err("dispatch error"); + + assert_eq!(err.error, DispatchError::Other("call is 0")); + + // check scenario for "other" too + let var_other: Variant = + ExtensionVariant::Other(ExtV2::new(OtherExtension { token: 5, w: 55 })); + let outcome = var_other + .dispatch_transaction(MockOrigin, call, &Default::default(), 0) + .expect("Should be ok"); + assert!(outcome.is_ok()); + } +}