-
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
total_deltas becoming negative #7
Comments
I think this is a real concern we'll need to resolve. Nice catch! |
I think the issue might be even worse than that, because we’re subtracting absolute slashed amounts from the validator total deltas. The validator total deltas are summed in each epoch, so we cannot tell what amount has been bonded and unbonded just from this field alone, so the absolute slashed amounts are effectively slashing the bonds (positive deltas), but not the unbonds (negative deltas), which doesn’t just lead to underflow but also wrong values when slashed bond is partially unbonded and when new bonds are added to be materialized at the same epoch as unbonds (submit a new bond unbonding_len - pipeline_len epochs after unbonding) With the example from the issue, say the slash rate is 50%, but instead of unbonding the full a1 , the validator unbonds just a half. Then what we end up with in the validator total deltas would amount to sum(a1, -a1/2, -a1/2) = 0 , but in fact, the validator should be left with a1/4 (half of the bond that wasn’t unbonded, slashed by 50%). We're gonna revisit this with new slashing design. |
I tried to reason through some ideas on how to fix this issue, but something always ends up being off :/ I think the problem is that when we calculate the slashed amount from validator total deltas for epoch e, we cannot simply apply it at e+pipeline, because some of the bonds that constitute the total deltas might have been unbonded by then. In the total_deltas field, the unbonded amounts are subtracted without any slashes applied, they only get slashed when withdrawing, because slashable events might not be discovered yet. This then causes the problem where we calculate slash from the total_deltas and we try to apply it in future total_deltas without knowing how much of it is to be unbonded. We want to avoid iterating bonds when slashing, but this seems to force us to do so, so I’m not sure if it can be solved without a change in the design. |
If we consider the stake of the validator at the epoch when the infraction was committed, and the stake of the validator at the current epoch, the difference of these two values will be the cumulative effect of any new delegations to the validator (adding stake) and any new unbondings from the validator (subtracting stake). The former should be unaffected by the slash; the latter is being double-counted for |
It would be great. Then I can add it to the model and eventually verify that works :) |
This sounds good! I think tracking total unbonded amounts per validator per epoch should work, because an evidence is only valid before the epoch |
This issue is still relevant in the context of cubic slashing. @tzemanovic fix suggestion:
end_of_epoch()
{
var set_validators = {val | val = slash.validator && slash in enqueued_slashes[cur_epoch]}
forall (validator_address in set_validators) do
//iterate over all slashes for infractions within (-1,+1) epochs range (Step 2.1 of cubic slashing)
var set_slashes = {s | s in enqueued_slashes[epoch] && cur_epoch-1 <= epoch <= cur_epoch+1 && s.validator == validator_address}
//calculate the slash rate (Step 2.2 of cubic slashing)
var rate = compute_final_rate(set_slashes)
forall (slash in {s | s in enqueued_slashes[cur_epoch] && s.validator == validator_address}) do
//set the slash on the now "finalised" slash amount in storage (Step 2.3 of cubic slashing)
slash.rate = rate
append(slashes[validator_address], slash)
var total_staked = read_epoched_field(validators[validator_address].total_deltas, slash.epoch, 0)
// CHANGES start from here
var offsets = {offset | 1 <= offset <= unbonding_length}
var total_unbonded = 0
//find the total unbonded from the slash epoch up to the current epoch first
forall (epoch in epochs | slash.epoch <= epoch <= cur_epoch) do
total_unbonded += read_epoched_field(validators[validator_address].total_unbonded, epoch, 0)
var last_slash = 0
forall (offset in offsets) do
total_unbonded += read_epoched_field(validators[validator_address].total_unbonded, cur_epoch + offset, 0)
var this_slash = (total_staked - total_unbonded) * slash.rate
var slashed_amount = this_slash - last_slash
last_slash = this_slash
update_total_deltas(validator_address, offset, -1*slashed_amount)
update_voting_power(validator_address, offset)
// CHANGES end here
//unfreeze the validator (Step 2.5 of cubic slashing)
validators[validator_address].frozen = false
cur_epoch = cur_epoch + 1
} |
I am a bit confused about your proposal. Why isn't the following enough? forall (slash in {s | s in enqueued_slashes[cur_epoch] && s.validator == validator_address}) do
//set the slash on the now "finalised" slash amount in storage (Step 2.3 of cubic slashing)
slash.rate = rate
append(slashes[validator_address], slash)
var total_staked = read_epoched_field(validators[validator_address].total_deltas, slash.epoch, 0)
var total_unbonded = 0
forall (epoch in slash.epoch+1..cur_epoch) do
total_unbonded += read_epoched_field(validators[validator_address].total_unbonded, epoch, 0)
var slashed_amount = (total_staked - total_unbonded) * slash.rate
//update voting power (Step 2.4 of cubic slashing)
update_total_deltas(validator_address, 1, -1*slashed_amount)
update_voting_power(validator_address, 1) Maybe the indexes are a bit off (I need to think about it), but I think it should work. First we compute how much stake has been unbonded since the validator misbehaved (from |
@angbrav this was my reasoning, consider:
|
Thanks for the clarification! |
@tzemanovic does the change to when we apply the voting power changes for unbonding (now at the pipeline offset) that we discussed last week change any of the logic which is necessary here? |
we'll need to check everything carefully, but it seems to me that the change (anoma/namada#366) shouldn't affect the logic for slashing in any way |
I've discovered the following. I think there is a missing piece in the fix proposed in #7 (comment). Consider the following execution:
Conclusion: the same way we carefully update I've already fixed the tla+ model, I'll soon comment the solution for the pseudocode model. |
@angbrav Thank you, the described issue makes sense and the proposed solution looks good to me! I think there’s however most likely another issue that is related to #27:
I think there's a risk of overriding/discarding previous slashes that may have already been applied at the same offsets, because in If I understand the issue in #27 correctly (in short - the slashes applied on
Additionally, I think it would be better if the slash rates for slashes processed in the same epoch are just their sums. To do this, we could group them by the validator addresses and sum the rates within the same epoch for each validator (again, this has to be consistent in both the |
I am not sure I am following. The function
Unless I misunderstood your concern or the simplification of assuming that both infractions happened at the same epoch precludes the problem, I do not see why what we have is not correct. I think #27 is a different issue: in the execution there, the second infraction happens when the first one has been processed, so the stake cached when the evidence is processed already accounts for the amount slashed by the first infraction. The problem there is that when withdrawing, we apply the slash to the unbonded amount, which is set from the bond. The bonds do not get updated when slashes are processed, so there is a mismatch there. |
@angbrav ah, that makes sense, thanks for clearing it up! In relation to #27, is it so that this implies the following?
If this holds, we can then match that logic to apply slashes when withdrawing, right? |
* alternative fix to 24 * delete commentted text and unused function * fix typo * change name variable * inf -> bottom * fix to latest issue of #25 * fix typo
@tzemanovic it does not hold exactly like that, but quite similar. I've opened a PR (#29) that includes a fix. Please check :) |
…ing windows (#28) * VotingpowerDelagations invariant * add comment * add a bunch of PoS invariants * fix new issue total_deltas * proper handling of misbehaving windows * fix typos * fix typos
It seems that this issue is fixed! Closing in. |
Potential problem with total_deltas
Following there is an execution that makes
total_deltas
of a validatorval
become negative. The VP code seems to disallow it:https://github.com/anoma/anoma/blob/master/proof_of_stake/src/validation.rs#L694-L698
validators[val].total_deltas[e+1-unbonding_length] = a1
val
decides to unbond a1 ate+1-unbonding_length
validators[val].total_deltas[e+1] = a1 - a1 = 0
evidence
is found for an actionval
did at epoche
slashed_amount = validators[val].total_deltas[e] * slash_rate = a1 * slash_rate
total_deltas
at the offset:validators[val].total_deltas[e+pipeline_length] = 0 - a1*slash_rate < 0
The text was updated successfully, but these errors were encountered: