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

pallet-utility: if_else #6321

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
24 changes: 12 additions & 12 deletions prdoc/pr_6321.prdoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,26 @@ doc:

crates:
- name: asset-hub-rococo-runtime
bump: patch
bump: major
- name: asset-hub-westend-runtime
bump: patch
bump: major
- name: bridge-hub-rococo-runtime
bump: patch
bump: major
- name: bridge-hub-westend-runtime
bump: patch
bump: major
- name: collectives-westend-runtime
bump: patch
bump: major
- name: coretime-rococo-runtime
bump: patch
bump: major
- name: coretime-westend-runtime
bump: patch
bump: major
- name: people-rococo-runtime
bump: patch
bump: major
- name: people-westend-runtime
bump: patch
bump: major
- name: rococo-runtime
bump: patch
bump: major
- name: westend-runtime
bump: patch
bump: major
- name: pallet-utility
bump: minor
bump: major
2 changes: 1 addition & 1 deletion substrate/frame/utility/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ mod benchmark {
#[extrinsic_call]
_(RawOrigin::Signed(caller), main_call, fallback_call);
Copy link
Contributor

Choose a reason for hiding this comment

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

The err branch should be slightly more costly, but difference should be negligible so it is ok.

Copy link
Author

Choose a reason for hiding this comment

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

So adjusting with a failed main call will give the proper weights as it enters the else branch? will update this.

Thanks for the review


assert_last_event::<T>(Event::IfElseCompleted { call: Which::Main }.into());
assert_last_event::<T>(Event::IfElseMainSuccess.into());
}

impl_benchmark_test_suite! {
Expand Down
53 changes: 39 additions & 14 deletions substrate/frame/utility/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,6 @@ pub use weights::WeightInfo;

pub use pallet::*;

#[derive(PartialEq, Eq, Clone, Copy, Encode, Decode, Debug, TypeInfo)]
pub enum Which {
Main,
Fallback,
}

#[frame_support::pallet]
pub mod pallet {
use super::*;
Expand Down Expand Up @@ -127,8 +121,12 @@ pub mod pallet {
ItemFailed { error: DispatchError },
/// A call was dispatched.
DispatchedAs { result: DispatchResult },
/// if_else completed.
IfElseCompleted { call: Which },
/// Main call was dispatched.
IfElseMainSuccess,
/// The fallback call was dispatched.
IfElseFallbackSuccess { main_error: DispatchError },
/// Both calls failed.
IfElseBothFailure { main_error: DispatchError, fallback_error: DispatchError },
}

// Align the call size to 1KB. As we are currently compiling the runtime for native/wasm
Expand Down Expand Up @@ -466,6 +464,32 @@ pub mod pallet {
res.map(|_| ()).map_err(|e| e.error)
}

/// Dispatch a fallback call in the event the main call fails to execute.
///
/// This function first attempts to dispatch the `main` call. If the `main` call fails, the `fallback` call
/// is dispatched instead. Both calls are executed with the same origin, and the weight of both calls
/// is accumulated and returned. The success or failure of the main and fallback calls is tracked and
/// appropriate events are deposited.
///
/// May be called from any origin except `None`.
///
/// - `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.
///
/// ## Complexity
/// - O(1) for the origin check and dispatching the main call.
/// - O(1) for the origin check and dispatching the fallback call (if needed).
/// - Overall complexity is O(1) since we only dispatch at most two calls.
///
/// ## Weight
/// The weight of this call is calculated as the sum of:
/// - The weight of the `main` call (if it is executed),
/// - The weight of the `fallback` call (if the `main` call fails and the `fallback` is executed),
/// - A base weight (`WeightInfo::if_else()`), which accounts for the logic involved in dispatching and handling both calls.
#[pallet::call_index(6)]
rainbow-promise marked this conversation as resolved.
Show resolved Hide resolved
#[pallet::weight({
let main_dispatch_info = main.get_dispatch_info();
Expand All @@ -490,7 +514,7 @@ pub mod pallet {
let is_root = ensure_root(origin.clone()).is_ok();

// Track the weights
let mut weight = Weight::zero();
let mut weight = T::WeightInfo::if_else();

let info = main.get_dispatch_info();

Expand All @@ -504,30 +528,31 @@ pub mod pallet {
// Add weight of the main call
weight = weight.saturating_add(extract_actual_weight(&main_result, &info));

if let Err(_main_call_error) = main_result {
if let Err(main_error) = main_result {
// 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.clone())
fallback.dispatch(origin)
};

// Add weight of the fallback call
weight =
weight.saturating_add(extract_actual_weight(&fallback_result, &fallback_info));

if let Err(_fallback_error) = fallback_result {
if let Err(fallback_error) = fallback_result {
// Both calls have faild.
Self::deposit_event(Event::IfElseBothFailure { main_error: main_error.error, fallback_error: fallback_error.error });
return Err(Error::<T>::InvalidCalls.into())
rainbow-promise marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

We can also do the refund in case of failure.

Suggested change
return Err(Error::<T>::InvalidCalls.into())
return Err(DispatchErrorWithPostInfo { error: Error::<T>::InvalidCalls.into(), post_info: Some(weight).into() })

}
// Fallback succeeded.
Self::deposit_event(Event::IfElseCompleted { call: Which::Fallback });
Self::deposit_event(Event::IfElseFallbackSuccess { main_error: main_error.error });
return Ok(Some(weight).into());
}
// Main call succeeded.
Self::deposit_event(Event::IfElseCompleted { call: Which::Main });
Self::deposit_event(Event::IfElseMainSuccess);
Ok(Some(weight).into())
}
}
Expand Down
36 changes: 36 additions & 0 deletions substrate/frame/utility/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -952,5 +952,41 @@ fn if_else_with_signed_works() {
));
assert_eq!(Balances::free_balance(1), 5);
assert_eq!(Balances::free_balance(2), 15);

System::assert_last_event(utility::Event::IfElseFallbackSuccess { main_error: TokenError::FundsUnavailable.into() }.into());
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe 2 more tests:

  • successful main call.
  • failing else call.


#[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),
Box::new(call_transfer(2, 9)),
Box::new(call_transfer(2, 1))
));
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_else_call() {
new_test_ext().execute_with(|| {
assert_eq!(Balances::free_balance(1), 10);
assert_eq!(Balances::free_balance(2), 10);
assert_noop!(Utility::if_else(
RuntimeOrigin::signed(1),
Box::new(call_transfer(2, 11)),
Box::new(call_transfer(2, 11))
), utility::Error::<Test>::InvalidCalls);
assert_eq!(Balances::free_balance(1), 10);
assert_eq!(Balances::free_balance(2), 10);

System::assert_last_event(utility::Event::IfElseBothFailure { main_error: TokenError::FundsUnavailable.into(), fallback_error: TokenError::FundsUnavailable.into()}.into());
})
}