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

Fix treasury benchmarks when no SpendOrigin #3049

Merged
merged 8 commits into from
Sep 13, 2024

Conversation

bgallois
Copy link
Contributor

@bgallois bgallois commented Jan 24, 2024

Issue

It was impossible to benchmark the pallet_treasury when SpendOrigin = frame_support::traits::NeverEnsureOrigin<u64>; was specified.

Done

  • Use weight = 0 for all extrinsics that are un-callable with no SpendOrigin.
  • Fix benchmarks for extrinsics requiring a Spend even if SpendOrigin = frame_support::traits::NeverEnsureOrigin<u64>;

@bgallois bgallois requested review from a team January 24, 2024 15:47
@cla-bot-2021
Copy link

cla-bot-2021 bot commented Jan 24, 2024

User @bgallois, please sign the CLA here.

@bgallois bgallois marked this pull request as draft January 24, 2024 16:05
@bgallois bgallois marked this pull request as ready for review January 24, 2024 16:42
@bgallois
Copy link
Contributor Author

I'm unable to sign the CLA: Content Encoding Error. An error occurred during a connection to cla.parity.io.

Copy link
Member

@ggwpez ggwpez left a comment

Choose a reason for hiding this comment

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

Thanks!

substrate/frame/treasury/src/benchmarking.rs Outdated Show resolved Hide resolved
@ggwpez
Copy link
Member

ggwpez commented Jan 27, 2024

I'm unable to sign the CLA: Content Encoding Error. An error occurred during a connection to cla.parity.io.

Hm, can you try again?
Not sure whom to ping here @mutantcornholio maybe?

@bgallois
Copy link
Contributor Author

I'm unable to sign the CLA: Content Encoding Error. An error occurred during a connection to cla.parity.io.

Hm, can you try again? Not sure whom to ping here @mutantcornholio maybe?

Still no luck.

Copy link
Member

@bkchr bkchr left a comment

Choose a reason for hiding this comment

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

This is actually not correct. You are right that the benchmarks are not correct the way they are written, but these calls clearly don't have zero weight when SpendOrigin fails. If you look at the functions of the dispatchables, you will see that we access Spends aka reading from the DB which is one of the most expensive operations we have.

@bgallois
Copy link
Contributor Author

bgallois commented Jan 28, 2024

This is actually not correct. You are right that the benchmarks are not correct the way they are written, but these calls clearly don't have zero weight when SpendOrigin fails. If you look at the functions of the dispatchables, you will see that we access Spends aka reading from the DB which is one of the most expensive operations we have.

In fact, these extrinsics should not be callable when SpendOrigin = frame_support::traits::NeverEnsureOrigin<u64>;. In this case, we can authorize weight=0 so that the benchmark can pass for this pallet (or that is the expected behavior I understand from the benchmarks already written).

Another solution is to benchmark the path until failure occurs and use this weight as the default weight, and these extrinsics could be callable even with SpendOrigin = frame_support::traits::NeverEnsureOrigin<u64>;.

@ggwpez
Copy link
Member

ggwpez commented Jan 28, 2024

You are right that the benchmarks are not correct the way they are written, but these calls clearly don't have zero weight when SpendOrigin fails.

Yes true. But he is fixing the case that SpendOrigin is configured to be impossible to pass. There should be no DB reads in that case, right?
Otherwise the try_success_origin for benchmarks is advised to return something that always works by using a feature flag. Although its better to go through the normal setup to also benchmark the effort of the try_success_origin.

@bkchr
Copy link
Member

bkchr commented Jan 28, 2024

