Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make TransactionExtension tuple of tuple transparent for implication #7028

Merged
merged 11 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions prdoc/pr_7028.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
title: 'Fix implication order in implementation of `TransactionExtension` for tuple'
doc:
- audience:
- Runtime Dev
- Runtime User
description: |-
Before this PR, the implications were different in the pipeline `(A, B, C)` and `((A, B), C)`.
This PR fixes this behavior and make nested tuple transparant, the implication order of tuple of
tuple is now the same as in a single tuple.

For runtime users this mean that the implication can be breaking depending on the pipeline used
in the runtime.

For runtime developers this breaks usage of `TransactionExtension::validate`.
When calling `TransactionExtension::validate` the implication must now implement `Implication`
trait, you can use `TxBaseImplication` to wrap the type and use it as the base implication.
E.g. instead of `&(extension_version, call),` you can write `&TxBaseImplication((extension_version, call))`.

crates:
- name: sp-runtime
bump: major
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ use frame_support::{
use scale_info::{StaticTypeInfo, TypeInfo};
use sp_runtime::{
traits::{
DispatchInfoOf, DispatchOriginOf, PostDispatchInfoOf, TransactionExtension, ValidateResult,
DispatchInfoOf, DispatchOriginOf, Implication, PostDispatchInfoOf, TransactionExtension,
ValidateResult,
},
transaction_validity::TransactionValidityError,
};
Expand Down Expand Up @@ -147,7 +148,7 @@ where
info: &DispatchInfoOf<T::RuntimeCall>,
len: usize,
self_implicit: S::Implicit,
inherited_implication: &impl Encode,
inherited_implication: &impl Implication,
source: TransactionSource,
) -> ValidateResult<Self::Val, T::RuntimeCall> {
if call.is_feeless(&origin) {
Expand Down
3 changes: 2 additions & 1 deletion substrate/primitives/runtime/src/traits/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ use std::str::FromStr;

pub mod transaction_extension;
pub use transaction_extension::{
DispatchTransaction, TransactionExtension, TransactionExtensionMetadata, ValidateResult,
DispatchTransaction, Implication, ImplicationParts, TransactionExtension,
TransactionExtensionMetadata, TxBaseImplication, ValidateResult,
};

/// A lazy value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ where
info,
len,
self.implicit()?,
&(extension_version, call),
&TxBaseImplication((extension_version, call)),
source,
) {
// After validation, some origin must have been authorized.
Expand Down
245 changes: 230 additions & 15 deletions substrate/primitives/runtime/src/traits/transaction_extension/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,59 @@ mod dispatch_transaction;
pub use as_transaction_extension::AsTransactionExtension;
pub use dispatch_transaction::DispatchTransaction;

/// The base implication in a transaction.
///
/// This struct is used to represent the base implication in the transaction, that is
/// the implication not part of any transaction extensions. It usually comprises of the call and
/// the transaction extension version.
///
/// The concept of implication in the transaction extension pipeline is explained in the trait
/// documentation: [`TransactionExtension`].
#[derive(Encode)]
pub struct TxBaseImplication<T>(pub T);

impl<T: Encode> Implication for TxBaseImplication<T> {
fn parts(&self) -> ImplicationParts<&impl Encode, &impl Encode, &impl Encode> {
ImplicationParts { base: self, explicit: &(), implicit: &() }
}
}

/// The implication in a transaction.
///
/// The concept of implication in the transaction extension pipeline is explained in the trait
/// documentation: [`TransactionExtension`].
#[derive(Encode)]
pub struct ImplicationParts<Base, Explicit, Implicit> {
/// The base implication, that is implication not part of any transaction extension, usually
/// the call and the transaction extension version,
gui1117 marked this conversation as resolved.
Show resolved Hide resolved
pub base: Base,
/// The explicit implication in transaction extensions,
gui1117 marked this conversation as resolved.
Show resolved Hide resolved
pub explicit: Explicit,
/// The implicit implication in transaction extensions.
pub implicit: Implicit,
}

impl<Base: Encode, Explicit: Encode, Implicit: Encode> Implication
for ImplicationParts<Base, Explicit, Implicit>
{
fn parts(&self) -> ImplicationParts<&impl Encode, &impl Encode, &impl Encode> {
ImplicationParts { base: &self.base, explicit: &self.explicit, implicit: &self.implicit }
}
}

/// Interface of implications in the transaction extension pipeline.
///
/// Implications can be encoded, this is useful for checking signature on the implications.
/// Implications can be split into parts, this allow to destructure and restructure the
/// implications, this is useful for nested pipeline.
///
/// The concept of implication in the transaction extension pipeline is explained in the trait
/// documentation: [`TransactionExtension`].
pub trait Implication: Encode {
gui1117 marked this conversation as resolved.
Show resolved Hide resolved
/// Destructure the implication into its parts.
fn parts(&self) -> ImplicationParts<&impl Encode, &impl Encode, &impl Encode>;
}

/// Shortcut for the result value of the `validate` function.
pub type ValidateResult<Val, Call> =
Result<(ValidTransaction, Val, DispatchOriginOf<Call>), TransactionValidityError>;
Expand Down Expand Up @@ -244,7 +297,7 @@ pub trait TransactionExtension<Call: Dispatchable>:
info: &DispatchInfoOf<Call>,
len: usize,
self_implicit: Self::Implicit,
inherited_implication: &impl Encode,
inherited_implication: &impl Implication,
source: TransactionSource,
) -> ValidateResult<Self::Val, Call>;

Expand Down Expand Up @@ -499,7 +552,7 @@ impl<Call: Dispatchable> TransactionExtension<Call> for Tuple {
info: &DispatchInfoOf<Call>,
len: usize,
self_implicit: Self::Implicit,
inherited_implication: &impl Encode,
inherited_implication: &impl Implication,
source: TransactionSource,
) -> Result<
(ValidTransaction, Self::Val, <Call as Dispatchable>::RuntimeOrigin),
Expand All @@ -510,23 +563,20 @@ impl<Call: Dispatchable> TransactionExtension<Call> for Tuple {
let following_explicit_implications = for_tuples!( ( #( &self.Tuple ),* ) );
let following_implicit_implications = self_implicit;

let implication_parts = inherited_implication.parts();

for_tuples!(#(
// Implication of this pipeline element not relevant for later items, so we pop it.
let (_item, following_explicit_implications) = following_explicit_implications.pop_front();
let (item_implicit, following_implicit_implications) = following_implicit_implications.pop_front();
let (item_valid, item_val, origin) = {
let implications = (
// The first is the implications born of the fact we return the mutated
// origin.
inherited_implication,
// This is the explicitly made implication born of the fact the new origin is
// passed into the next items in this pipeline-tuple.
&following_explicit_implications,
// This is the implicitly made implication born of the fact the new origin is
// passed into the next items in this pipeline-tuple.
&following_implicit_implications,
);
Tuple.validate(origin, call, info, len, item_implicit, &implications, source)?
Tuple.validate(origin, call, info, len, item_implicit,
&ImplicationParts {
base: implication_parts.base,
explicit: (&following_explicit_implications, implication_parts.explicit),
implicit: (&following_implicit_implications, implication_parts.implicit),
Copy link
Member

Choose a reason for hiding this comment

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

This keeps the order correct as
((Base1, Base2..), (Explicit1, Explicit2..), (Implicit1, Implicit2..)) instead of
((Base1, Explicit1, Implicit1), (Base2, Explicit2, Implicit2)...)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On master the order is:

  • For the pipeline (A, B, C, D) the implication in the validation of A is (Base, ExpB, ExpC, ExpD, ImpB, ImpC, ImpD).
  • For the pipeline ((A, B), C, D) the implication in the validation of A is ((Base, ExpC, ExpD, ImpC, ImpD), ExpB, ImpB). Because when calling the validation of the inner tuple, the outer give the whole (Base, ExpC, ExpD, ImpC, ImpD) as inherited implication, and the inner tuple can't destructure and restructure it.

With this PR the order is in both case (Base, ExpB, ExpC, ExpD, ImpB, ImpC, ImpD).

},
source)?
};
let valid = valid.combine_with(item_valid);
let val = val.push_back(item_val);
Expand Down Expand Up @@ -620,7 +670,7 @@ impl<Call: Dispatchable> TransactionExtension<Call> for () {
_info: &DispatchInfoOf<Call>,
_len: usize,
_self_implicit: Self::Implicit,
_inherited_implication: &impl Encode,
_inherited_implication: &impl Implication,
_source: TransactionSource,
) -> Result<
(ValidTransaction, (), <Call as Dispatchable>::RuntimeOrigin),
Expand All @@ -639,3 +689,168 @@ impl<Call: Dispatchable> TransactionExtension<Call> for () {
Ok(())
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_implications_on_nested_structure() {
use scale_info::TypeInfo;
use std::cell::RefCell;

#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo)]
struct MockExtension {
also_implicit: u8,
explicit: u8,
}

const CALL_IMPLICIT: u8 = 23;

thread_local! {
static COUNTER: RefCell<u8> = RefCell::new(1);
}

impl TransactionExtension<()> for MockExtension {
const IDENTIFIER: &'static str = "MockExtension";
type Implicit = u8;
fn implicit(&self) -> Result<Self::Implicit, TransactionValidityError> {
Ok(self.also_implicit)
}
type Val = ();
type Pre = ();
fn weight(&self, _call: &()) -> Weight {
Weight::zero()
}
fn prepare(
self,
_val: Self::Val,
_origin: &DispatchOriginOf<()>,
_call: &(),
_info: &DispatchInfoOf<()>,
_len: usize,
) -> Result<Self::Pre, TransactionValidityError> {
Ok(())
}
fn validate(
&self,
origin: DispatchOriginOf<()>,
_call: &(),
_info: &DispatchInfoOf<()>,
_len: usize,
self_implicit: Self::Implicit,
inherited_implication: &impl Implication,
_source: TransactionSource,
) -> ValidateResult<Self::Val, ()> {
COUNTER.with(|c| {
let mut counter = c.borrow_mut();

assert_eq!(self_implicit, *counter);
assert_eq!(
self,
&MockExtension { also_implicit: *counter, explicit: *counter + 1 }
);

// Implications must be call then 1 to 22 then 1 to 22 odd.
let mut assert_implications = Vec::new();
assert_implications.push(CALL_IMPLICIT);
for i in *counter + 2..23 {
assert_implications.push(i);
}
for i in *counter + 2..23 {
if i % 2 == 1 {
assert_implications.push(i);
}
}
assert_eq!(inherited_implication.encode(), assert_implications,);
gui1117 marked this conversation as resolved.
Show resolved Hide resolved

*counter += 2;
});
Ok((ValidTransaction::default(), (), origin))
}
fn post_dispatch_details(
_pre: Self::Pre,
_info: &DispatchInfoOf<()>,
_post_info: &PostDispatchInfoOf<()>,
_len: usize,
_result: &DispatchResult,
) -> Result<Weight, TransactionValidityError> {
Ok(Weight::zero())
}
}

// Test for one nested structure

let ext = (
MockExtension { also_implicit: 1, explicit: 2 },
MockExtension { also_implicit: 3, explicit: 4 },
(
MockExtension { also_implicit: 5, explicit: 6 },
MockExtension { also_implicit: 7, explicit: 8 },
(
MockExtension { also_implicit: 9, explicit: 10 },
MockExtension { also_implicit: 11, explicit: 12 },
),
MockExtension { also_implicit: 13, explicit: 14 },
MockExtension { also_implicit: 15, explicit: 16 },
),
MockExtension { also_implicit: 17, explicit: 18 },
(MockExtension { also_implicit: 19, explicit: 20 },),
MockExtension { also_implicit: 21, explicit: 22 },
);

let implicit = ext.implicit().unwrap();

let res = ext
.validate(
(),
&(),
&DispatchInfoOf::<()>::default(),
0,
implicit,
&TxBaseImplication(CALL_IMPLICIT),
TransactionSource::Local,
)
.expect("valid");

assert_eq!(res.0, ValidTransaction::default());

// Test for another nested structure

COUNTER.with(|c| {
*c.borrow_mut() = 1;
});

let ext = (
MockExtension { also_implicit: 1, explicit: 2 },
MockExtension { also_implicit: 3, explicit: 4 },
MockExtension { also_implicit: 5, explicit: 6 },
MockExtension { also_implicit: 7, explicit: 8 },
MockExtension { also_implicit: 9, explicit: 10 },
MockExtension { also_implicit: 11, explicit: 12 },
(
MockExtension { also_implicit: 13, explicit: 14 },
MockExtension { also_implicit: 15, explicit: 16 },
MockExtension { also_implicit: 17, explicit: 18 },
MockExtension { also_implicit: 19, explicit: 20 },
MockExtension { also_implicit: 21, explicit: 22 },
),
);

let implicit = ext.implicit().unwrap();

let res = ext
.validate(
(),
&(),
&DispatchInfoOf::<()>::default(),
0,
implicit,
&TxBaseImplication(CALL_IMPLICIT),
TransactionSource::Local,
)
.expect("valid");

assert_eq!(res.0, ValidTransaction::default());
}
}
Loading