-
Notifications
You must be signed in to change notification settings - Fork 1k
[AHM/Staking/VMP] Paginated Offence Reports + Retries for Validator Set #9619
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
Conversation
log::error!(target: "runtime::staking-async::rc-client", "📨 Failed to split message {}: {:?}", message_type_name, e); | ||
})?; | ||
|
||
match with_transaction_opaque_err(|| { |
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.
@franciscoaguirre @bkontur I don't expect us to need this mechanism anymore, but as far as I can say this code should still be correct. Can you confirm?
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.
Haven't gone through the whole thing yet.
/// * a too low of a value is assigned to [`Config::MaximumValidatorsWithPoints`] | ||
/// * Those who are calling into our `RewardsReporter` likely have a bad view of the | ||
/// validator set, and are spamming us. | ||
ValidatorPointDropped, |
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.
Might be better to send the message in chunks instead of dropping points, or? If this issue can really happen (points being set on non-validator accounts), then in theory all real validator points could be dropped while only the spammed ones are sent.
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.
About splitting, I learned a few new things:
- All of our messages are small enough to fit in the limits of a single-message.
- If they don't fit in the limits of the entire queue, I decided to not send them and wait until the queue can accept it. Because otherwise, we would have to buffer parts of a message that was not sent, and deal with that, which I suspect opens more cans of worms than helping.
- Note that we still have machinery to both
split
all messages, and combine them, but for now I have removed them from the main code path.
(PTAL at this as well for more context)
Co-authored-by: Ankan <[email protected]>
Co-authored-by: Ankan <[email protected]>
Still have to review the whole PR but since I have noticed... nit and out of scope: @kianenigma , since you are changing papi-test's cmd.ts please replace |
OutgoingValidatorSet::<T>::put((report, new_retries_left)) | ||
} else { | ||
Self::deposit_event(Event::<T>::Unexpected( | ||
UnexpectedKind::ValidatorSetDropped, |
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.
If this happens, we need to either kick off a new election (or decrement current era), right? Otherwise we will not do a re-election again?
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.
Correct, we will get stuck, and for now in favor of time I assume we will do the recovery manually with openGov.
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.
- what would be the long term solution? Chunking validator sets and send a chunk only if it fits in message limits?
- assuming we recovery manually with openGov for the time being, would it make still sense to save to storage the validator set we failed to send instead of completely dropping it in something like
DroppedValidatorSet
or similar, and add a recovery extrinsic for openGov ?
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.
ok, dropping 1. as per #9619 (comment), we eventually will adopt a chunking-over-blocks in the future.
Keeping 2. around to 👂 from you what you think about it
Self::deposit_event(Event::<T>::Unexpected( | ||
UnexpectedKind::ValidatorSetSendFailed, | ||
)); | ||
if let Some(new_retries_left) = retries_left.checked_sub(One::one()) { |
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.
When would this fail? Do we need some backoff strategy as well?
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 size of a single message can never cause this to fail. Only if an attacker is filling the UMP queue. The retry mechanism, combined with exponential fee increase in the UMP, are meant to be the defence.
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.
Symbolic approve. Critical things that I think still needs to be addressed:
- Retry mechanism for session report (RC to AH).
- Ensuring validator election can resume normally If validator set is dropped on AH (while sending to RC).
OutgoingValidatorSet::<T>::put((report, new_retries_left)) | ||
} else { | ||
Self::deposit_event(Event::<T>::Unexpected( | ||
UnexpectedKind::ValidatorSetDropped, |
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.
- what would be the long term solution? Chunking validator sets and send a chunk only if it fits in message limits?
- assuming we recovery manually with openGov for the time being, would it make still sense to save to storage the validator set we failed to send instead of completely dropping it in something like
DroppedValidatorSet
or similar, and add a recovery extrinsic for openGov ?
weight = weight.saturating_add(processing_weight); | ||
// then, take a page from our send queue, and if present, send it. | ||
weight.saturating_accrue(T::DbWeight::get().reads(2)); | ||
OffenceSendQueue::<T>::get_and_maybe_delete(|page| { |
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.
Being OffenceSendQueue unbounded (only page size is via MaxOffenceBatchSize), it can grow infinitely, aren't we at risk of potentially increasing storage indefinitely in case of attack scenario and/or network congestion? I understand we can't drop offences so we would need a mechanism (out of scope of this PR) to somehow slow down offence reporting in consensus code to be on the safe side or did I get it wrong?
Maybe on staking side, we should add some kind of monitoring / event when some threshold is exceeded.
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.
Your intuition is right, this is a band-aid and a useful mechanism to begin with, but eventually we exactly need some way to deduplicate offences, or lower their quantity somehow, before this code path is reached.
This band-aid will just remove the risk of an attacker being able to cause offences to drop for chaep.
For some mad scenario where a bug is causing perpetual offences for everyone forever, it won't help of course.
return weight; | ||
// if we have any pending session reports, send it. | ||
weight.saturating_accrue(T::DbWeight::get().reads(1)); | ||
if let Some((session_report, retries_left)) = OutgoingSessionReport::<T>::take() { |
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 are always prioritizing session report over offence report in terms of block weight - which is probably fine since it happens much more rarely. Ideally we should support chunking for session reports and somehow split weight budget between session report (if present) and offences (if present) here to prevent some offence starving
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 weight is not divided. Everything in on_initialize
is mandatory1 and will execute. It is just that since we enqueue the session report first, we are giving it more priority in consuming any of the DMP resource limits (more info in the doc).
- But, since session report gets a finite number of retries
- and offences get basically infinite retries
- and as you said session report is systematically a rare event
I think it is a better choice to prioritize it.
Footnotes
-
A bit of an archaic topic in Substrate, and probably not well understood or documented except in PBA. Checkout
DispatchClass
, and all places where we useDispatchClass::Mandatory
. These are all mandatory hooks that will MUST always happen. And therefore are also kinda dangerous as well. Another improvement I want to do for Polkadot AHM is to move as much as we can toon_poll
, which is exactly the same, but is weight aware and skip if the block is full. ↩
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.
Definitely a strong +1 for migrating to on_poll()
when the time is right (and thanks for the extra explanation around DispatchClass::Mandatory and on_initialize vs on_poll, TIL 🙇)
// if we have any pending session reports, send it. | ||
weight.saturating_accrue(T::DbWeight::get().reads(1)); | ||
if let Some((session_report, retries_left)) = OutgoingSessionReport::<T>::take() { | ||
match T::SendToAssetHub::relay_session_report(session_report.clone()) { |
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.
Would it make sense to implement both here and on rc-client a sort of size checking vs max UMP/DMP message size? E.g. from the risk document we know that for Kusama / Polkadot we have UMP message limit of 64kb and DM 50kb. We could try to check before sending and avoid to send if we are close to the risk threshold. It would play well in the future once/if we introduce chunking.
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.
Oh great point, let me explain:
I envisioned the need for this actually quite early, and this is why SessionReport
and ValidatorSet
both had a leftover: bool
field from the get go. There is also a XCMSeder::split_then_send
that mostly does what you suggest. Moreover, #8409 came to make sure such errors are reported upwards.
But, now that I have revisited the topic, I have more info:
- We know that the size of all of our messages is small enough to fit the single-message-size-limit. Only a mistaken change in the configuration involved might break this. See
mod message_queue_sizes
. - So the real limit we have is the whole-queue-limits. For that, I realized that splitting a message and sending it all at once won't actually help. We have to chunk the message, send it over blocks.
- This is why I no longer use
XCMSender::split_then_send
, and instead only use a singletonXCMSender::send
- I have also changed
split_then_send
such that it makes sure all chunks can be sent, and if revert all, in case it is used.
TLDR: single message size is not a problem. Whole queue is the issue, and chunk-and-send-all won't help with it. A chunk-and-send-over-many-blocks is better, but also more complicated than the approach here which retries the whole message. This can be a future improvement.
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.
Great explanation, thank you!
Nitpick: why not removing split_then_send
instead of keeping as deprecated?
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.
Great stuff! I would be in favor of having a better recovery mechanism for ValidatorSet. One relatively low hanging fruit is suggested in the review comments (saving to storage something like DeprecatedValidatorSet + ad-hoc extrinsic for governance). Maybe we could do even better automatically.
Not a blocker though
All GitHub workflows were cancelled due to failure one of the required jobs. |
Created backport PR for
Please cherry-pick the changes locally and resolve any conflicts. git fetch origin backport-9619-to-unstable2507
git worktree add --checkout .worktree/backport-9619-to-unstable2507 backport-9619-to-unstable2507
cd .worktree/backport-9619-to-unstable2507
git reset --hard HEAD^
git cherry-pick -x f7b0396e3b7f826166cb5acc4a4248307af6d708
git push --force-with-lease |
…et (#9619) * Please see the full design do [here](https://docs.google.com/document/d/1l2COWct1f-gC8nM0tq7Xs8pBWeAP6pX0LociWC6enUg/edit?tab=t.0) * closes https://github.com/paritytech-secops/srlabs_findings/issues/520 This PR makes the following changes: #### Common * `SendToRelayChain` and `SendToAssetHub` traits now return a result, allowing the caller to know if the underlying XCM was sent or not. * Adds a number of testing facilities to `pallet-root-offences`, and `staking-async/papi-tests`. Both of which can be ignored in the review. #### Offences * `SendToAssetHub::relay_new_offence` is removed. Instead, we use the new `relay_new_offence_paged` which is a vector of self-contained offences, not requiring us to group offences per session in each message. * Offences are not sent immediately anymore. * Instead, they are stored in a paginated `OffenceSendQueue`. * `on-init`, we grab one page of this storage map, and sent it. #### Session Report * Session reports now also have a retry mechanism. * Upon each failure, we emit an `UnexpectedEvent` * If our retries run out and we still can't send the session report, we will emit a different `UnexpectedEvent`. We also retore the validator points that we meant to send, and merge them back, so that they are sent in the next session report. #### Validator Set * Similar to offences, they are not sent immediately anymore. * Instead, they are stored in a storage item, and are sent on subsequent on-inits. * A maximum retry count is added. ### Review notes As noted above, ignore all changes in * `staking-async/runtimes` * `staking-async/runtimes/papi-tests` * `root-offences` As they are only related to testing. --------- Co-authored-by: Ankan <[email protected]> Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com> (cherry picked from commit f7b0396)
Successfully created backport PR for |
Backport #9619 into `stable2509` from kianenigma. See the [documentation](https://github.com/paritytech/polkadot-sdk/blob/master/docs/BACKPORT.md) on how to use this bot. NOTE: this PR introduces **major** changes in the staking-async pallet, needed to address critical issues related to Staking vs VMP. They are needed to improve robustness and resilience of the staking machinery (paginated offences, retry mechanism for session report and validator set), this is why we are backporting it. <!-- # To be used by other automation, do not modify: original-pr-number: #${pull_number} --> Co-authored-by: Kian Paimani <[email protected]> Co-authored-by: Ankan <[email protected]> Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…et (#9619) * Please see the full design do [here](https://docs.google.com/document/d/1l2COWct1f-gC8nM0tq7Xs8pBWeAP6pX0LociWC6enUg/edit?tab=t.0) * closes paritytech-secops/srlabs_findings#520 This PR makes the following changes: * `SendToRelayChain` and `SendToAssetHub` traits now return a result, allowing the caller to know if the underlying XCM was sent or not. * Adds a number of testing facilities to `pallet-root-offences`, and `staking-async/papi-tests`. Both of which can be ignored in the review. * `SendToAssetHub::relay_new_offence` is removed. Instead, we use the new `relay_new_offence_paged` which is a vector of self-contained offences, not requiring us to group offences per session in each message. * Offences are not sent immediately anymore. * Instead, they are stored in a paginated `OffenceSendQueue`. * `on-init`, we grab one page of this storage map, and sent it. * Session reports now also have a retry mechanism. * Upon each failure, we emit an `UnexpectedEvent` * If our retries run out and we still can't send the session report, we will emit a different `UnexpectedEvent`. We also retore the validator points that we meant to send, and merge them back, so that they are sent in the next session report. * Similar to offences, they are not sent immediately anymore. * Instead, they are stored in a storage item, and are sent on subsequent on-inits. * A maximum retry count is added. As noted above, ignore all changes in * `staking-async/runtimes` * `staking-async/runtimes/papi-tests` * `root-offences` As they are only related to testing. --------- Co-authored-by: Ankan <[email protected]> Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com> (cherry picked from commit f7b0396)
This PR makes the following changes:
Common
SendToRelayChain
andSendToAssetHub
traits now return a result, allowing the caller to know if the underlying XCM was sent or not.pallet-root-offences
, andstaking-async/papi-tests
. Both of which can be ignored in the review.Offences
SendToAssetHub::relay_new_offence
is removed. Instead, we use the newrelay_new_offence_paged
which is a vector of self-contained offences, not requiring us to group offences per session in each message.OffenceSendQueue
.on-init
, we grab one page of this storage map, and sent it.Session Report
UnexpectedEvent
UnexpectedEvent
. We also retore the validator points that we meant to send, and merge them back, so that they are sent in the next session report.Validator Set
Review notes
As noted above, ignore all changes in
staking-async/runtimes
staking-async/runtimes/papi-tests
root-offences
As they are only related to testing.