Skip to content

apply_authorized_upgrade: Remote authorization if the version check fails#7812

Merged
bkchr merged 10 commits intomasterfrom
bkchr-code-upgrade-stuff
Mar 7, 2025
Merged

apply_authorized_upgrade: Remote authorization if the version check fails#7812
bkchr merged 10 commits intomasterfrom
bkchr-code-upgrade-stuff

Conversation

@bkchr
Copy link
Copy Markdown
Member

@bkchr bkchr commented Mar 5, 2025

This pr ensures that we remove the authorization for a runtime upgrade if the version check failed. If that check is failing, it means that the runtime upgrade is invalid and the check will never succeed.

Besides that the pr is doing some clean ups.

This pr implements the `feeless_if` feature for `apply_authorized_upgrade`. The call would be feeless if the given code is authorized.

Besides that the pr is doing some clean ups.
@bkchr bkchr added the T2-pallets This PR/Issue is related to a particular pallet. label Mar 5, 2025
@bkchr bkchr requested a review from a team as a code owner March 5, 2025 21:59
@bkchr
Copy link
Copy Markdown
Member Author

bkchr commented Mar 5, 2025

/cmd prdoc --audience runtime_user --bump major

/// All origins are allowed.
#[pallet::call_index(11)]
#[pallet::weight((T::SystemWeightInfo::apply_authorized_upgrade(), DispatchClass::Operational))]
#[pallet::feeless_if(|_: &OriginFor<T>, code: &Vec<u8>| -> bool {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice! Finally, a real usage example. I will also adjust my PR accordingly.

But iiuc, feeless_if won't work unless we add SkipCheckIfFeeless and pallet_skip_feeless_payment to the runtimes, right? Please see my related questions here: #7592 (comment).

So, iiuc, feeless_if won't have any effect without SkipCheckIfFeeless, but in this case, execution will still be free because of pays_fee: Pays::No, correct?

Next, if we add SkipCheckIfFeeless and pallet_skip_feeless_payment, and we change DispatchResultWithPostInfo to DispatchResult, then we no longer need the PostDispatchInfo { pays_fee: Pays::No, ... } code, right? (Probably because of prevent further transactions we want to keep it as it is here)

And lastly, when we make feeless_if + SkipCheckIfFeeless work with TransactionExtensions, how does it behave in the case of an extrinsic error? For example, if feeless_if passes and returns true, but fn apply_authorized_upgrade(..) fails on Self::can_set_code(..), will the user still be charged, or will it remain free?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

But iiuc, feeless_if won't work unless we add SkipCheckIfFeeless and pallet_skip_feeless_payment to the runtimes, right? Please see my related questions here: #7592 (comment).

Yes feeless_if is effective only if SkipCheckIfFeeless is used, usually wrapping the transaction payment extension.

So, iiuc, feeless_if won't have any effect without SkipCheckIfFeeless, but in this case, execution will still be free because of pays_fee: Pays::No, correct?

It would be free only if successful, people will have to pay for it, and they get refunded at the end of the transaction if is successful.

Next, if we add SkipCheckIfFeeless and pallet_skip_feeless_payment, and we change DispatchResultWithPostInfo to DispatchResult, then we no longer need the PostDispatchInfo { pays_fee: Pays::No, ... } code, right? (Probably because of prevent further transactions we want to keep it as it is here)

Yes, but it doesn't harm.

And lastly, when we make feeless_if + SkipCheckIfFeeless work with TransactionExtensions, how does it behave in the case of an extrinsic error? For example, if feeless_if passes and returns true, but fn apply_authorized_upgrade(..) fails on Self::can_set_code(..), will the user still be charged, or will it remain free?

if feeless_if passes, the user is never charged, (the transaction payment happens in the transaction extension in prepare phase, this payment will be skipped).
So if the transaction fails the changes in the call dispatch is reverted. So probably people will be able to spam a failing transaction for free.
Signer will not be charged after the failing of the transaction.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@gui1117 Thank you :)

So if the transaction fails the changes in the call dispatch is reverted. So probably people will be able to spam a failing transaction for free.
Signer will not be charged after the failing of the transaction.

This is exactly what I was worried about :), but we probably just need to ensure that feeless_if is correct and covers all the necessary validations, right?

Also, another thing, here we are using/expecting an unsigned call: ValidateUnsigned::validate_unsigned(Call::apply_authorized_upgrade(..)). So, is this the spam protection mechanism that it doesn’t even add call to the transaction pool?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

will the user still be charged, or will it remain free?

If this fails, it is not because of the guy who tried to apply the authorized call, it is because of the one who wanted authorized some incorrect upgrade.

Copy link
Copy Markdown
Contributor

@georgepisaltu georgepisaltu left a comment

Choose a reason for hiding this comment

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

The inherent problem of feeless_if is that it does the check for call validity twice, once in the feeless_if closure called on the SkipCheckIfFeeless extension wrapping the tx fee extension, and then it must do the checks again inside the call, otherwise it may be spammable. If the feeless_if check fails, all it does is charge the fee, so it is mandatory that the checks are done again in the call.

We have a better approach for these free transactions using the authorized mechanism, soon to be introduced in #6324. You can see it addressing the issue of this PR in this draft implementation here.

After the last 3 remaining PRs related to this mechanic are merged, we should deprecate SkipCheckIfFeeless altogether and use the new mechanic. I don't think there's anything wrong in this PR but I don't see a reason to merge it considering we have a better solution already drafted.

@bkchr
Copy link
Copy Markdown
Member Author

bkchr commented Mar 6, 2025

We have a better approach for these free transactions using the authorized mechanism, soon to be introduced in #6324. You can see it addressing the issue of this PR in this draft implementation here.

Your draft implementation there is not correct. Like you are checking the version in the is_authorized call, which is not a great idea.

@bkchr
Copy link
Copy Markdown
Member Author

bkchr commented Mar 6, 2025

But yeah, I will remove the feeless_if stuff and just merge the rest of the code changes.

@bkchr bkchr changed the title apply_authorized_upgrade: Implement feeless_if apply_authorized_upgrade: Remote authorization if the version check fails Mar 6, 2025
@bkchr
Copy link
Copy Markdown
Member Author

bkchr commented Mar 6, 2025

@bkontur @georgepisaltu @gui1117 as I have you here now, please review :D

@paritytech-workflow-stopper
Copy link
Copy Markdown

All GitHub workflows were cancelled due to failure one of the required jobs.
Failed workflow url: https://github.com/paritytech/polkadot-sdk/actions/runs/13706992656
Failed job name: fmt

@bkchr bkchr requested a review from gui1117 March 6, 2025 20:31
@bkchr bkchr enabled auto-merge March 6, 2025 21:21
fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
if let Call::apply_authorized_upgrade { ref code } = call {
if let Ok(hash) = Self::validate_authorized_upgrade(&code[..]) {
if let Ok(res) = Self::validate_code_is_authorized(&code[..]) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If a migration is ongoing, the call will fail, and here we don't test for it. But at the same time, if a migration is ongoing it is impossible to dispatch the transaction, so we are fine.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The call is still valid. We are not testing here if the call will not error, we never do this.

Copy link
Copy Markdown
Contributor

@gui1117 gui1117 Mar 10, 2025

Choose a reason for hiding this comment

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

Yes, but failing code is roll-backed, so people can spam the chain with this failing code for free, it is better to avoid.
But here we are fine.
And also in any case, the provides being unique, I think it can't spam the transaction pool, only one per block would then be included. But still validator could fill the block with useless extrinsic and the fees would go up.

I wouldn't say a replayable free failing call is valid.

EDIT: Maybe it is useful to keep it around in the transaction pool until it can dispatched, I guess the correct implementation would be to mark it as invalid: Future.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You should not upload invalid wasm files that would fail this check :)

Also the code is now changed to remove the authorization if someone tried and the version check failed. So, you can only send this once.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the check I was talking about is T::MultiBlockMigrator::ongoing()

if T::MultiBlockMigrator::ongoing() {
return CanSetCodeResult::MultiBlockMigrationsOngoing
}

Not about the version check.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hmm yeah okay, good point.

@bkchr bkchr added this pull request to the merge queue Mar 7, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Mar 7, 2025
@bkchr bkchr added this pull request to the merge queue Mar 7, 2025
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

Status: Done

Development

Successfully merging this pull request may close these issues.

6 participants