-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Runtime Task Executor + Example for staking slashing spans #8197
Conversation
You mean the lazy storage removal? From a glance over this code I think I am doing the same thing as you are doing here. Just in a less generalized way (queuing tasks in storage and working on them in This would allow us to have a global queue of tasks. It would solve the problem of coming up with proper weight limits for each pallet's |
yeah exactly, I thought maybe we can try this in the contracts pallet as well to see if it is any good or not. As you said, I think they should fit nicely as they are doing the same thing, this one is just trying to be reusable. |
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.
Not sure if my comments are useful because it is still WIP.
frame/staking/src/scheduler.rs
Outdated
|
||
/// Add a new task to the internal queue. | ||
pub fn add_task(&mut self, task: Task) { | ||
self.tasks.push(task) |
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.
We would need to cap the size of the queue because the weight to decode the Vec
is linear to the size of the queue. Also we should make use of append
here to have O(1)
costs when pusing items to storage.
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.
Yeah so in the new API, ideally we need both append
and decode_len
in StoredExecutor
.
Although, this situation exists because I want to abstract the task management from the pallet. As you see now, as a pallet you just accept a type E: StoredExecutor
, put it in storage and mutate it whenever you want to .execute
. This is a okay-ish abstraction (compared to you manually being in charge of putting a Vec<Task>
in storage), but we kinda miss the benefit of decode_len
and append
.
but I am sure we can build these abstractions, if the whole idea of having StoredExecutor
seems like a good way to go.
@@ -1721,16 +1824,16 @@ decl_module! { | |||
ensure!(!targets.is_empty(), Error::<T>::EmptyTargets); | |||
ensure!(targets.len() <= MAX_NOMINATIONS, Error::<T>::TooManyTargets); | |||
|
|||
let old = Nominators::<T>::get(stash).map_or_else(Vec::new, |x| x.targets); | |||
let old = Nominators::<T>::get(stash).map_or_else(Vec::new, |x| x.targets.into_iter().map(|(x, _)| x).collect::<Vec<_>>()); | |||
|
|||
let targets = targets.into_iter() | |||
.map(|t| T::Lookup::lookup(t).map_err(DispatchError::from)) | |||
.map(|n| n.and_then(|n| if old.contains(&n) || !Validators::<T>::get(&n).blocked { |
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.
same note about allocations here
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.
Not sure how it can be avoided here?
This is now ready for review. I'd be happy to either try myself, or if anyone's interested, explore both #7911 paritytech/polkadot-sdk#465 using this abstraction. You'll see that it is a bit over-engineered just for the purpose of slashing tasks, and the reason is that I envision building other multi-block stuff with it. |
Actually, I can pretty much see the possibility of creating a new pallet that has just a storage task executor, and creating trait interfaces so that for example staking would only delegate its own tasks to the |
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 am worried by the fact that the StoredExecutor
declares its own storage related weights out of scope. This means that accounting for the weight that results from encoding and decoding the potentially unbounded task queue is solely upon the user of this feature. I am not even sure if it would be possible for the user to do so.
The dependent introduced in this PR (staking) already disregards those weights (see my other comments). I can't tell if this is a problem but I wager that it would profit if the executor would offer an abstraction to deal with it.
I am not sure if this feature should be changed to take coding weight into account or a higher level abstraction can be build onto it that handles it. This would also need to include storage append support.
I won't oppose the merge as you surely have a purpose for it. However, I cannot use it in contracts for the reasons stated.
frame/support/src/executor.rs
Outdated
use crate::{weights::Weight, traits::Get}; | ||
use codec::{Encode, Decode}; | ||
use sp_runtime::traits::Zero; | ||
use crate::{RuntimeDebugNoBound, PartialEqNoBound, EqNoBound, CloneNoBound}; |
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 would prefer to merge those crate::
imports.
frame/staking/src/lib.rs
Outdated
// if this is a non-zero slash, schedule tasks to chill their nominations. | ||
if !slash_fraction.is_zero() { | ||
let task = SlashTask::new(stash.clone()); | ||
<SlashTaskExecutor<T>>::mutate(|e| e.add_task(task)); |
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.
The add_task
complexity is linear to the amount of already existing tasks in storage because of decoding and encoding of all tasks by mutate. Are the number of queued slashes capped and benchmarked for the worst case?
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.
Agreed.
frame/staking/src/lib.rs
Outdated
|
||
let task_weight = <SlashTaskExecutor<T>>::mutate(|e| e.execute()); | ||
// The additional weight of reading the tasks, and writing back the result. | ||
add_weight(1, 1, task_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.
This does not only add the storage accesses but also decoding and encoding weight proportional to the amount of queued tasks.
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.
Indeed, this is one of the things that need to be fixed. Not sure where, but there's a TODO for it.
frame/support/src/executor.rs
Outdated
pub trait RuntimeTask: | ||
Sized + Clone + Default + Encode + Decode + PartialEq + Eq + sp_std::fmt::Debug |
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.
Why do we need Default
?
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 believe everything going to storage needs Default
.
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.
But the RuntimeTask
does not go into storage. The StoredExecutor
does which does not need its task to implement Default
in order to implement it.
/// Return a vector of all tasks. | ||
#[cfg(any(test, feature = "std"))] | ||
fn tasks(&self) -> Vec<Self::Task>; | ||
// TODO: providing an iter() might also be 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.
I would really like to have those as a github issue rather than a TODO comment. But this is a personal preference. Feel free to ignore.
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.
A PR shall never be merged with TODOs, these are notes that I want my reviewers to also see and be aware that I maybe intend to do them.
Co-authored-by: Alexander Theißen <[email protected]>
Co-authored-by: Alexander Theißen <[email protected]>
I totally agree with this, as you already mentioned it in your previous round. Honestly, I haven't addressed it yet because I am sure it is solvable and it is not the core of the work. An executor that has a bounded queue, IMO, can initially be deployed even with this code as-is, since the amount of work that the executor is doing under the hood + linear decoding is simply negligible. So we have two issues to fix here:
I see the second one being more important to fix.
I think what contracts is doing is a good use case and rather bend to your requirements, as long as they are reasonable. If your only grumbles are the two points above, I think they are easily fixable. |
shush bot, this is important work, and needs to stay around :D |
Hey, is anyone still working on this? Due to the inactivity this issue has been automatically marked as stale. It will be closed if no further activity occurs. Thank you for your contributions. |
Could fix https://github.com/paritytech/srlabs_findings/issues/57
Fixes #6835
Fixes #5188
Will also lay the foundation of #7911 and paritytech/polkadot-sdk#465 via the scheduler.
The gist of it is that: From now on each vote has an
enabled
flag, and we flip that upon slash. This means for each slash event we need to iterate all nominators, which is why I made the scheduler.@shawntabrizi pointed out an interesting idea that we could outsource this operation: If we track the total voters of each validator, we can have someone submit the list of nominators that need to be iterated, potentially saving a lot of time.
I am okay with that as well, in which case we need to decouple this PR. But note that even then, iterating a few hundred keys might be something that we want to split among blocks. Overall, the scheduler is a personal interest of mine, and I want to experiemnt with it anyhow.
Also, I belive perhaps the work of @athei for contracts can be rewritten using a similar approach as what we do here.