-
Notifications
You must be signed in to change notification settings - Fork 0
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
Prevent overslashing and negative stakes #55
Conversation
41ae010
to
164e548
Compare
a19e8a3
to
d9af2d0
Compare
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 only thing that's unclear to me is the comment about capping in unbonding. I am integrating these changes to the Quint spec to see what happens. BTW, what about the slash pool? I think it did not make it into any PR.. it is fine like that, but we should not forget about it.
@@ -316,16 +322,24 @@ func withdraw(validator_address, delegator_address) | |||
|
|||
```go | |||
compute_amount_after_slashing(set_slashes, amount) { | |||
// First, group the slashes by epoch and a total rate |
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 to do 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.
Slashes within the same epoch will always be applied by summing their rates, so we can essntially turn all slashes into one effective slash per epoch. These lines also cap the slash rate per epoch at 1.0
|
||
// Ensure that the validator's stake does not go negative due to the slashing | ||
var pipeline_stake = read_epoched_field(validators[validator_address].total_deltas, cur_epoch + pipeline_length, 0) | ||
var token_change = min{ amount_after_slashing, pipeline_stake } |
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.
Can you think in a scenario in which this is needed? Intuitively I'd say we do not need it. We are computing how much from the bond is left after slashing and we are concerned about overslashing because the validator misbehaved at least twice with the same tokens. In those cases, amount_after_slashing
would be 0. But I do not know if I am missing some scenarios in which the validator is slashed all its stake at the end of an epoch, and then when applying the slashes to individual bonds here some of the bonds won't be fully slashed... that would be a case in which we would need to be careful at this point...
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 think you may be right in thinking this may not be necessary. It's possible I threw it in there when I was initially trying to prevent negative stake in lots of places. I've just tested the removal of this logic with our native state machine test and the test passed.
I'll remove this for now and remove it from the Namada code in a new PR.
…replace `slash.epoch` with `infraction_epoch`
d9af2d0
to
8b2b338
Compare
var sum_post_bonds = sum{bonds[validator_address].deltas[start] s.t. start > infraction_epoch && start <= cur_epoch + offset>} | ||
var validator_stake = read_epoched_field(validators[validator_address].total_deltas, cur_epoch + offset, 0) | ||
var slashable_stake = validator_stake - sum_post_bonds | ||
var change = min{-slashable_stake, diff_slashed_amount} |
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 part I am not sure about. I think we just need to compute how much from this_slash is left after applying any preceding slash. Could not figure out if what it is in here is equivalent, but we should avoid iterating over bonds at the end of an epoch.
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 keep thinking about this. I believe that to implement this correctly we need to be able to answer the following question: given an epoch e
and a validator val
, how much stake is left at an epoch e' > e
from the stake the validator had at e
. I haven't been able to figure out how to answer this question with the state variables that we have and without iterating over bonds/unbonds.
My guess is that your code tries to do something like that, but I am not sure it succeeds in doing so. My concern is related to unbonds that remove bonds but are only effective at the pipeline_length
offset.
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 proposing the following: keep a new variable total_bonded
for each validator which is a map from Epoch to int.
We update the variable when bonding as follows:
validators[validator_address].total_bonded[cur_epoch+pipeline_length] += amount
Given a offset
, we use this state variable as follows to substitute lines 564.
var recent_unbonds = 0
forall (let [bond_start, amount] of validators[validator_address].unbond_records[cur_epoch+offset] s.t. amount > 0 &&
bond_start > infraction_epoch) do
recent_unbonds += amount
sum_post_bonds = sum_post_bonds + (validators[validator_address].total_bonded[cur_epoch+offset] - recent_unbonds)
Note that sum_post_bonds
is always >= 0
(we are only interested in the bonds that were added after the infraction epoch and we subtract how much of those have been unbonded) and that sum_post_bonds
is computed iteratively within the for all loops. How does this look for you? It is similar to your code but it addresses to issues:
- Does not iterate over bonds
- I am not 100% sure, but I think your code has the issue of not accounting for unbonds properly.
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.
Does the commit 01a5028 satisfy everything you had in mind 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.
I think we should start computing sum_post_bonds
in the previous forall
: the one that goes from slash.epoch+1
to cur_epoch
. Otherwise, we would be possibly missing a bunch of bonds.
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.
Tried to put this in pseudocode quickly here: ac99310
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.
Tried to put this in pseudocode quickly here: ac99310
That may not work. Let's say you bond x
tokens at epoch e
and unbond those tokens at epoch e+2
and you are computing sum_post_bonds
at epoch e
. Then the stake at e
includes those x
tokens but your global_bonds[validator_address].deltas[start]
won't: you subtract them when unbonding. This looks incorrect to me.
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.
Sorry, I think I need more context for your example here... if I'm computing sum_post_bonds
at epoch e
in my version of things, then there is no way that an unbond of tokens can remove stake at e+2
because the validator will be frozen already by epoch e
when the unbond tx is submitted (only computing sum_post_bonds
for future epochs relative to the current).
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.
- Assume that
unbonding_length=4
,pipeline_length=3
andcubic_offset=1
- A validator misbehaves at
e
. The infraction is submitted ate+unbonding_length=e+4
and processed at the end ofe+unbonding_length+cubic_offset=e+5
- A user delegates
x
tokens to the validator ate+1
and unbonds them ate+unbonding_length=e+4
(before the infraction is submitted). - When processing the infraction at the end of
e+5
, we would computesum_post_bonds
fore+6
(cur_epoch+1
). By that epoch, the unbond has not materialized (it materializes ate+4+pipeline_length=e+7)
, butglobal_bonds[e+1]
accounts for it.
I know those are not typical values for unbonding_length
and pipeline_length
, but I'd rather not have the correctness of the design depend on the value of these parameters.
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.
Thx for this example. I agree that your proposed changes do indeed solve a problem that currently exists. I wrote up a comparison of the two approaches that demonstrates where the current one fails and where the proposed one succeeds: https://hackmd.io/yRsLbn5IT8C2xkj8csSZMg?both
01a5028
to
65f7335
Compare
65f7335
to
16d4268
Compare
3a0e1ff
to
85ff661
Compare
Looks good, merged |
Closes #46.
The changes in this PR were initiated by the need to adjust the pseudocode to reflect changes in the Namada implementation that prevent the validator's deltas from going negative with the existence of more arbitrary slashing conditions.