Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Introduce on_runtime_upgrade similarly to on_initialize but executed before. #4056

Closed
gui1117 opened this issue Nov 8, 2019 · 9 comments · Fixed by #5058
Closed

Introduce on_runtime_upgrade similarly to on_initialize but executed before. #4056

gui1117 opened this issue Nov 8, 2019 · 9 comments · Fixed by #5058
Labels
J0-enhancement An additional feature request.
Milestone

Comments

@gui1117
Copy link
Contributor

gui1117 commented Nov 8, 2019

With the recent introduction of StorageValue::translate a new pattern appeared in on_initialize:

fn on_initialize() {
    if new_runtime.get() == true {
        MyStorageValue::translate(...);
        new_runtime.put(false);
    }
}

The error proneness is that on_initialize are executed in the order declaration in construct_runtime. Thus if one upper module in on_initialize call your module and use this storage, then the value is still the old encoded one. And thus can leads to very error.

To solve this we can introduce a new function: on_runtime_upgrade_initialize which will be call before all on_initialize.
With this the code to check is just the code contained in on_runtime_upgrade_initialize of other module which logic is easier to audit.

cc @shawntabrizi

@gui1117 gui1117 added the J0-enhancement An additional feature request. label Nov 8, 2019
@gui1117 gui1117 added this to the As-and-when milestone Nov 8, 2019
@gui1117
Copy link
Contributor Author

gui1117 commented Dec 11, 2019

note: even if we execute on_runtime_upgrade just before on_initialize of next block we still have some logic executing in between for validating transaction:

such logic is regular validate transaction but also the per-pallet defined signed extensions, this signed extensions logic could fail.

Maybe the validate_transaction could return an error with transaction_validity::UnknownTransaction::CannotLookUp on runtime upgrade.

Either on all transaction but that means having an empty block after each upgrade (which looks fine) or only for transaction that have some signed extension define in module.

EDIT: having an empty block after an update looks very fine to me actually, or making the queue aware that transactions invalid for this block should be kept.

@gui1117
Copy link
Contributor Author

gui1117 commented Dec 13, 2019

note: the staking module has update its storage one time, you can see an example of a migration code in frame/staking/src/migration.rs

This kind of transition work for staking but in case of SignedExtensions this has one caveat the validation of transaction in the queue could panic, thus probably having them removed from the queue. Or the signed extension validation should do the storage migration, but then this validation function can cost a lot.

@shawntabrizi
Copy link
Member

shawntabrizi commented Jan 8, 2020

Expanding on this idea, it is very possible that post-runtime-upgrade functions may want to execute logic which would take more than a single block.

In magical dream land, on_runtime_upgrade would be special such that it could handle complex logic which could take multiple blocks, and Substrate would be able to correctly handle this.

For example: Let's imagine we want to update the balances module of a live chain such that the balance type is u256 rather than u128 or something like that.

With thousands, maybe millions of accounts, a storage upgrade (#4555) would not complete in just one block.

But, if a runtime developer could simply write in the on_runtime_upgrade: "Upgrade all the balances", and magically, it should be able to batch the storage migration over multiple blocks.

As long as there is still some on_runtime_upgrade logic to be run, the blockchain will not accept any other extrinsics or runtime tasks, but the blockchain will not halt or stop producing blocks.

@gui1117
Copy link
Contributor Author

gui1117 commented Jan 8, 2020

we could also think as non-blocking migration, like we create a new storages and do as such:

  • insert value: remove value from storage1 and insert new value in storage2
  • remove: remove value from both storages
  • get: get value from storage2 if none then get value from storage1 translate it and insert it to storage2 and return it.
  • and maybe at initialization of each block we translate the nth first value of storage1

@shawntabrizi
Copy link
Member

Yes, a lazy migration pattern would also be very useful.

@bkchr
Copy link
Member

bkchr commented Jan 8, 2020

How would you know that the item was already migrated?

@shawntabrizi
Copy link
Member

You would be able to check storage2(item).exists(), right?

@bkchr
Copy link
Member

bkchr commented Jan 8, 2020

Why would the key change? And if we had 2 keys, we would need to check both keys until the end of the universe, which also requires to have one extra storage access, each time.

@gui1117
Copy link
Contributor Author

gui1117 commented Jan 8, 2020

my idea for lazy_migration was to do this as well:

and maybe at initialization of each block we translate the nth first value of storage1

Then after some block the entire storage1 will be empty thus we no longer need to check it.

It would be a performance regression for only the time of the migration.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
J0-enhancement An additional feature request.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants