-
Notifications
You must be signed in to change notification settings - Fork 335
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
Delay nominator revocations and exits #610
Conversation
…diate and it is now delayed and there is no leave candidates TS test to show how to delay
|
} | ||
|
||
#[pallet::hooks] | ||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { | ||
fn on_runtime_upgrade() -> Weight { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would you feel about using the migrations pallet for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know how to use it, and don't think it's necessary for this migration.
Feel free to make a PR into this branch which uses it.
|
||
#[derive(Encode, Decode, RuntimeDebug)] | ||
/// Nominator state | ||
pub struct Nominator2<AccountId, Balance> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be awesome if we could architect this migration such that the pallet itself just changes the Nominator
struct (in a non-backwards-compatible way) and we put all the gory details of what the original struct was and how to migrate in a separate file/lib/etc.
This is still a bit vague to me, but I'd be happy to help figure out what this should look like you want to go down this path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the PR description:
From storage map NominatorState -> NominatorState2 such that the keys (AccountId) stay the same, but the value for the map changes from struct Nominator -> struct Nominator2. There is an implementation of
From<Nominator> for Nominator2
which is what is used to migrate the storage values from the first map to the second.
I commented out this impl From<Nominator<AccountId, Balance>> for Nominator2<AccountId, Balance>
below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. From
is an elegant way to handle that.
I guess, then, my suggestion would come down to:
- Rename
Nominator
->OldNominator
(etc) - Move it to a different file (for clarity and/or potentially being excluded from future runtimes)
- Rename
Nominator2
->Nominator
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like pulling a #[pallet::storage]
definition out of the main pallet isn't really possible, but these utilities might help:
https://crates.parity.io/frame_support/storage/migration/index.html
I'll play with this to see if I can come up with something clean.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see.
From
is an elegant way to handle that.I guess, then, my suggestion would come down to:
- Rename
Nominator
->OldNominator
(etc)- Move it to a different file (for clarity and/or potentially being excluded from future runtimes)
- Rename
Nominator2
->Nominator
I had the same initial feeling when we did the previous migration which moved from CollatorState to CollatorState2. I had 2 reasons for that:
- It is more elegant in the code to not have a number at the end of a variable (not too important)
- It changes the way we query for the CollatorState (chain state -> parachainStaking -> CollatorState2)
But after thinking more about it, I'm not sure if it makes more sense to try to keep a same (exposed) variable with a different type. Because in both cases (changing the name of the storage, or changing its type), it is a breaking change that requires the developer to update its tools/sdk.
I'm open to discussion on this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One idea for handling these cases:
The new version of the pallet depends on the old version. The old type definition and storage definition are imported from the old pallet.
pallets/parachain-staking/src/lib.rs
Outdated
fn delay_nomination_exits_migration_execution<T: Config>() { | ||
if !<DelayNominationExitsMigration<T>>::get() { | ||
// migrate from Nominator -> Nominator2 | ||
for (acc, nominator_state) in NominatorState::<T>::drain() { | ||
let state: Nominator2<T::AccountId, BalanceOf<T>> = nominator_state.into(); | ||
<NominatorState2<T>>::insert(acc, state); | ||
} | ||
// migrate from ExitQueue -> ExitQueue2 | ||
let just_collators_exit_queue = <ExitQueue<T>>::take(); | ||
let mut candidates: Vec<T::AccountId> = Vec::new(); | ||
for (acc, _) in just_collators_exit_queue.clone().into_iter() { | ||
candidates.push(acc); | ||
} | ||
<ExitQueue2<T>>::put(ExitQ { | ||
candidates: candidates.into(), | ||
nominators_leaving: OrderedSet::new(), | ||
candidate_schedule: just_collators_exit_queue, | ||
nominator_schedule: Vec::new(), | ||
}); | ||
<DelayNominationExitsMigration<T>>::put(true); | ||
Pallet::<T>::deposit_event(Event::DelayNominationExitsMigrationExecuted); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@notlesh this is the full migration logic
It emits an event when it executes and also replaces a false storage value with true so it doesn't run more than once.
if !<DelayNominationExitsMigration<T>>::get() { | ||
// migrate from Nominator -> Nominator2 | ||
for (acc, nominator_state) in NominatorState::<T>::drain() { | ||
let state: Nominator2<T::AccountId, BalanceOf<T>> = nominator_state.into(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@notlesh This .into()
uses the From
impl, which contains exactly how we convert each Nominator
-> Nominator2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this is an elegant use of the From, IF this is only used here in the migration, I would (for future times) argue that keeping the mutation logic directly here would benefit the reader and prevent bug where someone would use the wrong structure because it gets converted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would imply we keep the old struct in the code as well. I'd prefer to move the old struct and the From
impl to docs, something that isn't code. We can link the code at each migration commit so someone could easily write From<Nominator1> for Nominator3
if they want to, but we don't have to write that ourselves.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm, I had a few questions but nothing blocking
@notlesh if you can review this one and once done change the labels (remove the pleasereview -> appropriate Ax-xxxxxx) |
I feel more comfortable with the migration design in this PR than the one in #616 (which uses To be fair, I don't fully understand the |
Thanks for the feedback. I should spend some time documenting That said, I wanted to point out that the changes to this particular migration in #616 actually have little to do with If you wanted to avoid the |
@4meta5 , let me give you a bit of context. While your migration is safe in case it gets executed multiple times, it is not safe from when we are going to remove it from the pallet. For example, when we will resume Moonshadow, it is likely that the staking pallet won't include this migration anymore, and Moonshadow wouldn't not be upgraded properly. The idea behind the pallet migration is to keep track and offer an easy solution to perform those runtime upgrade in the runtime directly. @notlesh, we should probably have you make a presentation of your pallet and also spend more time testing it before using it in production. |
This PR does contain the implementation
How does We either have to purge chain state or store migrations from every schema to every other schema. There's no other solution as far as I can see. |
Right, I've probably made some confusion here because I did multiple things in that PR. One of the suggestions it implements is to isolate the migration logic so that the main pallet itself remains uncluttered with the migration code (and also avoids needing to rename or duplicate types, storage items, etc.)
It tracks which migrations (based on their unique name) have been executed, which means that it's safe to leave a migration around as long as desired. In our case, this is helpful because we can leave something like your migration laying around until it has been deployed to all networks we care about, and then it could be removed (or replaced with a no-op, or just left there, etc. But in no case will it be called more or less than once.) Your implementation handles this manually by adding an extra storage item -- it's not fundamentally different than what The code isn't stored on chain. (all that is stored on chain is basically a map of
Yes, what I'd like to propose is that each migration is fully self sufficient in that it knows the old schema and the new schema (in neither case relying on types defined in the main pallet). This would allow the migration to live on long after a pallet has significantly changed, lending all the flexibility you could want out of that migration. |
What does it do?
Splits
Config::BondDuration
into 4 new constants for each delay constant:Config::LeaveCandidatesDelay
rounds (was implemented before but constant delay was calledConfig::BondDuration
)Config::RewardPaymentDelay
rounds (was implemented before but constant delay was calledConfig::BondDuration
)Config::RevokeNominationDelay
rounds (added in this in PR, before nominator revocations occurred as soon as the request succeeded)Config::LeaveNominatorsDelay
rounds (added in this PR, before nominator exits occurred as soon as the request succeeded)Storage Migration
The migration code is in
fn delay_nomination_exits_migration_execution
. It will only run once by design. When it executes, it will emit an event and put atrue
value in storage so that it does not execute again.Storage Value
ExitQueue
->ExitQueue2
The
ExitQueue
went from just handling candidate exits to also handling delayed nominator exits and revocations.Storage Map
NominatorState
->NominatorState2
When delaying execution of nominator revocation and nominator exits, we need to have local context when the nominator is leaving to prevent it from performing more nominations when it is already scheduled to leave.
From storage map
NominatorState
->NominatorState2
such that the keys (AccountId
) stay the same, but the value for the map changes fromstruct Nominator
->struct Nominator2
. There is an implementation ofFrom<Nominator> for Nominator2
which is what is used to migrate the storage values from the first map to the second.Weight Updates
The weights for
leave_nominators
andrevoke_nomination
is consistently lower than before. That's because they change from executing exits to scheduling exits. Scheduling execution of exits is cheaper than executing the exit itself.