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

Introduce TransactionExtensionPipeline to use instead of tuple for pipeline with more than 12 elements #6571

Closed
wants to merge 14 commits into from
11 changes: 11 additions & 0 deletions prdoc/pr_6571.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
title: 'Introduce `TransactionExtensionPipeline` to use instead of tuple for pipeline with more than 12 elements'
doc:
- audience: Runtime Dev
description: |-
Introduce a new type `TransactionExtensionPipeline` that has 32 generics that default to `()`.
This type accepts up to 32 transaction extensions. It can be used in place of tuple which is
limited to 12 transaction extensions.

crates:
- name: sp-runtime
bump: minor
272 changes: 272 additions & 0 deletions substrate/frame/support/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1580,3 +1580,275 @@ mod extension_weight_tests {
});
}
}

// Same tests as extension_weight_tests but using a pipeline instead of a tuple.
#[cfg(test)]
// Do not complain about unused `dispatch` and `dispatch_aux`.
#[allow(dead_code)]
mod extension_weight_tests_with_pipeline {
use crate::assert_ok;

use super::*;
use sp_core::parameter_types;
use sp_runtime::{
generic::{self, ExtrinsicFormat},
traits::{
Applyable, BlakeTwo256, DispatchTransaction, TransactionExtension,
TransactionExtensionPipeline,
},
};
use sp_weights::RuntimeDbWeight;
use test_extensions::{ActualWeightIs, FreeIfUnder, HalfCostIf};

use super::weight_tests::frame_system;
use frame_support::construct_runtime;

pub type TxExtension = TransactionExtensionPipeline<HalfCostIf, FreeIfUnder, ActualWeightIs>;
pub type UncheckedExtrinsic = generic::UncheckedExtrinsic<u64, RuntimeCall, (), TxExtension>;
pub type Header = generic::Header<BlockNumber, BlakeTwo256>;
pub type Block = generic::Block<Header, UncheckedExtrinsic>;
pub type AccountId = u64;
pub type Balance = u32;
pub type BlockNumber = u32;

construct_runtime!(
pub enum ExtRuntime {
System: frame_system,
}
);

impl frame_system::Config for ExtRuntime {
type Block = Block;
type AccountId = AccountId;
type Balance = Balance;
type BaseCallFilter = crate::traits::Everything;
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type RuntimeTask = RuntimeTask;
type DbWeight = DbWeight;
type PalletInfo = PalletInfo;
}

parameter_types! {
pub const DbWeight: RuntimeDbWeight = RuntimeDbWeight {
read: 100,
write: 1000,
};
}

pub struct ExtBuilder {}

impl Default for ExtBuilder {
fn default() -> Self {
Self {}
}
}

impl ExtBuilder {
pub fn build(self) -> sp_io::TestExternalities {
let mut ext = sp_io::TestExternalities::new(Default::default());
ext.execute_with(|| {});
ext
}

pub fn build_and_execute(self, test: impl FnOnce() -> ()) {
self.build().execute_with(|| {
test();
})
}
}

#[test]
fn no_post_dispatch_with_no_refund() {
ExtBuilder::default().build_and_execute(|| {
let call = RuntimeCall::System(frame_system::Call::<ExtRuntime>::f99 {});
let ext: TxExtension = (HalfCostIf(false), FreeIfUnder(1500), ActualWeightIs(0)).into();
let uxt = UncheckedExtrinsic::new_signed(call.clone(), 0, (), ext.clone());
assert_eq!(uxt.extension_weight(), Weight::from_parts(600, 0));

let mut info = call.get_dispatch_info();
assert_eq!(info.total_weight(), Weight::from_parts(1000, 0));
info.extension_weight = ext.weight(&call);
let (pre, _) = ext.validate_and_prepare(Some(0).into(), &call, &info, 0, 0).unwrap();
let res = call.dispatch(Some(0).into());
let mut post_info = res.unwrap();
assert!(post_info.actual_weight.is_none());
assert_ok!(<TxExtension as TransactionExtension<RuntimeCall>>::post_dispatch(
pre,
&info,
&mut post_info,
0,
&Ok(()),
));
assert!(post_info.actual_weight.is_none());
});
}

#[test]
fn no_post_dispatch_refunds_when_dispatched() {
ExtBuilder::default().build_and_execute(|| {
let call = RuntimeCall::System(frame_system::Call::<ExtRuntime>::f99 {});
let ext: TxExtension = (HalfCostIf(true), FreeIfUnder(100), ActualWeightIs(0)).into();
let uxt = UncheckedExtrinsic::new_signed(call.clone(), 0, (), ext.clone());
assert_eq!(uxt.extension_weight(), Weight::from_parts(600, 0));

let mut info = call.get_dispatch_info();
assert_eq!(info.total_weight(), Weight::from_parts(1000, 0));
info.extension_weight = ext.weight(&call);
let post_info =
ext.dispatch_transaction(Some(0).into(), call, &info, 0, 0).unwrap().unwrap();
// 1000 call weight + 50 + 200 + 0
assert_eq!(post_info.actual_weight, Some(Weight::from_parts(1250, 0)));
});
}

#[test]
fn post_dispatch_with_refunds() {
ExtBuilder::default().build_and_execute(|| {
let call = RuntimeCall::System(frame_system::Call::<ExtRuntime>::f100 {});
// First testcase
let ext: TxExtension = (HalfCostIf(false), FreeIfUnder(2000), ActualWeightIs(0)).into();
let uxt = UncheckedExtrinsic::new_signed(call.clone(), 0, (), ext.clone());
assert_eq!(uxt.extension_weight(), Weight::from_parts(600, 0));

let mut info = call.get_dispatch_info();
assert_eq!(info.call_weight, Weight::from_parts(1000, 0));
info.extension_weight = ext.weight(&call);
assert_eq!(info.total_weight(), Weight::from_parts(1600, 0));
let (pre, _) = ext.validate_and_prepare(Some(0).into(), &call, &info, 0, 0).unwrap();
let res = call.clone().dispatch(Some(0).into());
let mut post_info = res.unwrap();
// 500 actual call weight
assert_eq!(post_info.actual_weight, Some(Weight::from_parts(500, 0)));
// add the 600 worst case extension weight
post_info.set_extension_weight(&info);
// extension weight should be refunded
assert_ok!(<TxExtension as TransactionExtension<RuntimeCall>>::post_dispatch(
pre,
&info,
&mut post_info,
0,
&Ok(()),
));
// 500 actual call weight + 100 + 0 + 0
assert_eq!(post_info.actual_weight, Some(Weight::from_parts(600, 0)));

// Second testcase
let ext: TxExtension =
(HalfCostIf(false), FreeIfUnder(1100), ActualWeightIs(200)).into();
let (pre, _) = ext.validate_and_prepare(Some(0).into(), &call, &info, 0, 0).unwrap();
let res = call.clone().dispatch(Some(0).into());
let mut post_info = res.unwrap();
// 500 actual call weight
assert_eq!(post_info.actual_weight, Some(Weight::from_parts(500, 0)));
// add the 600 worst case extension weight
post_info.set_extension_weight(&info);
// extension weight should be refunded
assert_ok!(<TxExtension as TransactionExtension<RuntimeCall>>::post_dispatch(
pre,
&info,
&mut post_info,
0,
&Ok(()),
));
// 500 actual call weight + 100 + 200 + 200
assert_eq!(post_info.actual_weight, Some(Weight::from_parts(1000, 0)));

// Third testcase
let ext: TxExtension =
(HalfCostIf(true), FreeIfUnder(1060), ActualWeightIs(200)).into();
let (pre, _) = ext.validate_and_prepare(Some(0).into(), &call, &info, 0, 0).unwrap();
let res = call.clone().dispatch(Some(0).into());
let mut post_info = res.unwrap();
// 500 actual call weight
assert_eq!(post_info.actual_weight, Some(Weight::from_parts(500, 0)));
// add the 600 worst case extension weight
post_info.set_extension_weight(&info);
// extension weight should be refunded
assert_ok!(<TxExtension as TransactionExtension<RuntimeCall>>::post_dispatch(
pre,
&info,
&mut post_info,
0,
&Ok(()),
));
// 500 actual call weight + 50 + 0 + 200
assert_eq!(post_info.actual_weight, Some(Weight::from_parts(750, 0)));

// Fourth testcase
let ext: TxExtension =
(HalfCostIf(false), FreeIfUnder(100), ActualWeightIs(300)).into();
let (pre, _) = ext.validate_and_prepare(Some(0).into(), &call, &info, 0, 0).unwrap();
let res = call.clone().dispatch(Some(0).into());
let mut post_info = res.unwrap();
// 500 actual call weight
assert_eq!(post_info.actual_weight, Some(Weight::from_parts(500, 0)));
// add the 600 worst case extension weight
post_info.set_extension_weight(&info);
// extension weight should be refunded
assert_ok!(<TxExtension as TransactionExtension<RuntimeCall>>::post_dispatch(
pre,
&info,
&mut post_info,
0,
&Ok(()),
));
// 500 actual call weight + 100 + 200 + 300
assert_eq!(post_info.actual_weight, Some(Weight::from_parts(1100, 0)));
});
}

#[test]
fn checked_extrinsic_apply() {
ExtBuilder::default().build_and_execute(|| {
let call = RuntimeCall::System(frame_system::Call::<ExtRuntime>::f100 {});
// First testcase
let ext: TxExtension = (HalfCostIf(false), FreeIfUnder(2000), ActualWeightIs(0)).into();
let xt = CheckedExtrinsic {
format: ExtrinsicFormat::Signed(0, ext.clone()),
function: call.clone(),
};
assert_eq!(xt.extension_weight(), Weight::from_parts(600, 0));
let mut info = call.get_dispatch_info();
assert_eq!(info.call_weight, Weight::from_parts(1000, 0));
info.extension_weight = ext.weight(&call);
assert_eq!(info.total_weight(), Weight::from_parts(1600, 0));
let post_info = xt.apply::<ExtRuntime>(&info, 0).unwrap().unwrap();
// 500 actual call weight + 100 + 0 + 0
assert_eq!(post_info.actual_weight, Some(Weight::from_parts(600, 0)));

// Second testcase
let ext: TxExtension =
(HalfCostIf(false), FreeIfUnder(1100), ActualWeightIs(200)).into();
let xt = CheckedExtrinsic {
format: ExtrinsicFormat::Signed(0, ext),
function: call.clone(),
};
let post_info = xt.apply::<ExtRuntime>(&info, 0).unwrap().unwrap();
// 500 actual call weight + 100 + 200 + 200
assert_eq!(post_info.actual_weight, Some(Weight::from_parts(1000, 0)));

// Third testcase
let ext: TxExtension =
(HalfCostIf(true), FreeIfUnder(1060), ActualWeightIs(200)).into();
let xt = CheckedExtrinsic {
format: ExtrinsicFormat::Signed(0, ext),
function: call.clone(),
};
let post_info = xt.apply::<ExtRuntime>(&info, 0).unwrap().unwrap();
// 500 actual call weight + 50 + 0 + 200
assert_eq!(post_info.actual_weight, Some(Weight::from_parts(750, 0)));

// Fourth testcase
let ext: TxExtension =
(HalfCostIf(false), FreeIfUnder(100), ActualWeightIs(300)).into();
let xt = CheckedExtrinsic {
format: ExtrinsicFormat::Signed(0, ext),
function: call.clone(),
};
let post_info = xt.apply::<ExtRuntime>(&info, 0).unwrap().unwrap();
// 500 actual call weight + 100 + 200 + 300
assert_eq!(post_info.actual_weight, Some(Weight::from_parts(1100, 0)));
});
}
}
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, TransactionExtension, TransactionExtensionMetadata,
TransactionExtensionPipeline, TransactionExtensionPipelineImplicit, ValidateResult,
};

/// A lazy value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,13 @@ use super::{

mod as_transaction_extension;
mod dispatch_transaction;
mod transaction_extension_pipeline;
#[allow(deprecated)]
pub use as_transaction_extension::AsTransactionExtension;
pub use dispatch_transaction::DispatchTransaction;
pub use transaction_extension_pipeline::{
NoTxExt, TransactionExtensionPipeline, TransactionExtensionPipelineImplicit,
};

/// Shortcut for the result value of the `validate` function.
pub type ValidateResult<Val, Call> =
Expand Down Expand Up @@ -605,6 +609,7 @@ impl<Call: Dispatchable> TransactionExtension<Call> for Tuple {
impl<Call: Dispatchable> TransactionExtension<Call> for () {
const IDENTIFIER: &'static str = "UnitTransactionExtension";
type Implicit = ();
#[inline]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TransactionExtensionPipeline take 32 generics that defaults to (), so I inlined methods here so hopefully the code gets optimized by the compiler.

fn implicit(&self) -> sp_std::result::Result<Self::Implicit, TransactionValidityError> {
Ok(())
}
Expand Down
Loading
Loading