From 668ecade60abe5a644868aefd3d231c9839fe652 Mon Sep 17 00:00:00 2001 From: Xiliang Chen Date: Wed, 28 Oct 2020 08:08:51 +1300 Subject: [PATCH] Implement batch_all and update Utility pallet for weight refunds (#7188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: Shawn Tabrizi Co-authored-by: Bastian Köcher --- .../runtime/src/weights/pallet_utility.rs | 16 +- frame/utility/src/benchmarking.rs | 14 + frame/utility/src/default_weights.rs | 16 +- frame/utility/src/lib.rs | 99 +++++- frame/utility/src/tests.rs | 293 +++++++++++++++++- 5 files changed, 414 insertions(+), 24 deletions(-) diff --git a/bin/node/runtime/src/weights/pallet_utility.rs b/bin/node/runtime/src/weights/pallet_utility.rs index af48267d9b5de..5b2aace87cb6a 100644 --- a/bin/node/runtime/src/weights/pallet_utility.rs +++ b/bin/node/runtime/src/weights/pallet_utility.rs @@ -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(PhantomData); impl pallet_utility::WeightInfo for WeightInfo { 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)) } } diff --git a/frame/utility/src/benchmarking.rs b/frame/utility/src/benchmarking.rs index 1c1b3f5815005..413ed66ac8498 100644 --- a/frame/utility/src/benchmarking.rs +++ b/frame/utility/src/benchmarking.rs @@ -56,6 +56,19 @@ benchmarks! { let caller_key = frame_system::Account::::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<::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::(Event::BatchCompleted.into()) + } } #[cfg(test)] @@ -69,6 +82,7 @@ mod tests { new_test_ext().execute_with(|| { assert_ok!(test_benchmark_batch::()); assert_ok!(test_benchmark_as_derivative::()); + assert_ok!(test_benchmark_batch_all::()); }); } } diff --git a/frame/utility/src/default_weights.rs b/frame/utility/src/default_weights.rs index d63f010612ec1..8dc9b6fb8c4b2 100644 --- a/frame/utility/src/default_weights.rs +++ b/frame/utility/src/default_weights.rs @@ -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)) } } diff --git a/frame/utility/src/lib.rs b/frame/utility/src/lib.rs index d0bb99d917455..3aa310c8acb74 100644 --- a/frame/utility/src/lib.rs +++ b/frame/utility/src/lib.rs @@ -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`). /// /// # - /// - 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. /// # /// /// 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<::Call>) { + fn batch(origin, calls: Vec<::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<::Call>) -> DispatchResult { + fn as_derivative(origin, index: u16, call: Box<::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`). + /// + /// # + /// - Complexity: O(C) where C is the number of calls to be batched. + /// # + #[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<::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()) } } } diff --git a/frame/utility/src/tests.rs b/frame/utility/src/tests.rs index 8e693b234a939..a3c33bdf2081f 100644 --- a/frame/utility/src/tests.rs +++ b/frame/utility/src/tests.rs @@ -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 for enum Call where origin: ::Origin { + #[weight = *weight] + fn noop(_origin, weight: Weight) { } + + #[weight = *start_weight] + fn foobar( + origin, + err: bool, + start_weight: Weight, + end_weight: Option, + ) -> 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 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; type Balances = pallet_balances::Module; +type Example = example::Module; type Utility = Module; +type ExampleCall = example::Call; +type UtilityCall = crate::Call; + 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 + ::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(::WeightInfo::batch_all(2) + info.weight * 2), + pays_fee: Pays::Yes + }, + error: pallet_balances::Error::::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 + ::WeightInfo::batch_all(2) + end_weight * 2, + ); + }); +}