pub fn payout(origin: OriginFor<T>, index: SpendIndex) -> DispatchResult {
ensure_signed(origin)?;
let mut spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
let now = frame_system::Pallet::<T>::block_number();

pub fn check_status(origin: OriginFor<T>, index: SpendIndex) -> DispatchResultWithPostInfo {
use PaymentState as State;
use PaymentStatus as Status;
ensure_signed(origin)?;
let mut spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
let now = frame_system::Pallet::<T>::block_number();

pub fn void_spend(origin: OriginFor<T>, index: SpendIndex) -> DispatchResult {
T::RejectOrigin::ensure_origin(origin)?;
let spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;

As we can see, there are reads. If SpendOrigin isn't set, this prevents that any value is stored in Spends. However, it doesn't prevent that these other functions are doing a read.

@bgallois
Copy link
Contributor Author

You are right that the benchmarks are not correct the way they are written, but these calls clearly don't have zero weight when SpendOrigin fails.

Yes true. But he is fixing the case that SpendOrigin is configured to be impossible to pass. There should be no DB reads in that case, right? Otherwise the try_success_origin for benchmarks is advised to return something that always works by using a feature flag. Although its better to go through the normal setup to also benchmark the effort of the try_success_origin.

Alright, but in this case, we need to adapt the three benchmarks(check_status, void_spend and payout) that always failed not because of a SpendOrigin check, but because the extrinsic will fail when reading the Spend storage, which will always be empty. For these, the weight will be one signed origin check and then one read to the database.

@bgallois bgallois marked this pull request as draft January 28, 2024 14:37
@bkchr
Copy link
Member

bkchr commented Jan 28, 2024

Alright, but in this case, we need to adapt the three benchmarks(check_status, void_spend and payout) that always failed not because of a SpendOrigin check,

Yes, I actually wanted this. I should have been more clear :D

@mutantcornholio
Copy link
Contributor

I'm unable to sign the CLA: Content Encoding Error. An error occurred during a connection to cla.parity.io.

Hm, can you try again? Not sure whom to ping here @mutantcornholio maybe?

Thanks for report, I'm on it

@bgallois bgallois marked this pull request as ready for review January 29, 2024 10:59
Comment on lines 110 to 114
fn force_add_spend<T: Config<I>, I: 'static>(
asset_kind: Box<T::AssetKind>,
amount: AssetBalanceOf<T, I>,
beneficiary: Box<BeneficiaryLookupOf<T, I>>,
) -> Result<(), &'static str> {
Copy link
Member

Choose a reason for hiding this comment

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

This is not correct. What you should do is to skip adding something to Spend when T::SpendOrigin::try_successful_origin() fails.

So,

		if let Ok(origin) = T::SpendOrigin::try_successful_origin() {
		Treasury::<T, _>::spend(
			origin,
		force_add_spend::<T, _>(
			Box::new(asset_kind.clone()),
			amount,
			Box::new(beneficiary_lookup),
			None,
		)?;
}

is the logic I would expect for these benchmarks below.

Copy link
Contributor Author

@bgallois bgallois Jan 29, 2024

Choose a reason for hiding this comment

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

If we omit adding something to the spend, the benchmark will fail during execution with the 'InvalidIndex' error. While it is probably possible to bypass this error using a solution like this, it appears error-prone with an extrinsic that will sometime complete and sometime fail.

As extrinsic benchmark cover worst case scenario, It seems not far-fetched that even with SpendOrigin = frame_support::traits::NeverEnsureOrigin<u64> we take this impossible case.

Copy link
Member

Choose a reason for hiding this comment

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

I mean we should be able to use the same condition as above to check either on success or error?

CC @ggwpez

Copy link
Contributor Author

@bgallois bgallois Jan 30, 2024

Choose a reason for hiding this comment

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

I know that the #[extrinsic_call] cannot be included inside a conditional block.

If there is a way to perform a benchmark with a condition and two expected results, I have never seen it.

Copy link
Member

Choose a reason for hiding this comment

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

You can use #[block] and put the if inside:

#[block]
{
    if condition {
        ...
    } else {
        ...
    }
}

Condition should be a bool already to not mess up the timing. Otherwise you can create a new benchmark just for the failing case and then use the maximum of the two as weight.

Copy link
Contributor Author

@bgallois bgallois Jan 31, 2024

Choose a reason for hiding this comment

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

The more general case (forcing a spend then processing it) has a weight that is superior to the edge case (one read then an error), thus posing no risk of spamming without overly complicating the benchmark.

Let take the case of:

pub fn payout(origin: OriginFor<T>, index: SpendIndex) -> DispatchResult {
ensure_signed(origin)?;
let mut spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
let now = frame_system::Pallet::<T>::block_number();
ensure!(now >= spend.valid_from, Error::<T, I>::EarlyPayout);
ensure!(spend.expire_at > now, Error::<T, I>::SpendExpired);
ensure!(
matches!(spend.status, PaymentState::Pending | PaymentState::Failed),
Error::<T, I>::AlreadyAttempted
);
let id = T::Paymaster::pay(&spend.beneficiary, spend.asset_kind.clone(), spend.amount)
.map_err(|_| Error::<T, I>::PayoutError)?;
spend.status = PaymentState::Attempted { id };
Spends::<T, I>::insert(index, spend);
Self::deposit_event(Event::<T, I>::Paid { index, payment_id: id });
Ok(())

The worst-case scenario occurs when a valid spend is found and paid. Therefore, even if SpendOrigin = frame_support::traits::NeverEnsureOrigin<u64>, and in reality, no spend can be added, we can still benchmark the worst case by forcibly adding a spend (what I have done in the last commit).

It is analog to an extrinsic than necessitate a root origin, we write a benchmark using the root origin and always charge the weight of this path. We did not split the benchmark to one with success when root and one with a fail when not root. The only time we split the benchmark is when the extrinsic will always fail and when there is no valid path, that is not the case with payout, check_status, and void_spend, but is it the case for spend, spend_local.

Copy link
Member

Choose a reason for hiding this comment

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

The worst-case scenario occurs when a valid spend is found and paid.

But this isn't the worst-case when you have SpendOrigin = NeverEnsureOrigin<u64>;. Then the worst-case is basically the best case as well and just means that there is only one read of the storage.

Copy link
Contributor Author

@bgallois bgallois Feb 3, 2024

Choose a reason for hiding this comment

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

The worst-case scenario occurs when a valid spend is found and paid.

But this isn't the worst-case when you have SpendOrigin = NeverEnsureOrigin<u64>;. Then the worst-case is basically the best case as well and just means that there is only one read of the storage.

From the extrinsic point of view, it is the worst path because there is no mention of SpendOrigin in its implementation. In all the benchmarks that I found in Substrate, the worst path is determined from the extrinsic perspective, rather than considering all the (sometimes complicated) interference of all the parameters that can play a role outside the extrinsic implementation (and that can be quite complicated to anticipate when writing the benchmark, see this error for example).

I think that the implementation you're asking is suboptimal by adding unnecessary complexity to the benchmark for something that doesn't pose any threat to the chain and just add a slight overweight. However, you seem very committed to this specific approach, so I will move this pull request to draft status and I will implement it according to your suggestions when I have some time.

Copy link
Member

Choose a reason for hiding this comment

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

Not sure your arguments are correct here. I mean the problem here is that the benchmark is failing because it is written in an incorrect way. The benchmark is "abusing" SpendOrigin to setup the state and the benchmark is failing because SpendOrigin is set to NeverEnsureOrigin. Now if we fix the benchmark, we should do it properly that it generates the correct weights. The thing I'm proposing here is also not really that complicated and can be implemented in some minutes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I agree that the benchmark is failing because it was written incorrectly. In my opinion, this failure directly stems from not approaching it from the extrinsic point of view, as I explained before.

No, it's not complicated. However, since I'm paid to work on another Substrate chain, this fix isn't a priority in my work schedule that is quite full at the moment. Feel free to complete it if you want it done rapidly (i.e. before the end of a week).

@mutantcornholio
Copy link
Contributor

@bgallois could you try signing the CLA again please?

@bgallois
Copy link
Contributor Author

@bgallois could you try signing the CLA again please?

Thanks it is working :)

@bgallois bgallois marked this pull request as draft February 3, 2024 11:42
@Hugo-Trentesaux
Copy link
Contributor

The thing I'm proposing here is also not really that complicated and can be implemented in some minutes.

@bkchr, those minutes transformed into months and the benchmarks are still broken.

If you are still interested by a fix, we have it on our fork: https://github.com/duniter/duniter-polkadot-sdk/commits/duniter-substrate-v1.14.0/ (latest commit here).

@bkchr
Copy link
Member

bkchr commented Sep 13, 2024

@Hugo-Trentesaux I have done now the changes as I had requested.

@bkchr bkchr added the T2-pallets This PR/Issue is related to a particular pallet. label Sep 13, 2024
@bkchr bkchr added the A4-needs-backport Pull request must be backported to all maintained releases. label Sep 13, 2024
@ggwpez ggwpez added this pull request to the merge queue Sep 13, 2024
Merged via the queue into paritytech:master with commit 51f3367 Sep 13, 2024
204 of 206 checks passed
bgallois added a commit to duniter/duniter-polkadot-sdk that referenced this pull request Sep 13, 2024
It was impossible to benchmark the pallet_treasury when `SpendOrigin =
frame_support::traits::NeverEnsureOrigin<u64>;` was specified.

- [x] Use `weight = 0` for all extrinsics that are un-callable with no
`SpendOrigin`.
- [x] Fix benchmarks for extrinsics requiring a Spend even if
`SpendOrigin = frame_support::traits::NeverEnsureOrigin<u64>;`

---------

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Co-authored-by: Bastian Köcher <info@kchr.de>
Co-authored-by: Bastian Köcher <git@kchr.de>
Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
@bgallois
Copy link
Contributor Author

Thank you for the fix.

There is still a small attack vector if a chain is improbably (mis)configured with SpendOrigin set to NeverOrigin, but RejectOrigin can be successful. In this case, the code from remove_approval could be executed without any weight at all.

@bgallois bgallois deleted the fix_treasury_benchmark branch September 13, 2024 18:31
@bkchr
Copy link
Member

bkchr commented Sep 13, 2024

Ty! #5713

@ggwpez ggwpez added A4-needs-backport Pull request must be backported to all maintained releases. and removed A4-needs-backport Pull request must be backported to all maintained releases. labels Sep 16, 2024
@paritytech-cmd-bot-polkadot-sdk

Git push to origin failed for stable2407 with exitcode 1

@paritytech-cmd-bot-polkadot-sdk

Successfully created backport PR for stable2409:

github-actions bot pushed a commit that referenced this pull request Sep 16, 2024
### Issue

It was impossible to benchmark the pallet_treasury when `SpendOrigin =
frame_support::traits::NeverEnsureOrigin<u64>;` was specified.

### Done

- [x] Use `weight = 0` for all extrinsics that are un-callable with no
`SpendOrigin`.
- [x] Fix benchmarks for extrinsics requiring a Spend even if
`SpendOrigin = frame_support::traits::NeverEnsureOrigin<u64>;`

---------

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Co-authored-by: Bastian Köcher <info@kchr.de>
Co-authored-by: Bastian Köcher <git@kchr.de>
Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
(cherry picked from commit 51f3367)
@ggwpez ggwpez added A4-needs-backport Pull request must be backported to all maintained releases. and removed A4-needs-backport Pull request must be backported to all maintained releases. labels Sep 16, 2024
@paritytech-cmd-bot-polkadot-sdk

Created backport PR for stable2407:

Please cherry-pick the changes locally and resolve any conflicts.

git fetch origin backport-3049-to-stable2407
git worktree add --checkout .worktree/backport-3049-to-stable2407 backport-3049-to-stable2407
cd .worktree/backport-3049-to-stable2407
git reset --hard HEAD^
git cherry-pick -x 51f336711a0391987db69d6281c9b57bfe49d925
git push --force-with-lease

@paritytech-cmd-bot-polkadot-sdk

Git push to origin failed for stable2409 with exitcode 1

@ggwpez ggwpez removed the A4-needs-backport Pull request must be backported to all maintained releases. label Sep 16, 2024
ggwpez pushed a commit that referenced this pull request Sep 17, 2024
Backport #3049 into `stable2409` from bgallois.

See the
[documentation](https://github.com/paritytech/polkadot-sdk/blob/master/docs/BACKPORT.md)
on how to use this bot.

<!--
  # To be used by other automation, do not modify:
  original-pr-number: #${pull_number}
-->

Co-authored-by: Benjamin Gallois <gallois.benjamin08@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T2-pallets This PR/Issue is related to a particular pallet.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants