Implement batch_all and update Utility pallet for weight refunds (#7188)

* implement batch_all

* bump version

* updates

* Better weight story for utility

* small fixes

* weights

* assert_noop_ignore_postinfo doesnt make sense

* Apply suggestions from code review

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>
Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>
This commit is contained in:
Xiliang Chen
2020-10-28 08:08:51 +13:00
committed by GitHub
parent f373ecbcf0
commit f14809779d
5 changed files with 414 additions and 24 deletions
@@ -1,6 +1,6 @@
// This file is part of Substrate.
// Copyright (C) 2017-2020 Parity Technologies (UK) Ltd.
// Copyright (C) 2020 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,7 +15,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 2.0.0-rc5
//! Weights for pallet_utility
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 2.0.0
//! DATE: 2020-10-02, STEPS: [50], REPEAT: 20, LOW RANGE: [], HIGH RANGE: []
#![allow(unused_parens)]
#![allow(unused_imports)]
@@ -26,10 +28,14 @@ use sp_std::marker::PhantomData;
pub struct WeightInfo<T>(PhantomData<T>);
impl<T: frame_system::Trait> pallet_utility::WeightInfo for WeightInfo<T> {
fn batch(c: u32, ) -> Weight {
(16461000 as Weight)
.saturating_add((1982000 as Weight).saturating_mul(c as Weight))
(20_803_000 as Weight)
.saturating_add((1_984_000 as Weight).saturating_mul(c as Weight))
}
fn as_derivative() -> Weight {
(4086000 as Weight)
(5_853_000 as Weight)
}
fn batch_all(c: u32, ) -> Weight {
(21_104_000 as Weight)
.saturating_add((1_509_000 as Weight).saturating_mul(c as Weight))
}
}
@@ -56,6 +56,19 @@ benchmarks! {
let caller_key = frame_system::Account::<T>::hashed_key_for(&caller);
frame_benchmarking::benchmarking::add_to_whitelist(caller_key.into());
}: _(RawOrigin::Signed(caller), SEED as u16, call)
batch_all {
let c in 0 .. 1000;
let mut calls: Vec<<T as Trait>::Call> = Vec::new();
for i in 0 .. c {
let call = frame_system::Call::remark(vec![]).into();
calls.push(call);
}
let caller = whitelisted_caller();
}: _(RawOrigin::Signed(caller), calls)
verify {
assert_last_event::<T>(Event::BatchCompleted.into())
}
}
#[cfg(test)]
@@ -69,6 +82,7 @@ mod tests {
new_test_ext().execute_with(|| {
assert_ok!(test_benchmark_batch::<Test>());
assert_ok!(test_benchmark_as_derivative::<Test>());
assert_ok!(test_benchmark_batch_all::<Test>());
});
}
}
+11 -5
View File
@@ -1,6 +1,6 @@
// This file is part of Substrate.
// Copyright (C) 2019-2020 Parity Technologies (UK) Ltd.
// Copyright (C) 2020 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,7 +15,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 2.0.0-rc5
//! Weights for pallet_utility
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 2.0.0
//! DATE: 2020-10-02, STEPS: [50], REPEAT: 20, LOW RANGE: [], HIGH RANGE: []
#![allow(unused_parens)]
#![allow(unused_imports)]
@@ -24,10 +26,14 @@ use frame_support::weights::{Weight, constants::RocksDbWeight as DbWeight};
impl crate::WeightInfo for () {
fn batch(c: u32, ) -> Weight {
(16461000 as Weight)
.saturating_add((1982000 as Weight).saturating_mul(c as Weight))
(20_803_000 as Weight)
.saturating_add((1_984_000 as Weight).saturating_mul(c as Weight))
}
fn as_derivative() -> Weight {
(4086000 as Weight)
(5_853_000 as Weight)
}
fn batch_all(c: u32, ) -> Weight {
(21_104_000 as Weight)
.saturating_add((1_509_000 as Weight).saturating_mul(c as Weight))
}
}
+89 -10
View File
@@ -59,13 +59,14 @@ use sp_std::prelude::*;
use codec::{Encode, Decode};
use sp_core::TypeId;
use sp_io::hashing::blake2_256;
use frame_support::{decl_module, decl_event, decl_storage, Parameter};
use frame_support::{decl_module, decl_event, decl_storage, Parameter, transactional};
use frame_support::{
traits::{OriginTrait, UnfilteredDispatchable, Get},
weights::{Weight, GetDispatchInfo, DispatchClass}, dispatch::PostDispatchInfo,
weights::{Weight, GetDispatchInfo, DispatchClass, extract_actual_weight},
dispatch::{PostDispatchInfo, DispatchResultWithPostInfo},
};
use frame_system::{ensure_signed, ensure_root};
use sp_runtime::{DispatchError, DispatchResult, traits::Dispatchable};
use sp_runtime::{DispatchError, traits::Dispatchable};
mod tests;
mod benchmarking;
@@ -74,6 +75,7 @@ mod default_weights;
pub trait WeightInfo {
fn batch(c: u32, ) -> Weight;
fn as_derivative() -> Weight;
fn batch_all(c: u32, ) -> Weight;
}
/// Configuration trait.
@@ -128,9 +130,7 @@ decl_module! {
/// bypassing `frame_system::Trait::BaseCallFilter`).
///
/// # <weight>
/// - Base weight: 14.39 + .987 * c µs
/// - Plus the sum of the weights of the `calls`.
/// - Plus one additional event. (repeat read/write)
/// - Complexity: O(C) where C is the number of calls to be batched.
/// # </weight>
///
/// This will return `Ok` in all circumstances. To determine the success of the batch, an
@@ -154,20 +154,32 @@ decl_module! {
}
},
)]
fn batch(origin, calls: Vec<<T as Trait>::Call>) {
fn batch(origin, calls: Vec<<T as Trait>::Call>) -> DispatchResultWithPostInfo {
let is_root = ensure_root(origin.clone()).is_ok();
let calls_len = calls.len();
// Track the actual weight of each of the batch calls.
let mut weight: Weight = 0;
for (index, call) in calls.into_iter().enumerate() {
let info = call.get_dispatch_info();
// If origin is root, don't apply any dispatch filters; root can call anything.
let result = if is_root {
call.dispatch_bypass_filter(origin.clone())
} else {
call.dispatch(origin.clone())
};
// Add the weight of this call.
weight = weight.saturating_add(extract_actual_weight(&result, &info));
if let Err(e) = result {
Self::deposit_event(Event::BatchInterrupted(index as u32, e.error));
return Ok(());
// Take the weight of this function itself into account.
let base_weight = T::WeightInfo::batch(index.saturating_add(1) as u32);
// Return the actual used weight + base_weight of this call.
return Ok(Some(base_weight + weight).into());
}
}
Self::deposit_event(Event::BatchCompleted);
let base_weight = T::WeightInfo::batch(calls_len as u32);
Ok(Some(base_weight + weight).into())
}
/// Send a call through an indexed pseudonym of the sender.
@@ -190,12 +202,79 @@ decl_module! {
.saturating_add(T::DbWeight::get().reads_writes(1, 1)),
call.get_dispatch_info().class,
)]
fn as_derivative(origin, index: u16, call: Box<<T as Trait>::Call>) -> DispatchResult {
fn as_derivative(origin, index: u16, call: Box<<T as Trait>::Call>) -> DispatchResultWithPostInfo {
let mut origin = origin;
let who = ensure_signed(origin.clone())?;
let pseudonym = Self::derivative_account_id(who, index);
origin.set_caller_from(frame_system::RawOrigin::Signed(pseudonym));
call.dispatch(origin).map(|_| ()).map_err(|e| e.error)
let info = call.get_dispatch_info();
let result = call.dispatch(origin);
// Always take into account the base weight of this call.
let mut weight = T::WeightInfo::as_derivative().saturating_add(T::DbWeight::get().reads_writes(1, 1));
// Add the real weight of the dispatch.
weight = weight.saturating_add(extract_actual_weight(&result, &info));
result.map_err(|mut err| {
err.post_info = Some(weight).into();
err
}).map(|_| Some(weight).into())
}
/// Send a batch of dispatch calls and atomically execute them.
/// The whole transaction will rollback and fail if any of the calls failed.
///
/// May be called from any origin.
///
/// - `calls`: The calls to be dispatched from the same origin.
///
/// If origin is root then call are dispatch without checking origin filter. (This includes
/// bypassing `frame_system::Trait::BaseCallFilter`).
///
/// # <weight>
/// - Complexity: O(C) where C is the number of calls to be batched.
/// # </weight>
#[weight = (
calls.iter()
.map(|call| call.get_dispatch_info().weight)
.fold(0, |total: Weight, weight: Weight| total.saturating_add(weight))
.saturating_add(T::WeightInfo::batch_all(calls.len() as u32)),
{
let all_operational = calls.iter()
.map(|call| call.get_dispatch_info().class)
.all(|class| class == DispatchClass::Operational);
if all_operational {
DispatchClass::Operational
} else {
DispatchClass::Normal
}
},
)]
#[transactional]
fn batch_all(origin, calls: Vec<<T as Trait>::Call>) -> DispatchResultWithPostInfo {
let is_root = ensure_root(origin.clone()).is_ok();
let calls_len = calls.len();
// Track the actual weight of each of the batch calls.
let mut weight: Weight = 0;
for (index, call) in calls.into_iter().enumerate() {
let info = call.get_dispatch_info();
// If origin is root, bypass any dispatch filter; root can call anything.
let result = if is_root {
call.dispatch_bypass_filter(origin.clone())
} else {
call.dispatch(origin.clone())
};
// Add the weight of this call.
weight = weight.saturating_add(extract_actual_weight(&result, &info));
result.map_err(|mut err| {
// Take the weight of this function itself into account.
let base_weight = T::WeightInfo::batch_all(index.saturating_add(1) as u32);
// Return the actual used weight + base_weight of this call.
err.post_info = Some(base_weight + weight).into();
err
})?;
}
Self::deposit_event(Event::BatchCompleted);
let base_weight = T::WeightInfo::batch_all(calls_len as u32);
Ok(Some(base_weight + weight).into())
}
}
}
+289 -4
View File
@@ -22,13 +22,51 @@
use super::*;
use frame_support::{
assert_ok, assert_noop, impl_outer_origin, parameter_types, impl_outer_dispatch,
weights::Weight, impl_outer_event, dispatch::DispatchError, traits::Filter, storage,
assert_ok, assert_noop, impl_outer_origin, parameter_types, impl_outer_dispatch, impl_outer_event,
assert_err_ignore_postinfo,
weights::{Weight, Pays},
dispatch::{DispatchError, DispatchErrorWithPostInfo, Dispatchable},
traits::Filter,
storage,
};
use sp_core::H256;
use sp_runtime::{Perbill, traits::{BlakeTwo256, IdentityLookup}, testing::Header};
use crate as utility;
// example module to test behaviors.
pub mod example {
use super::*;
use frame_support::dispatch::WithPostDispatchInfo;
pub trait Trait: frame_system::Trait { }
decl_module! {
pub struct Module<T: Trait> for enum Call where origin: <T as frame_system::Trait>::Origin {
#[weight = *weight]
fn noop(_origin, weight: Weight) { }
#[weight = *start_weight]
fn foobar(
origin,
err: bool,
start_weight: Weight,
end_weight: Option<Weight>,
) -> DispatchResultWithPostInfo {
let _ = ensure_signed(origin)?;
if err {
let error: DispatchError = "The cake is a lie.".into();
if let Some(weight) = end_weight {
Err(error.with_weight(weight))
} else {
Err(error)?
}
} else {
Ok(end_weight.into())
}
}
}
}
}
impl_outer_origin! {
pub enum Origin for Test where system = frame_system {}
}
@@ -44,6 +82,7 @@ impl_outer_dispatch! {
frame_system::System,
pallet_balances::Balances,
utility::Utility,
example::Example,
}
}
@@ -102,13 +141,19 @@ parameter_types! {
pub const MultisigDepositFactor: u64 = 1;
pub const MaxSignatories: u16 = 3;
}
impl example::Trait for Test {}
pub struct TestBaseCallFilter;
impl Filter<Call> for TestBaseCallFilter {
fn filter(c: &Call) -> bool {
match *c {
Call::Balances(_) => true,
Call::Utility(_) => true,
// For benchmarking, this acts as a noop call
Call::System(frame_system::Call::remark(..)) => true,
// For tests
Call::Example(_) => true,
_ => false,
}
}
@@ -120,8 +165,12 @@ impl Trait for Test {
}
type System = frame_system::Module<Test>;
type Balances = pallet_balances::Module<Test>;
type Example = example::Module<Test>;
type Utility = Module<Test>;
type ExampleCall = example::Call<Test>;
type UtilityCall = crate::Call<Test>;
use frame_system::Call as SystemCall;
use pallet_balances::Call as BalancesCall;
use pallet_balances::Error as BalancesError;
@@ -149,7 +198,7 @@ fn as_derivative_works() {
new_test_ext().execute_with(|| {
let sub_1_0 = Utility::derivative_account_id(1, 0);
assert_ok!(Balances::transfer(Origin::signed(1), sub_1_0, 5));
assert_noop!(Utility::as_derivative(
assert_err_ignore_postinfo!(Utility::as_derivative(
Origin::signed(1),
1,
Box::new(Call::Balances(BalancesCall::transfer(6, 3))),
@@ -164,10 +213,70 @@ fn as_derivative_works() {
});
}
#[test]
fn as_derivative_handles_weight_refund() {
new_test_ext().execute_with(|| {
let start_weight = 100;
let end_weight = 75;
let diff = start_weight - end_weight;
// Full weight when ok
let inner_call = Call::Example(ExampleCall::foobar(false, start_weight, None));
let call = Call::Utility(UtilityCall::as_derivative(0, Box::new(inner_call)));
let info = call.get_dispatch_info();
let result = call.dispatch(Origin::signed(1));
assert_ok!(result);
assert_eq!(extract_actual_weight(&result, &info), info.weight);
// Refund weight when ok
let inner_call = Call::Example(ExampleCall::foobar(false, start_weight, Some(end_weight)));
let call = Call::Utility(UtilityCall::as_derivative(0, Box::new(inner_call)));
let info = call.get_dispatch_info();
let result = call.dispatch(Origin::signed(1));
assert_ok!(result);
// Diff is refunded
assert_eq!(extract_actual_weight(&result, &info), info.weight - diff);
// Full weight when err
let inner_call = Call::Example(ExampleCall::foobar(true, start_weight, None));
let call = Call::Utility(UtilityCall::as_derivative(0, Box::new(inner_call)));
let info = call.get_dispatch_info();
let result = call.dispatch(Origin::signed(1));
assert_noop!(
result,
DispatchErrorWithPostInfo {
post_info: PostDispatchInfo {
// No weight is refunded
actual_weight: Some(info.weight),
pays_fee: Pays::Yes,
},
error: DispatchError::Other("The cake is a lie."),
}
);
// Refund weight when err
let inner_call = Call::Example(ExampleCall::foobar(true, start_weight, Some(end_weight)));
let call = Call::Utility(UtilityCall::as_derivative(0, Box::new(inner_call)));
let info = call.get_dispatch_info();
let result = call.dispatch(Origin::signed(1));
assert_noop!(
result,
DispatchErrorWithPostInfo {
post_info: PostDispatchInfo {
// Diff is refunded
actual_weight: Some(info.weight - diff),
pays_fee: Pays::Yes,
},
error: DispatchError::Other("The cake is a lie."),
}
);
});
}
#[test]
fn as_derivative_filters() {
new_test_ext().execute_with(|| {
assert_noop!(Utility::as_derivative(
assert_err_ignore_postinfo!(Utility::as_derivative(
Origin::signed(1),
1,
Box::new(Call::System(frame_system::Call::suicide())),
@@ -255,3 +364,179 @@ fn batch_weight_calculation_doesnt_overflow() {
assert_eq!(batch_call.get_dispatch_info().weight, Weight::max_value());
});
}
#[test]
fn batch_handles_weight_refund() {
new_test_ext().execute_with(|| {
let start_weight = 100;
let end_weight = 75;
let diff = start_weight - end_weight;
let batch_len: Weight = 4;
// Full weight when ok
let inner_call = Call::Example(ExampleCall::foobar(false, start_weight, None));
let batch_calls = vec![inner_call; batch_len as usize];
let call = Call::Utility(UtilityCall::batch(batch_calls));
let info = call.get_dispatch_info();
let result = call.dispatch(Origin::signed(1));
assert_ok!(result);
assert_eq!(extract_actual_weight(&result, &info), info.weight);
// Refund weight when ok
let inner_call = Call::Example(ExampleCall::foobar(false, start_weight, Some(end_weight)));
let batch_calls = vec![inner_call; batch_len as usize];
let call = Call::Utility(UtilityCall::batch(batch_calls));
let info = call.get_dispatch_info();
let result = call.dispatch(Origin::signed(1));
assert_ok!(result);
// Diff is refunded
assert_eq!(extract_actual_weight(&result, &info), info.weight - diff * batch_len);
// Full weight when err
let good_call = Call::Example(ExampleCall::foobar(false, start_weight, None));
let bad_call = Call::Example(ExampleCall::foobar(true, start_weight, None));
let batch_calls = vec![good_call, bad_call];
let call = Call::Utility(UtilityCall::batch(batch_calls));
let info = call.get_dispatch_info();
let result = call.dispatch(Origin::signed(1));
assert_ok!(result);
expect_event(Event::BatchInterrupted(1, DispatchError::Other("")));
// No weight is refunded
assert_eq!(extract_actual_weight(&result, &info), info.weight);
// Refund weight when err
let good_call = Call::Example(ExampleCall::foobar(false, start_weight, Some(end_weight)));
let bad_call = Call::Example(ExampleCall::foobar(true, start_weight, Some(end_weight)));
let batch_calls = vec![good_call, bad_call];
let batch_len = batch_calls.len() as Weight;
let call = Call::Utility(UtilityCall::batch(batch_calls));
let info = call.get_dispatch_info();
let result = call.dispatch(Origin::signed(1));
assert_ok!(result);
expect_event(Event::BatchInterrupted(1, DispatchError::Other("")));
assert_eq!(extract_actual_weight(&result, &info), info.weight - diff * batch_len);
// Partial batch completion
let good_call = Call::Example(ExampleCall::foobar(false, start_weight, Some(end_weight)));
let bad_call = Call::Example(ExampleCall::foobar(true, start_weight, Some(end_weight)));
let batch_calls = vec![good_call, bad_call.clone(), bad_call];
let call = Call::Utility(UtilityCall::batch(batch_calls));
let info = call.get_dispatch_info();
let result = call.dispatch(Origin::signed(1));
assert_ok!(result);
expect_event(Event::BatchInterrupted(1, DispatchError::Other("")));
assert_eq!(
extract_actual_weight(&result, &info),
// Real weight is 2 calls at end_weight
<Test as Trait>::WeightInfo::batch(2) + end_weight * 2,
);
});
}
#[test]
fn batch_all_works() {
new_test_ext().execute_with(|| {
assert_eq!(Balances::free_balance(1), 10);
assert_eq!(Balances::free_balance(2), 10);
assert_ok!(
Utility::batch_all(Origin::signed(1), vec![
Call::Balances(BalancesCall::transfer(2, 5)),
Call::Balances(BalancesCall::transfer(2, 5))
]),
);
assert_eq!(Balances::free_balance(1), 0);
assert_eq!(Balances::free_balance(2), 20);
});
}
#[test]
fn batch_all_revert() {
new_test_ext().execute_with(|| {
let call = Call::Balances(BalancesCall::transfer(2, 5));
let info = call.get_dispatch_info();
assert_eq!(Balances::free_balance(1), 10);
assert_eq!(Balances::free_balance(2), 10);
assert_noop!(
Utility::batch_all(Origin::signed(1), vec![
Call::Balances(BalancesCall::transfer(2, 5)),
Call::Balances(BalancesCall::transfer(2, 10)),
Call::Balances(BalancesCall::transfer(2, 5)),
]),
DispatchErrorWithPostInfo {
post_info: PostDispatchInfo {
actual_weight: Some(<Test as Trait>::WeightInfo::batch_all(2) + info.weight * 2),
pays_fee: Pays::Yes
},
error: pallet_balances::Error::<Test, _>::InsufficientBalance.into()
}
);
assert_eq!(Balances::free_balance(1), 10);
assert_eq!(Balances::free_balance(2), 10);
});
}
#[test]
fn batch_all_handles_weight_refund() {
new_test_ext().execute_with(|| {
let start_weight = 100;
let end_weight = 75;
let diff = start_weight - end_weight;
let batch_len: Weight = 4;
// Full weight when ok
let inner_call = Call::Example(ExampleCall::foobar(false, start_weight, None));
let batch_calls = vec![inner_call; batch_len as usize];
let call = Call::Utility(UtilityCall::batch_all(batch_calls));
let info = call.get_dispatch_info();
let result = call.dispatch(Origin::signed(1));
assert_ok!(result);
assert_eq!(extract_actual_weight(&result, &info), info.weight);
// Refund weight when ok
let inner_call = Call::Example(ExampleCall::foobar(false, start_weight, Some(end_weight)));
let batch_calls = vec![inner_call; batch_len as usize];
let call = Call::Utility(UtilityCall::batch_all(batch_calls));
let info = call.get_dispatch_info();
let result = call.dispatch(Origin::signed(1));
assert_ok!(result);
// Diff is refunded
assert_eq!(extract_actual_weight(&result, &info), info.weight - diff * batch_len);
// Full weight when err
let good_call = Call::Example(ExampleCall::foobar(false, start_weight, None));
let bad_call = Call::Example(ExampleCall::foobar(true, start_weight, None));
let batch_calls = vec![good_call, bad_call];
let call = Call::Utility(UtilityCall::batch_all(batch_calls));
let info = call.get_dispatch_info();
let result = call.dispatch(Origin::signed(1));
assert_err_ignore_postinfo!(result, "The cake is a lie.");
// No weight is refunded
assert_eq!(extract_actual_weight(&result, &info), info.weight);
// Refund weight when err
let good_call = Call::Example(ExampleCall::foobar(false, start_weight, Some(end_weight)));
let bad_call = Call::Example(ExampleCall::foobar(true, start_weight, Some(end_weight)));
let batch_calls = vec![good_call, bad_call];
let batch_len = batch_calls.len() as Weight;
let call = Call::Utility(UtilityCall::batch_all(batch_calls));
let info = call.get_dispatch_info();
let result = call.dispatch(Origin::signed(1));
assert_err_ignore_postinfo!(result, "The cake is a lie.");
assert_eq!(extract_actual_weight(&result, &info), info.weight - diff * batch_len);
// Partial batch completion
let good_call = Call::Example(ExampleCall::foobar(false, start_weight, Some(end_weight)));
let bad_call = Call::Example(ExampleCall::foobar(true, start_weight, Some(end_weight)));
let batch_calls = vec![good_call, bad_call.clone(), bad_call];
let call = Call::Utility(UtilityCall::batch_all(batch_calls));
let info = call.get_dispatch_info();
let result = call.dispatch(Origin::signed(1));
assert_err_ignore_postinfo!(result, "The cake is a lie.");
assert_eq!(
extract_actual_weight(&result, &info),
// Real weight is 2 calls at end_weight
<Test as Trait>::WeightInfo::batch_all(2) + end_weight * 2,
);
});
}