-
Notifications
You must be signed in to change notification settings - Fork 709
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
[FRAME Core] Simple Multi-block Migrations #198
[FRAME Core] Simple Multi-block Migrations #198
Comments
I am currently implementing a multi block migration system for |
My 2 cents is that we should not make using things like So while
Doesn't sound too bad, I would consider keeping it a bit simpler, something some marker along the lines of |
Yes that's actually what I thought of first and I'd not be against a requirement to mark it While |
Also, by making it more explicit, we solve one other notable issue with the use of |
Okay bear with me as this might be shaving the yak. I can spin that out to another issue later but I just want to write it down here for now: In my prototype that I am building right now the migration introspection also includes the storage version it upgrades to. Right, now that allows me to check statically that the sequence of migrations makes sense (is in order without holes). It allows my to check dynamically (during What if we emit the information about the migrations that a runtime contains into a custom section. We also need to emit the in-code version of each pallet. We can then check on Maybe we could even emit each migration into its own custom section and then make offline tooling only include the required migrations when uploading the runtime. Then we might not need to periodically remove them for size reasons. |
Yes I think that is its own issue @athei We have some things planned to avoid the issue of forgetting migrations:
I dont get what you mean with "custom section". Do you think the mentioned issue above is not enough? |
With custom section I mean a section in a wasm file. We use that to store the runtime version for example. This would allow us to introspect a wasm file whether it contains all required migrations before deploying it. It would probably happen off-chain by tooling which then removes the sections before upload. It is something that comes on top of paritytech/substrate#13107. |
I want to bring @pgherveou in the loop as he's currently working on paritytech/substrate#13638 |
Just thinking about whether per-pallet migrations could be come feasible again with MB migration (i dont think so): Originally, the migrations were part of the pallets in But for both approaches i see the biggest downside being the ever-increasing WASM size… |
Yes, I also think that we should not yet fully deprecate From what I see here, MBMs would have a new interface, and a pallet wishing to define an MBM has to declare a new type/trait and has the
In other words, a type is declared to a mandatory/classic migration by implementing For now, it is safe to build this system, and assume that the pallet will expose these // runtime amalgamator;
type Migrations = (pallet_foo::Foo, pallet_bar::Bar);
type MBMs = (pallet_buzz::Buzz, pallet_qux::Qux);
type Executive = Executive<_, ..., Migrations>;
impl pallet_mbm::Config for Runtime {
type Migrations = MBMs;
} That being said, I think we are all very familiar with the pains of this approach, and could think about how we can streamline it such that you don't need to manually tweak these lists so much. Importantly, it would be great if we alleviate any risk associated with forgetting to remove the old migrations. |
The moonbeam pallet uses dynamic dispatch. Do we also want that? I think it is fine in this case since we only do one dynamic call per migration and block. It also gives us the possibility of a PS: Okay not sure if it is possible, since the |
Yeah I have exactly faced the same issue. Because different I would initially try with tuples, but in general I find code written without them easier to argue about. |
Tuples dont make this easier. I also tried that, and the aggregated tuple |
Yes, tuple or dynamic dispatch, the cursor for each migration is different, so it has to be encoded as opaque and decoded individually. This also means that, you need to keep the _type_s for the migrations (and consequently how to decode their cursor) around for as long as they need to execute. Afterwards they can be removed. |
You could also request the The definition looks clean, too: The tuple approach allows for statically checking that the sequence of versions make sense: |
Huh, that's a great idea. Did not think about it. I will try it locally, thanks! |
One interesting thing that the framework facilitate as well, and that we are pushing in this last PR, is also to enforce that no more than the exact set of migration steps are embedded into your wasm runtime |
This MR is the merge of paritytech/substrate#14414 and paritytech/substrate#14275. It implements [RFC#13](polkadot-fellows/RFCs#13), closes #198. ----- This Merge request introduces three major topicals: 1. Multi-Block-Migrations 1. New pallet `poll` hook for periodic service work 1. Replacement hooks for `on_initialize` and `on_finalize` in cases where `poll` cannot be used and some more general changes to FRAME. The changes for each topical span over multiple crates. They are listed in topical order below. # 1.) Multi-Block-Migrations Multi-Block-Migrations are facilitated by creating `pallet_migrations` and configuring `System::Config::MultiBlockMigrator` to point to it. Executive picks this up and triggers one step of the migrations pallet per block. The chain is in lockdown mode for as long as an MBM is ongoing. Executive does this by polling `MultiBlockMigrator::ongoing` and not allowing any transaction in a block, if true. A MBM is defined through trait `SteppedMigration`. A condensed version looks like this: ```rust /// A migration that can proceed in multiple steps. pub trait SteppedMigration { type Cursor: FullCodec + MaxEncodedLen; type Identifier: FullCodec + MaxEncodedLen; fn id() -> Self::Identifier; fn max_steps() -> Option<u32>; fn step( cursor: Option<Self::Cursor>, meter: &mut WeightMeter, ) -> Result<Option<Self::Cursor>, SteppedMigrationError>; } ``` `pallet_migrations` can be configured with an aggregated tuple of these migrations. It then starts to migrate them one-by-one on the next runtime upgrade. Two things are important here: - 1. Doing another runtime upgrade while MBMs are ongoing is not a good idea and can lead to messed up state. - 2. **Pallet Migrations MUST BE CONFIGURED IN `System::Config`, otherwise it is not used.** The pallet supports an `UpgradeStatusHandler` that can be used to notify external logic of upgrade start/finish (for example to pause XCM dispatch). Error recovery is very limited in the case that a migration errors or times out (exceeds its `max_steps`). Currently the runtime dev can decide in `FailedMigrationHandler::failed` how to handle this. One follow-up would be to pair this with the `SafeMode` pallet and enact safe mode when an upgrade fails, to allow governance to rescue the chain. This is currently not possible, since governance is not `Mandatory`. ## Runtime API - `Core`: `initialize_block` now returns `ExtrinsicInclusionMode` to inform the Block Author whether they can push transactions. ### Integration Add it to your runtime implementation of `Core` and `BlockBuilder`: ```patch diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs @@ impl_runtime_apis! { impl sp_block_builder::Core<Block> for Runtime { - fn initialize_block(header: &<Block as BlockT>::Header) { + fn initialize_block(header: &<Block as BlockT>::Header) -> RuntimeExecutiveMode { Executive::initialize_block(header) } ... } ``` # 2.) `poll` hook A new pallet hook is introduced: `poll`. `Poll` is intended to replace mostly all usage of `on_initialize`. The reason for this is that any code that can be called from `on_initialize` cannot be migrated through an MBM. Currently there is no way to statically check this; the implication is to use `on_initialize` as rarely as possible. Failing to do so can result in broken storage invariants. The implementation of the poll hook depends on the `Runtime API` changes that are explained above. # 3.) Hard-Deadline callbacks Three new callbacks are introduced and configured on `System::Config`: `PreInherents`, `PostInherents` and `PostTransactions`. These hooks are meant as replacement for `on_initialize` and `on_finalize` in cases where the code that runs cannot be moved to `poll`. The reason for this is to make the usage of HD-code (hard deadline) more explicit - again to prevent broken invariants by MBMs. # 4.) FRAME (general changes) ## `frame_system` pallet A new memorize storage item `InherentsApplied` is added. It is used by executive to track whether inherents have already been applied. Executive and can then execute the MBMs directly between inherents and transactions. The `Config` gets five new items: - `SingleBlockMigrations` this is the new way of configuring migrations that run in a single block. Previously they were defined as last generic argument of `Executive`. This shift is brings all central configuration about migrations closer into view of the developer (migrations that are configured in `Executive` will still work for now but is deprecated). - `MultiBlockMigrator` this can be configured to an engine that drives MBMs. One example would be the `pallet_migrations`. Note that this is only the engine; the exact MBMs are injected into the engine. - `PreInherents` a callback that executes after `on_initialize` but before inherents. - `PostInherents` a callback that executes after all inherents ran (including MBMs and `poll`). - `PostTransactions` in symmetry to `PreInherents`, this one is called before `on_finalize` but after all transactions. A sane default is to set all of these to `()`. Example diff suitable for any chain: ```patch @@ impl frame_system::Config for Test { type MaxConsumers = ConstU32<16>; + type SingleBlockMigrations = (); + type MultiBlockMigrator = (); + type PreInherents = (); + type PostInherents = (); + type PostTransactions = (); } ``` An overview of how the block execution now looks like is here. The same graph is also in the rust doc. <details><summary>Block Execution Flow</summary> <p> ![Screenshot 2023-12-04 at 19 11 29](https://github.com/paritytech/polkadot-sdk/assets/10380170/e88a80c4-ef11-4faa-8df5-8b33a724c054) </p> </details> ## Inherent Order Moved to #2154 --------------- ## TODO - [ ] Check that `try-runtime` still works - [ ] Ensure backwards compatibility with old Runtime APIs - [x] Consume weight correctly - [x] Cleanup --------- Signed-off-by: Oliver Tale-Yazdi <[email protected]> Co-authored-by: Liam Aharon <[email protected]> Co-authored-by: Juan Girini <[email protected]> Co-authored-by: command-bot <> Co-authored-by: Francisco Aguirre <[email protected]> Co-authored-by: Gavin Wood <[email protected]> Co-authored-by: Bastian Köcher <[email protected]>
…ech#1781) This MR is the merge of paritytech/substrate#14414 and paritytech/substrate#14275. It implements [RFC#13](polkadot-fellows/RFCs#13), closes paritytech#198. ----- This Merge request introduces three major topicals: 1. Multi-Block-Migrations 1. New pallet `poll` hook for periodic service work 1. Replacement hooks for `on_initialize` and `on_finalize` in cases where `poll` cannot be used and some more general changes to FRAME. The changes for each topical span over multiple crates. They are listed in topical order below. # 1.) Multi-Block-Migrations Multi-Block-Migrations are facilitated by creating `pallet_migrations` and configuring `System::Config::MultiBlockMigrator` to point to it. Executive picks this up and triggers one step of the migrations pallet per block. The chain is in lockdown mode for as long as an MBM is ongoing. Executive does this by polling `MultiBlockMigrator::ongoing` and not allowing any transaction in a block, if true. A MBM is defined through trait `SteppedMigration`. A condensed version looks like this: ```rust /// A migration that can proceed in multiple steps. pub trait SteppedMigration { type Cursor: FullCodec + MaxEncodedLen; type Identifier: FullCodec + MaxEncodedLen; fn id() -> Self::Identifier; fn max_steps() -> Option<u32>; fn step( cursor: Option<Self::Cursor>, meter: &mut WeightMeter, ) -> Result<Option<Self::Cursor>, SteppedMigrationError>; } ``` `pallet_migrations` can be configured with an aggregated tuple of these migrations. It then starts to migrate them one-by-one on the next runtime upgrade. Two things are important here: - 1. Doing another runtime upgrade while MBMs are ongoing is not a good idea and can lead to messed up state. - 2. **Pallet Migrations MUST BE CONFIGURED IN `System::Config`, otherwise it is not used.** The pallet supports an `UpgradeStatusHandler` that can be used to notify external logic of upgrade start/finish (for example to pause XCM dispatch). Error recovery is very limited in the case that a migration errors or times out (exceeds its `max_steps`). Currently the runtime dev can decide in `FailedMigrationHandler::failed` how to handle this. One follow-up would be to pair this with the `SafeMode` pallet and enact safe mode when an upgrade fails, to allow governance to rescue the chain. This is currently not possible, since governance is not `Mandatory`. ## Runtime API - `Core`: `initialize_block` now returns `ExtrinsicInclusionMode` to inform the Block Author whether they can push transactions. ### Integration Add it to your runtime implementation of `Core` and `BlockBuilder`: ```patch diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs @@ impl_runtime_apis! { impl sp_block_builder::Core<Block> for Runtime { - fn initialize_block(header: &<Block as BlockT>::Header) { + fn initialize_block(header: &<Block as BlockT>::Header) -> RuntimeExecutiveMode { Executive::initialize_block(header) } ... } ``` # 2.) `poll` hook A new pallet hook is introduced: `poll`. `Poll` is intended to replace mostly all usage of `on_initialize`. The reason for this is that any code that can be called from `on_initialize` cannot be migrated through an MBM. Currently there is no way to statically check this; the implication is to use `on_initialize` as rarely as possible. Failing to do so can result in broken storage invariants. The implementation of the poll hook depends on the `Runtime API` changes that are explained above. # 3.) Hard-Deadline callbacks Three new callbacks are introduced and configured on `System::Config`: `PreInherents`, `PostInherents` and `PostTransactions`. These hooks are meant as replacement for `on_initialize` and `on_finalize` in cases where the code that runs cannot be moved to `poll`. The reason for this is to make the usage of HD-code (hard deadline) more explicit - again to prevent broken invariants by MBMs. # 4.) FRAME (general changes) ## `frame_system` pallet A new memorize storage item `InherentsApplied` is added. It is used by executive to track whether inherents have already been applied. Executive and can then execute the MBMs directly between inherents and transactions. The `Config` gets five new items: - `SingleBlockMigrations` this is the new way of configuring migrations that run in a single block. Previously they were defined as last generic argument of `Executive`. This shift is brings all central configuration about migrations closer into view of the developer (migrations that are configured in `Executive` will still work for now but is deprecated). - `MultiBlockMigrator` this can be configured to an engine that drives MBMs. One example would be the `pallet_migrations`. Note that this is only the engine; the exact MBMs are injected into the engine. - `PreInherents` a callback that executes after `on_initialize` but before inherents. - `PostInherents` a callback that executes after all inherents ran (including MBMs and `poll`). - `PostTransactions` in symmetry to `PreInherents`, this one is called before `on_finalize` but after all transactions. A sane default is to set all of these to `()`. Example diff suitable for any chain: ```patch @@ impl frame_system::Config for Test { type MaxConsumers = ConstU32<16>; + type SingleBlockMigrations = (); + type MultiBlockMigrator = (); + type PreInherents = (); + type PostInherents = (); + type PostTransactions = (); } ``` An overview of how the block execution now looks like is here. The same graph is also in the rust doc. <details><summary>Block Execution Flow</summary> <p> ![Screenshot 2023-12-04 at 19 11 29](https://github.com/paritytech/polkadot-sdk/assets/10380170/e88a80c4-ef11-4faa-8df5-8b33a724c054) </p> </details> ## Inherent Order Moved to paritytech#2154 --------------- ## TODO - [ ] Check that `try-runtime` still works - [ ] Ensure backwards compatibility with old Runtime APIs - [x] Consume weight correctly - [x] Cleanup --------- Signed-off-by: Oliver Tale-Yazdi <[email protected]> Co-authored-by: Liam Aharon <[email protected]> Co-authored-by: Juan Girini <[email protected]> Co-authored-by: command-bot <> Co-authored-by: Francisco Aguirre <[email protected]> Co-authored-by: Gavin Wood <[email protected]> Co-authored-by: Bastian Köcher <[email protected]>
Needed for non-trivial migrations on parachains, and helpful for such migration on Relay/solo chains.
This is just a simple strategy and will work in probably around 80% of circumstances. It's good for pallets which are not depended upon by code which fires in
on_initialize
.Have a migrations pallet (not too dissimilar from that of Moonbeam). This is configured with a tuple of all migrations, which provides introspection and access to each migration which needs to be completed. Migrations look like calls/tasks: they have a possibly-pessimistic weight estimation and can return the actual weight consumed. They are always benchmarked. Migrations should be written to proceed "piecewise" (e.g. one unit at a time, rather than migrating all entries of a storage map all at once).
Each migration defines a type
Cursor
which isDefault + FullCodec + MaxEncodeLen
. The migration function accepts aCursor
value and returns anOption<Cursor>
value which isNone
if the migration is complete. IfSome
then it is up to the migration pallet to ensure it is used in the next call to the migrate function, storing it in state if weight constraints deem it necessary to wait until the next block before resuming.The migrations pallet should suspend all non-critical subsystems in
on_runtime_upgrade
. This includes XCM and transaction processing. It should only re-enable them when all migrations' cursors areNone
.One new hook function should be added:
fn poll() -> Weight
. This is guaranteed to execute regularly but not necessarily every block, providing a softer version ofon_initialize
. All Substrate pallets should have theiron_initialize
/on_finalize
functions checked; if they can be moved or adapted topoll
, they should be. Whileon_initialize
/on_finalize
should be called within an incomplete migration,poll
need not (and indeed, should not) be.Long-term
Deprecate
on_*
hookson_initialize
/on_finalize
should be deprecated. Pallets which absolutely require per-block-execution hooks on hard-deadlines (HDH) or mandatory extrinsics (ME) should go through a System config trait item which can give them low-level access at the cost of requiring the System config trait to be explicitly configured to reference them. There should be a regular facility for soft-deadline execution (which will work fine in almost all use-cases of parachain teams) which is implemented using tasks.Guarantees
We should mark all storage items so they fall into one of three groups:
Ideally we could have some means of statically verifying that HDH code only accessed items marked as (1) and MBM code only accessed items marked as (2).
Unfortunately, this probably cannot be done statically (can it?) but at least could form a basis for code reviews and audit.
The text was updated successfully, but these errors were encountered: