apply_authorized_upgrade: Remote authorization if the version check fails#7812
apply_authorized_upgrade: Remote authorization if the version check fails#7812
apply_authorized_upgrade: Remote authorization if the version check fails#7812Conversation
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.
|
/cmd prdoc --audience runtime_user --bump major |
substrate/frame/system/src/lib.rs
Outdated
| /// 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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
But iiuc,
feeless_ifwon't work unless we addSkipCheckIfFeelessandpallet_skip_feeless_paymentto 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_ifwon't have any effect withoutSkipCheckIfFeeless, but in this case, execution will still be free because ofpays_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
SkipCheckIfFeelessandpallet_skip_feeless_payment, and we changeDispatchResultWithPostInfotoDispatchResult, then we no longer need thePostDispatchInfo { pays_fee: Pays::No, ... }code, right? (Probably because ofprevent further transactionswe want to keep it as it is here)
Yes, but it doesn't harm.
And lastly, when we make
feeless_if+SkipCheckIfFeelesswork withTransactionExtensions, how does it behave in the case of an extrinsic error? For example, iffeeless_ifpasses and returnstrue, butfn apply_authorized_upgrade(..)fails onSelf::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.
There was a problem hiding this comment.
@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?
There was a problem hiding this comment.
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.
georgepisaltu
left a comment
There was a problem hiding this comment.
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.
Your draft implementation there is not correct. Like you are checking the version in the |
|
But yeah, I will remove the |
apply_authorized_upgrade: Implement feeless_ifapply_authorized_upgrade: Remote authorization if the version check fails
|
@bkontur @georgepisaltu @gui1117 as I have you here now, please review :D |
|
All GitHub workflows were cancelled due to failure one of the required jobs. |
| 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[..]) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
The call is still valid. We are not testing here if the call will not error, we never do this.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
the check I was talking about is T::MultiBlockMigrator::ongoing()
polkadot-sdk/substrate/frame/system/src/lib.rs
Lines 2269 to 2271 in 007565a
Not about the version check.
There was a problem hiding this comment.
Hmm yeah okay, good point.
This pr ensures that we remove the
authorizationfor 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.