diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_utility.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_utility.rs index a82115b9d093..4f3136cead78 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_utility.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_utility.rs @@ -99,4 +99,12 @@ impl pallet_utility::WeightInfo for WeightInfo { // Standard Error: 1_745 .saturating_add(Weight::from_parts(6_562_902, 0).saturating_mul(c.into())) } + fn if_else() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_utility.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_utility.rs index a7952d6da00e..ef6d9fb4ba94 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_utility.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_utility.rs @@ -98,4 +98,12 @@ impl pallet_utility::WeightInfo for WeightInfo { // Standard Error: 3_765 .saturating_add(Weight::from_parts(6_028_416, 0).saturating_mul(c.into())) } + fn if_else() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/pallet_utility.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/pallet_utility.rs index d96b9e88840f..4e531593f4d5 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/pallet_utility.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/pallet_utility.rs @@ -98,4 +98,12 @@ impl pallet_utility::WeightInfo for WeightInfo { // Standard Error: 1_601 .saturating_add(Weight::from_parts(5_138_293, 0).saturating_mul(c.into())) } + fn if_else() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/pallet_utility.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/pallet_utility.rs index 44cd0cf91e79..15d145a8f935 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/pallet_utility.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/pallet_utility.rs @@ -99,4 +99,12 @@ impl pallet_utility::WeightInfo for WeightInfo { // Standard Error: 1_601 .saturating_add(Weight::from_parts(5_138_293, 0).saturating_mul(c.into())) } + fn if_else() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } } diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/pallet_utility.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/pallet_utility.rs index c60a79d91da3..6887e41099e3 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/pallet_utility.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/pallet_utility.rs @@ -98,4 +98,12 @@ impl pallet_utility::WeightInfo for WeightInfo { // Standard Error: 1_395 .saturating_add(Weight::from_parts(5_000_971, 0).saturating_mul(c.into())) } + fn if_else() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } } diff --git a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/weights/pallet_utility.rs b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/weights/pallet_utility.rs index 84eb97838680..dfff076856f2 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/weights/pallet_utility.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/weights/pallet_utility.rs @@ -99,4 +99,12 @@ impl pallet_utility::WeightInfo for WeightInfo { // Standard Error: 1_621 .saturating_add(Weight::from_parts(3_312_302, 0).saturating_mul(c.into())) } + fn if_else() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } } diff --git a/cumulus/parachains/runtimes/coretime/coretime-westend/src/weights/pallet_utility.rs b/cumulus/parachains/runtimes/coretime/coretime-westend/src/weights/pallet_utility.rs index 0f5340843bd6..ec26f694a5b2 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-westend/src/weights/pallet_utility.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-westend/src/weights/pallet_utility.rs @@ -99,4 +99,12 @@ impl pallet_utility::WeightInfo for WeightInfo { // Standard Error: 740 .saturating_add(Weight::from_parts(2_800_888, 0).saturating_mul(c.into())) } + fn if_else() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } } diff --git a/cumulus/parachains/runtimes/people/people-rococo/src/weights/pallet_utility.rs b/cumulus/parachains/runtimes/people/people-rococo/src/weights/pallet_utility.rs index 134bd1fbbc58..f30f07769526 100644 --- a/cumulus/parachains/runtimes/people/people-rococo/src/weights/pallet_utility.rs +++ b/cumulus/parachains/runtimes/people/people-rococo/src/weights/pallet_utility.rs @@ -96,4 +96,12 @@ impl pallet_utility::WeightInfo for WeightInfo { // Standard Error: 3_915 .saturating_add(Weight::from_parts(4_372_646, 0).saturating_mul(c.into())) } + fn if_else() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } } diff --git a/cumulus/parachains/runtimes/people/people-westend/src/weights/pallet_utility.rs b/cumulus/parachains/runtimes/people/people-westend/src/weights/pallet_utility.rs index 782b0ad6de8d..c7f98f70fdd8 100644 --- a/cumulus/parachains/runtimes/people/people-westend/src/weights/pallet_utility.rs +++ b/cumulus/parachains/runtimes/people/people-westend/src/weights/pallet_utility.rs @@ -96,4 +96,12 @@ impl pallet_utility::WeightInfo for WeightInfo { // Standard Error: 7_605 .saturating_add(Weight::from_parts(4_306_193, 0).saturating_mul(c.into())) } + fn if_else() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } } diff --git a/polkadot/runtime/rococo/src/weights/pallet_utility.rs b/polkadot/runtime/rococo/src/weights/pallet_utility.rs index 6f2a374247f8..5e580de6aad5 100644 --- a/polkadot/runtime/rococo/src/weights/pallet_utility.rs +++ b/polkadot/runtime/rococo/src/weights/pallet_utility.rs @@ -99,4 +99,12 @@ impl pallet_utility::WeightInfo for WeightInfo { // Standard Error: 460 .saturating_add(Weight::from_parts(3_173_577, 0).saturating_mul(c.into())) } + fn if_else() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } } diff --git a/polkadot/runtime/westend/src/weights/pallet_utility.rs b/polkadot/runtime/westend/src/weights/pallet_utility.rs index f8238e9351dc..84fa0589a582 100644 --- a/polkadot/runtime/westend/src/weights/pallet_utility.rs +++ b/polkadot/runtime/westend/src/weights/pallet_utility.rs @@ -99,4 +99,12 @@ impl pallet_utility::WeightInfo for WeightInfo { // Standard Error: 2_817 .saturating_add(Weight::from_parts(5_113_539, 0).saturating_mul(c.into())) } + fn if_else() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } } diff --git a/prdoc/pr_6321.prdoc b/prdoc/pr_6321.prdoc new file mode 100644 index 000000000000..9f70a1b2ee69 --- /dev/null +++ b/prdoc/pr_6321.prdoc @@ -0,0 +1,33 @@ +title: Utility call fallback + +doc: + - audience: Runtime Dev + description: | + This PR adds the `if_else` call to `pallet-utility` + enabling an error fallback when the main call is unsuccessful. + +crates: + - name: asset-hub-rococo-runtime + bump: major + - name: asset-hub-westend-runtime + bump: major + - name: bridge-hub-rococo-runtime + bump: major + - name: bridge-hub-westend-runtime + bump: major + - name: collectives-westend-runtime + bump: major + - name: coretime-rococo-runtime + bump: major + - name: coretime-westend-runtime + bump: major + - name: people-rococo-runtime + bump: major + - name: people-westend-runtime + bump: major + - name: rococo-runtime + bump: major + - name: westend-runtime + bump: major + - name: pallet-utility + bump: major \ No newline at end of file diff --git a/substrate/frame/utility/src/benchmarking.rs b/substrate/frame/utility/src/benchmarking.rs index 88556c05195a..261d52436889 100644 --- a/substrate/frame/utility/src/benchmarking.rs +++ b/substrate/frame/utility/src/benchmarking.rs @@ -92,6 +92,17 @@ mod benchmark { assert_last_event::(Event::BatchCompleted.into()); } + #[benchmark] + fn if_else() { + // Failing main call. + let main_call = Box::new(frame_system::Call::set_code { code: vec![1] }.into()); + let fallback_call = Box::new(frame_system::Call::remark { remark: vec![1] }.into()); + let caller = whitelisted_caller(); + + #[extrinsic_call] + _(RawOrigin::Signed(caller), main_call, fallback_call); + } + impl_benchmark_test_suite! { Pallet, tests::new_test_ext(), diff --git a/substrate/frame/utility/src/lib.rs b/substrate/frame/utility/src/lib.rs index 26c38d1f0459..63a02febb94c 100644 --- a/substrate/frame/utility/src/lib.rs +++ b/substrate/frame/utility/src/lib.rs @@ -61,7 +61,11 @@ extern crate alloc; use alloc::{boxed::Box, vec::Vec}; use codec::{Decode, Encode}; use frame_support::{ - dispatch::{extract_actual_weight, GetDispatchInfo, PostDispatchInfo}, + dispatch::{ + extract_actual_weight, + DispatchClass::{Normal, Operational}, + GetDispatchInfo, PostDispatchInfo, + }, traits::{IsSubType, OriginTrait, UnfilteredDispatchable}, }; use sp_core::TypeId; @@ -120,6 +124,10 @@ pub mod pallet { ItemFailed { error: DispatchError }, /// A call was dispatched. DispatchedAs { result: DispatchResult }, + /// Main call was dispatched. + IfElseMainSuccess, + /// The fallback call was dispatched. + IfElseFallbackCalled { main_error: DispatchError }, } // Align the call size to 1KB. As we are currently compiling the runtime for native/wasm @@ -454,6 +462,98 @@ pub mod pallet { let res = call.dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); res.map(|_| ()).map_err(|e| e.error) } + + /// Dispatch a fallback call in the event the main call fails to execute. + /// May be called from any origin except `None`. + /// + /// This function first attempts to dispatch the `main` call. + /// If the `main` call fails, the `fallback` is attemted. + /// if the fallback is successfully dispatched, the weights of both calls + /// are accumulated and an event containing the main call error is deposited. + /// + /// In the event of a fallback failure the whole call fails + /// with the weights returned. + /// + /// - `main`: The main call to be dispatched. This is the primary action to execute. + /// - `fallback`: The fallback call to be dispatched in case the `main` call fails. + /// + /// ## Dispatch Logic + /// - If the origin is `root`, both the main and fallback calls are executed without + /// applying any origin filters. + /// - If the origin is not `root`, the origin filter is applied to both the `main` and + /// `fallback` calls. + /// + /// ## Use Case + /// - Some use cases might involve submitting a `batch` type call in either main, fallback + /// or both. + #[pallet::call_index(6)] + #[pallet::weight({ + let main = main.get_dispatch_info(); + let fallback = fallback.get_dispatch_info(); + ( + T::WeightInfo::if_else() + .saturating_add(main.call_weight) + .saturating_add(fallback.call_weight), + if main.class == Operational && fallback.class == Operational { Operational } else { Normal }, + ) + })] + pub fn if_else( + origin: OriginFor, + main: Box<::RuntimeCall>, + fallback: Box<::RuntimeCall>, + ) -> DispatchResultWithPostInfo { + // Do not allow the `None` origin. + if ensure_none(origin.clone()).is_ok() { + return Err(BadOrigin.into()); + } + + let is_root = ensure_root(origin.clone()).is_ok(); + + // Track the weights + let mut weight = T::WeightInfo::if_else(); + + let main_info = main.get_dispatch_info(); + + // Execute the main call first + let main_result = if is_root { + main.dispatch_bypass_filter(origin.clone()) + } else { + main.dispatch(origin.clone()) + }; + + // Add weight of the main call + weight = weight.saturating_add(extract_actual_weight(&main_result, &main_info)); + + let Err(main_error) = main_result else { + // If the main result is Ok, we skip the fallback logic entirely + Self::deposit_event(Event::IfElseMainSuccess); + return Ok(Some(weight).into()); + }; + + // If the main call failed, execute the fallback call + let fallback_info = fallback.get_dispatch_info(); + + let fallback_result = if is_root { + fallback.dispatch_bypass_filter(origin.clone()) + } else { + fallback.dispatch(origin) + }; + + // Add weight of the fallback call + weight = weight.saturating_add(extract_actual_weight(&fallback_result, &fallback_info)); + + let Err(fallback_error) = fallback_result else { + // Fallback succeeded. + Self::deposit_event(Event::IfElseFallbackCalled { main_error: main_error.error }); + return Ok(Some(weight).into()); + }; + + // Both calls have failed, return fallback error + Err(sp_runtime::DispatchErrorWithPostInfo { + error: fallback_error.error, + post_info: Some(weight).into(), + }) + } } impl Pallet { diff --git a/substrate/frame/utility/src/tests.rs b/substrate/frame/utility/src/tests.rs index 274a90d77cf0..64ad12e9c594 100644 --- a/substrate/frame/utility/src/tests.rs +++ b/substrate/frame/utility/src/tests.rs @@ -914,3 +914,117 @@ fn with_weight_works() { ); }) } + +#[test] +fn if_else_with_root_works() { + new_test_ext().execute_with(|| { + let k = b"a".to_vec(); + let call = RuntimeCall::System(frame_system::Call::set_storage { + items: vec![(k.clone(), k.clone())], + }); + assert!(!TestBaseCallFilter::contains(&call)); + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); + assert_ok!(Utility::if_else( + RuntimeOrigin::root(), + RuntimeCall::Balances(BalancesCall::force_transfer { source: 1, dest: 2, value: 11 }) + .into(), + call.into(), + )); + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); + assert_eq!(storage::unhashed::get_raw(&k), Some(k)); + System::assert_last_event( + utility::Event::IfElseFallbackCalled { + main_error: TokenError::FundsUnavailable.into(), + } + .into(), + ); + }); +} + +#[test] +fn if_else_with_signed_works() { + new_test_ext().execute_with(|| { + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); + assert_ok!(Utility::if_else( + RuntimeOrigin::signed(1), + call_transfer(2, 11).into(), + call_transfer(2, 5).into() + )); + assert_eq!(Balances::free_balance(1), 5); + assert_eq!(Balances::free_balance(2), 15); + + System::assert_last_event( + utility::Event::IfElseFallbackCalled { + main_error: TokenError::FundsUnavailable.into(), + } + .into(), + ); + }); +} + +#[test] +fn if_else_successful_main_call() { + new_test_ext().execute_with(|| { + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); + assert_ok!(Utility::if_else( + RuntimeOrigin::signed(1), + call_transfer(2, 9).into(), + call_transfer(2, 1).into() + )); + assert_eq!(Balances::free_balance(1), 1); + assert_eq!(Balances::free_balance(2), 19); + + System::assert_last_event(utility::Event::IfElseMainSuccess.into()); + }) +} + +#[test] +fn if_else_failing_fallback_call() { + new_test_ext().execute_with(|| { + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); + assert_err_ignore_postinfo!( + Utility::if_else( + RuntimeOrigin::signed(1), + call_transfer(2, 11).into(), + call_transfer(2, 11).into() + ), + TokenError::FundsUnavailable + ); + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); + }) +} + +#[test] +fn if_else_with_nested_if_else_works() { + new_test_ext().execute_with(|| { + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); + + let main_call = call_transfer(2, 11).into(); + let fallback_call = call_transfer(2, 5).into(); + + let nested_if_else_call = + RuntimeCall::Utility(UtilityCall::if_else { main: main_call, fallback: fallback_call }) + .into(); + + // Nested `if_else` call. + assert_ok!(Utility::if_else( + RuntimeOrigin::signed(1), + nested_if_else_call, + call_transfer(2, 7).into() + )); + + // inner if_else fallback is executed. + assert_eq!(Balances::free_balance(1), 5); + assert_eq!(Balances::free_balance(2), 15); + + // Ensure the correct event was triggered for the main call(nested if_else). + System::assert_last_event(utility::Event::IfElseMainSuccess.into()); + }); +} diff --git a/substrate/frame/utility/src/weights.rs b/substrate/frame/utility/src/weights.rs index 8b31eb2ced85..30922bbb22d5 100644 --- a/substrate/frame/utility/src/weights.rs +++ b/substrate/frame/utility/src/weights.rs @@ -56,6 +56,7 @@ pub trait WeightInfo { fn batch_all(c: u32, ) -> Weight; fn dispatch_as() -> Weight; fn force_batch(c: u32, ) -> Weight; + fn if_else() -> Weight; } /// Weights for `pallet_utility` using the Substrate node and recommended hardware. @@ -125,6 +126,14 @@ impl WeightInfo for SubstrateWeight { .saturating_add(Weight::from_parts(4_570_923, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(2_u64)) } + fn if_else() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } } // For backwards compatibility and tests. @@ -193,4 +202,12 @@ impl WeightInfo for () { .saturating_add(Weight::from_parts(4_570_923, 0).saturating_mul(c.into())) .saturating_add(RocksDbWeight::get().reads(2_u64)) } + fn if_else() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } }