use crate::{Trait, Module};
use codec::{Encode, Decode};
use sp_runtime::{
traits::{SignedExtension, DispatchInfoOf, Dispatchable, PostDispatchInfoOf, Printable},
transaction_validity::{
ValidTransaction, TransactionValidityError, InvalidTransaction, TransactionValidity,
TransactionPriority,
},
Perbill, DispatchResult,
};
use frame_support::{
traits::{Get},
weights::{PostDispatchInfo, DispatchInfo, DispatchClass, priority::FrameTransactionPriority},
StorageValue,
};
#[derive(Encode, Decode, Clone, Eq, PartialEq, Default)]
pub struct CheckWeight<T: Trait + Send + Sync>(sp_std::marker::PhantomData<T>);
impl<T: Trait + Send + Sync> CheckWeight<T> where
T::Call: Dispatchable<Info=DispatchInfo, PostInfo=PostDispatchInfo>
{
fn get_dispatch_limit_ratio(class: DispatchClass) -> Perbill {
match class {
DispatchClass::Operational | DispatchClass::Mandatory
=> <Perbill as sp_runtime::PerThing>::one(),
DispatchClass::Normal => T::AvailableBlockRatio::get(),
}
}
fn check_extrinsic_weight(
info: &DispatchInfoOf<T::Call>,
) -> Result<(), TransactionValidityError> {
match info.class {
DispatchClass::Mandatory => Ok(()),
DispatchClass::Normal => {
let maximum_weight = T::MaximumExtrinsicWeight::get();
let extrinsic_weight = info.weight.saturating_add(T::ExtrinsicBaseWeight::get());
if extrinsic_weight > maximum_weight {
Err(InvalidTransaction::ExhaustsResources.into())
} else {
Ok(())
}
},
DispatchClass::Operational => {
let maximum_weight = T::MaximumBlockWeight::get();
let operational_limit =
Self::get_dispatch_limit_ratio(DispatchClass::Operational) * maximum_weight;
let operational_limit =
operational_limit.saturating_sub(T::BlockExecutionWeight::get());
let extrinsic_weight = info.weight.saturating_add(T::ExtrinsicBaseWeight::get());
if extrinsic_weight > operational_limit {
Err(InvalidTransaction::ExhaustsResources.into())
} else {
Ok(())
}
},
}
}
fn check_block_weight(
info: &DispatchInfoOf<T::Call>,
) -> Result<crate::weights::ExtrinsicsWeight, TransactionValidityError> {
let maximum_weight = T::MaximumBlockWeight::get();
let mut all_weight = Module::<T>::block_weight();
match info.class {
DispatchClass::Mandatory => {
let extrinsic_weight = info.weight.saturating_add(T::ExtrinsicBaseWeight::get());
all_weight.add(extrinsic_weight, DispatchClass::Mandatory);
Ok(all_weight)
},
DispatchClass::Normal => {
let normal_limit = Self::get_dispatch_limit_ratio(DispatchClass::Normal) * maximum_weight;
let extrinsic_weight = info.weight.checked_add(T::ExtrinsicBaseWeight::get())
.ok_or(InvalidTransaction::ExhaustsResources)?;
all_weight.checked_add(extrinsic_weight, DispatchClass::Normal)
.map_err(|_| InvalidTransaction::ExhaustsResources)?;
if all_weight.get(DispatchClass::Normal) > normal_limit {
Err(InvalidTransaction::ExhaustsResources.into())
} else {
Ok(all_weight)
}
},
DispatchClass::Operational => {
let operational_limit = Self::get_dispatch_limit_ratio(DispatchClass::Operational) * maximum_weight;
let normal_limit = Self::get_dispatch_limit_ratio(DispatchClass::Normal) * maximum_weight;
let operational_space = operational_limit.saturating_sub(normal_limit);
let extrinsic_weight = info.weight.checked_add(T::ExtrinsicBaseWeight::get())
.ok_or(InvalidTransaction::ExhaustsResources)?;
all_weight.checked_add(extrinsic_weight, DispatchClass::Operational)
.map_err(|_| InvalidTransaction::ExhaustsResources)?;
if all_weight.total() <= maximum_weight ||
all_weight.get(DispatchClass::Operational) <= operational_space {
Ok(all_weight)
} else {
Err(InvalidTransaction::ExhaustsResources.into())
}
}
}
}
fn check_block_length(
info: &DispatchInfoOf<T::Call>,
len: usize,
) -> Result<u32, TransactionValidityError> {
let current_len = Module::<T>::all_extrinsics_len();
let maximum_len = T::MaximumBlockLength::get();
let limit = Self::get_dispatch_limit_ratio(info.class) * maximum_len;
let added_len = len as u32;
let next_len = current_len.saturating_add(added_len);
if next_len > limit {
Err(InvalidTransaction::ExhaustsResources.into())
} else {
Ok(next_len)
}
}
fn get_priority(info: &DispatchInfoOf<T::Call>) -> TransactionPriority {
match info.class {
DispatchClass::Normal =>
FrameTransactionPriority::Normal(info.weight.into()).into(),
DispatchClass::Operational =>
FrameTransactionPriority::Operational(info.weight.into()).into(),
DispatchClass::Mandatory => TransactionPriority::min_value(),
}
}
pub fn new() -> Self {
Self(Default::default())
}
fn do_pre_dispatch(
info: &DispatchInfoOf<T::Call>,
len: usize,
) -> Result<(), TransactionValidityError> {
let next_len = Self::check_block_length(info, len)?;
let next_weight = Self::check_block_weight(info)?;
Self::check_extrinsic_weight(info)?;
crate::AllExtrinsicsLen::put(next_len);
crate::BlockWeight::put(next_weight);
Ok(())
}
fn do_validate(
info: &DispatchInfoOf<T::Call>,
len: usize,
) -> TransactionValidity {
let _ = Self::check_block_length(info, len)?;
Self::check_extrinsic_weight(info)?;
Ok(ValidTransaction { priority: Self::get_priority(info), ..Default::default() })
}
}
impl<T: Trait + Send + Sync> SignedExtension for CheckWeight<T> where
T::Call: Dispatchable<Info=DispatchInfo, PostInfo=PostDispatchInfo>
{
type AccountId = T::AccountId;
type Call = T::Call;
type AdditionalSigned = ();
type Pre = ();
const IDENTIFIER: &'static str = "CheckWeight";
fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> { Ok(()) }
fn pre_dispatch(
self,
_who: &Self::AccountId,
_call: &Self::Call,
info: &DispatchInfoOf<Self::Call>,
len: usize,
) -> Result<(), TransactionValidityError> {
if info.class == DispatchClass::Mandatory {
Err(InvalidTransaction::MandatoryDispatch)?
}
Self::do_pre_dispatch(info, len)
}
fn validate(
&self,
_who: &Self::AccountId,
_call: &Self::Call,
info: &DispatchInfoOf<Self::Call>,
len: usize,
) -> TransactionValidity {
if info.class == DispatchClass::Mandatory {
Err(InvalidTransaction::MandatoryDispatch)?
}
Self::do_validate(info, len)
}
fn pre_dispatch_unsigned(
_call: &Self::Call,
info: &DispatchInfoOf<Self::Call>,
len: usize,
) -> Result<(), TransactionValidityError> {
Self::do_pre_dispatch(info, len)
}
fn validate_unsigned(
_call: &Self::Call,
info: &DispatchInfoOf<Self::Call>,
len: usize,
) -> TransactionValidity {
Self::do_validate(info, len)
}
fn post_dispatch(
_pre: Self::Pre,
info: &DispatchInfoOf<Self::Call>,
post_info: &PostDispatchInfoOf<Self::Call>,
_len: usize,
result: &DispatchResult,
) -> Result<(), TransactionValidityError> {
if let (DispatchClass::Mandatory, Err(e)) = (info.class, result) {
"Bad mandantory".print();
e.print();
Err(InvalidTransaction::BadMandatory)?
}
let unspent = post_info.calc_unspent(info);
if unspent > 0 {
crate::BlockWeight::mutate(|current_weight| {
current_weight.sub(unspent, info.class);
})
}
Ok(())
}
}
impl<T: Trait + Send + Sync> sp_std::fmt::Debug for CheckWeight<T> {
#[cfg(feature = "std")]
fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
write!(f, "CheckWeight")
}
#[cfg(not(feature = "std"))]
fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{BlockWeight, AllExtrinsicsLen};
use crate::mock::{Test, CALL, new_test_ext, System};
use sp_std::marker::PhantomData;
use frame_support::{assert_ok, assert_noop};
use frame_support::weights::{Weight, Pays};
fn normal_weight_limit() -> Weight {
<Test as Trait>::AvailableBlockRatio::get() * <Test as Trait>::MaximumBlockWeight::get()
}
fn normal_length_limit() -> u32 {
<Test as Trait>::AvailableBlockRatio::get() * <Test as Trait>::MaximumBlockLength::get()
}
#[test]
fn mandatory_extrinsic_doesnt_care_about_limits() {
fn check(call: impl FnOnce(&DispatchInfo, usize)) {
new_test_ext().execute_with(|| {
let max = DispatchInfo {
weight: Weight::max_value(),
class: DispatchClass::Mandatory,
..Default::default()
};
let len = 0_usize;
call(&max, len);
});
}
check(|max, len| {
assert_ok!(CheckWeight::<Test>::do_pre_dispatch(max, len));
assert_eq!(System::block_weight().total(), Weight::max_value());
assert!(System::block_weight().total() > <Test as Trait>::MaximumBlockWeight::get());
});
check(|max, len| {
assert_ok!(CheckWeight::<Test>::do_validate(max, len));
});
}
#[test]
fn normal_extrinsic_limited_by_maximum_extrinsic_weight() {
new_test_ext().execute_with(|| {
let max = DispatchInfo {
weight: <Test as Trait>::MaximumExtrinsicWeight::get() + 1,
class: DispatchClass::Normal,
..Default::default()
};
let len = 0_usize;
assert_noop!(
CheckWeight::<Test>::do_validate(&max, len),
InvalidTransaction::ExhaustsResources
);
});
}
#[test]
fn operational_extrinsic_limited_by_operational_space_limit() {
new_test_ext().execute_with(|| {
let operational_limit = CheckWeight::<Test>::get_dispatch_limit_ratio(
DispatchClass::Operational
) * <Test as Trait>::MaximumBlockWeight::get();
let base_weight = <Test as Trait>::ExtrinsicBaseWeight::get();
let block_base = <Test as Trait>::BlockExecutionWeight::get();
let weight = operational_limit - base_weight - block_base;
let okay = DispatchInfo {
weight,
class: DispatchClass::Operational,
..Default::default()
};
let max = DispatchInfo {
weight: weight + 1,
class: DispatchClass::Operational,
..Default::default()
};
let len = 0_usize;
assert_eq!(
CheckWeight::<Test>::do_validate(&okay, len),
Ok(ValidTransaction {
priority: CheckWeight::<Test>::get_priority(&okay),
..Default::default()
})
);
assert_noop!(
CheckWeight::<Test>::do_validate(&max, len),
InvalidTransaction::ExhaustsResources
);
});
}
#[test]
fn register_extra_weight_unchecked_doesnt_care_about_limits() {
new_test_ext().execute_with(|| {
System::register_extra_weight_unchecked(Weight::max_value(), DispatchClass::Normal);
assert_eq!(System::block_weight().total(), Weight::max_value());
assert!(System::block_weight().total() > <Test as Trait>::MaximumBlockWeight::get());
});
}
#[test]
fn full_block_with_normal_and_operational() {
new_test_ext().execute_with(|| {
let max_normal = DispatchInfo { weight: 753, ..Default::default() };
let rest_operational = DispatchInfo { weight: 251, class: DispatchClass::Operational, ..Default::default() };
let len = 0_usize;
assert_ok!(CheckWeight::<Test>::do_pre_dispatch(&max_normal, len));
assert_eq!(System::block_weight().total(), 768);
assert_ok!(CheckWeight::<Test>::do_pre_dispatch(&rest_operational, len));
assert_eq!(<Test as Trait>::MaximumBlockWeight::get(), 1024);
assert_eq!(System::block_weight().total(), <Test as Trait>::MaximumBlockWeight::get());
assert_eq!(CheckWeight::<Test>::check_extrinsic_weight(&rest_operational), Ok(()));
});
}
#[test]
fn dispatch_order_does_not_effect_weight_logic() {
new_test_ext().execute_with(|| {
let max_normal = DispatchInfo { weight: 753, ..Default::default() };
let rest_operational = DispatchInfo { weight: 251, class: DispatchClass::Operational, ..Default::default() };
let len = 0_usize;
assert_ok!(CheckWeight::<Test>::do_pre_dispatch(&rest_operational, len));
assert_eq!(System::block_weight().total(), 266);
assert_ok!(CheckWeight::<Test>::do_pre_dispatch(&max_normal, len));
assert_eq!(<Test as Trait>::MaximumBlockWeight::get(), 1024);
assert_eq!(System::block_weight().total(), <Test as Trait>::MaximumBlockWeight::get());
});
}
#[test]
fn operational_works_on_full_block() {
new_test_ext().execute_with(|| {
System::register_extra_weight_unchecked(Weight::max_value(), DispatchClass::Mandatory);
let dispatch_normal = DispatchInfo { weight: 251, class: DispatchClass::Normal, ..Default::default() };
let dispatch_operational = DispatchInfo { weight: 251, class: DispatchClass::Operational, ..Default::default() };
let len = 0_usize;
assert_noop!(
CheckWeight::<Test>::do_pre_dispatch(&dispatch_normal, len),
InvalidTransaction::ExhaustsResources
);
assert_ok!(CheckWeight::<Test>::do_pre_dispatch(&dispatch_operational, len));
assert_noop!(
CheckWeight::<Test>::do_pre_dispatch(&dispatch_operational, len),
InvalidTransaction::ExhaustsResources
);
assert_eq!(CheckWeight::<Test>::check_extrinsic_weight(&dispatch_operational), Ok(()));
});
}
#[test]
fn signed_ext_check_weight_works_operational_tx() {
new_test_ext().execute_with(|| {
let normal = DispatchInfo { weight: 100, ..Default::default() };
let op = DispatchInfo { weight: 100, class: DispatchClass::Operational, pays_fee: Pays::Yes };
let len = 0_usize;
let normal_limit = normal_weight_limit();
BlockWeight::mutate(|current_weight| {
current_weight.put(normal_limit, DispatchClass::Normal)
});
assert!(CheckWeight::<Test>(PhantomData).pre_dispatch(&1, CALL, &normal, len).is_err());
assert!(CheckWeight::<Test>(PhantomData).pre_dispatch(&1, CALL, &op, len).is_ok());
let len = 100_usize;
AllExtrinsicsLen::put(normal_length_limit());
assert!(CheckWeight::<Test>(PhantomData).pre_dispatch(&1, CALL, &normal, len).is_err());
assert!(CheckWeight::<Test>(PhantomData).pre_dispatch(&1, CALL, &op, len).is_ok());
})
}
#[test]
fn signed_ext_check_weight_works() {
new_test_ext().execute_with(|| {
let normal = DispatchInfo { weight: 100, class: DispatchClass::Normal, pays_fee: Pays::Yes };
let op = DispatchInfo { weight: 100, class: DispatchClass::Operational, pays_fee: Pays::Yes };
let len = 0_usize;
let priority = CheckWeight::<Test>(PhantomData)
.validate(&1, CALL, &normal, len)
.unwrap()
.priority;
assert_eq!(priority, 100);
let priority = CheckWeight::<Test>(PhantomData)
.validate(&1, CALL, &op, len)
.unwrap()
.priority;
assert_eq!(priority, frame_support::weights::priority::LIMIT + 100);
})
}
#[test]
fn signed_ext_check_weight_block_size_works() {
new_test_ext().execute_with(|| {
let normal = DispatchInfo::default();
let normal_limit = normal_weight_limit() as usize;
let reset_check_weight = |tx, s, f| {
AllExtrinsicsLen::put(0);
let r = CheckWeight::<Test>(PhantomData).pre_dispatch(&1, CALL, tx, s);
if f { assert!(r.is_err()) } else { assert!(r.is_ok()) }
};
reset_check_weight(&normal, normal_limit - 1, false);
reset_check_weight(&normal, normal_limit, false);
reset_check_weight(&normal, normal_limit + 1, true);
let op = DispatchInfo { weight: 0, class: DispatchClass::Operational, pays_fee: Pays::Yes };
reset_check_weight(&op, normal_limit, false);
reset_check_weight(&op, normal_limit + 100, false);
reset_check_weight(&op, 1024, false);
reset_check_weight(&op, 1025, true);
})
}
#[test]
fn signed_ext_check_weight_works_normal_tx() {
new_test_ext().execute_with(|| {
let normal_limit = normal_weight_limit();
let small = DispatchInfo { weight: 100, ..Default::default() };
let medium = DispatchInfo {
weight: normal_limit - <Test as Trait>::ExtrinsicBaseWeight::get(),
..Default::default()
};
let big = DispatchInfo {
weight: normal_limit - <Test as Trait>::ExtrinsicBaseWeight::get() + 1,
..Default::default()
};
let len = 0_usize;
let reset_check_weight = |i, f, s| {
BlockWeight::mutate(|current_weight| {
current_weight.put(s, DispatchClass::Normal)
});
let r = CheckWeight::<Test>(PhantomData).pre_dispatch(&1, CALL, i, len);
if f { assert!(r.is_err()) } else { assert!(r.is_ok()) }
};
reset_check_weight(&small, false, 0);
reset_check_weight(&medium, false, 0);
reset_check_weight(&big, true, 1);
})
}
#[test]
fn signed_ext_check_weight_refund_works() {
new_test_ext().execute_with(|| {
let info = DispatchInfo { weight: 512, ..Default::default() };
let post_info = PostDispatchInfo {
actual_weight: Some(128),
pays_fee: Default::default(),
};
let len = 0_usize;
BlockWeight::mutate(|current_weight| {
current_weight.put(256 - <Test as Trait>::ExtrinsicBaseWeight::get(), DispatchClass::Normal)
});
let pre = CheckWeight::<Test>(PhantomData).pre_dispatch(&1, CALL, &info, len).unwrap();
assert_eq!(BlockWeight::get().total(), info.weight + 256);
assert!(
CheckWeight::<Test>::post_dispatch(pre, &info, &post_info, len, &Ok(()))
.is_ok()
);
assert_eq!(
BlockWeight::get().total(),
post_info.actual_weight.unwrap() + 256,
);
})
}
#[test]
fn signed_ext_check_weight_actual_weight_higher_than_max_is_capped() {
new_test_ext().execute_with(|| {
let info = DispatchInfo { weight: 512, ..Default::default() };
let post_info = PostDispatchInfo {
actual_weight: Some(700),
pays_fee: Default::default(),
};
let len = 0_usize;
BlockWeight::mutate(|current_weight| {
current_weight.put(128, DispatchClass::Normal)
});
let pre = CheckWeight::<Test>(PhantomData).pre_dispatch(&1, CALL, &info, len).unwrap();
assert_eq!(
BlockWeight::get().total(),
info.weight + 128 + <Test as Trait>::ExtrinsicBaseWeight::get(),
);
assert!(
CheckWeight::<Test>::post_dispatch(pre, &info, &post_info, len, &Ok(()))
.is_ok()
);
assert_eq!(
BlockWeight::get().total(),
info.weight + 128 + <Test as Trait>::ExtrinsicBaseWeight::get(),
);
})
}
#[test]
fn zero_weight_extrinsic_still_has_base_weight() {
new_test_ext().execute_with(|| {
let free = DispatchInfo { weight: 0, ..Default::default() };
let len = 0_usize;
assert_eq!(System::block_weight().total(), <Test as Trait>::BlockExecutionWeight::get());
let r = CheckWeight::<Test>(PhantomData).pre_dispatch(&1, CALL, &free, len);
assert!(r.is_ok());
assert_eq!(
System::block_weight().total(),
<Test as Trait>::ExtrinsicBaseWeight::get() + <Test as Trait>::BlockExecutionWeight::get()
);
})
}
}