Skip to content
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

x/slashing/keeper: Keeper.Slash should return immediately if tokensToBurn.IsZero #18034

Closed
1 task done
odeke-em opened this issue Oct 10, 2023 · 0 comments · Fixed by #18049
Closed
1 task done

x/slashing/keeper: Keeper.Slash should return immediately if tokensToBurn.IsZero #18034

odeke-em opened this issue Oct 10, 2023 · 0 comments · Fixed by #18049
Labels
S:orijtech Squad: OrijTech T:Bug T: Performance Performance improvements

Comments

@odeke-em
Copy link
Collaborator

odeke-em commented Oct 10, 2023

Is there an existing issue for this?

  • I have searched the existing issues

What happened?

Noticed while auditing and testing that this code

// cannot decrease balance below zero
tokensToBurn := math.MinInt(remainingSlashAmount, validator.Tokens)
tokensToBurn = math.MaxInt(tokensToBurn, math.ZeroInt()) // defensive.
// we need to calculate the *effective* slash fraction for distribution
if validator.Tokens.IsPositive() {
effectiveFraction := math.LegacyNewDecFromInt(tokensToBurn).QuoRoundUp(math.LegacyNewDecFromInt(validator.Tokens))
// possible if power has changed
if effectiveFraction.GT(math.LegacyOneDec()) {
effectiveFraction = math.LegacyOneDec()
}
// call the before-slashed hook
if err := k.Hooks().BeforeValidatorSlashed(ctx, operatorAddress, effectiveFraction); err != nil {
k.Logger(ctx).Error("failed to call before validator slashed hook", "error", err)
}
}
// Deduct from validator's bonded tokens and update the validator.
// Burn the slashed tokens from the pool account and decrease the total supply.
validator, err = k.RemoveValidatorTokens(ctx, validator, tokensToBurn)
if err != nil {
return math.NewInt(0), err
}

if the value of tokensToBurn is zero, the code down below takes an unnecessary path
but in there:

  • firstly tokensToBurn will be zero hence all unnecessary multiplications in
    effectiveFraction := math.LegacyNewDecFromInt(tokensToBurn).QuoRoundUp(math.LegacyNewDecFromInt(validator.Tokens))
    // possible if power has changed
    if effectiveFraction.GT(math.LegacyOneDec()) {
    effectiveFraction = math.LegacyOneDec()
    }
    // call the before-slashed hook
    if err := k.Hooks().BeforeValidatorSlashed(ctx, operatorAddress, effectiveFraction); err != nil {
    k.Logger(ctx).Error("failed to call before validator slashed hook", "error", err)
  • invoking .BeforeSlashedValidator will be futile as effectiveFraction=0
  • invoking k.RemoveValidatorTokens() with a tokensToRemove=0 performs unnecessary work and ends up with .Sub(0), erroneous removals as well in
    if err := k.DeleteValidatorByPowerIndex(ctx, validator); err != nil {
    return validator, err
    }
    which could actually be a bug in which I can trigger removal of a validator that has not yet been delegated tokens (debatable though)

Suggestion

diff --git a/x/staking/keeper/slash.go b/x/staking/keeper/slash.go
index 5a0886141f..e61cbb5181 100644
--- a/x/staking/keeper/slash.go
+++ b/x/staking/keeper/slash.go
@@ -147,6 +147,18 @@ func (k Keeper) Slash(ctx context.Context, consAddr sdk.Con
sAddress, infractionH
        tokensToBurn := math.MinInt(remainingSlashAmount, validator.Tokens)
        tokensToBurn = math.MaxInt(tokensToBurn, math.ZeroInt()) // defensive.
 
+       if tokensToBurn.IsZero() {
+               // Nothing to burn, we can end this route immediately.
+               logger.Info(
+                       "no validator slashing because slash amount is zero",
+                       "validator", validator.GetOperator(),
+                       "slash_factor", slashFactor.String(),
+                       "burned", tokensToBurn,
+                       "validatorTokens", validator.Tokens,
+               )
+               return math.NewInt(0), nil
+       }
+

/cc @elias-orijtech

Cosmos SDK Version

main

How to reproduce?

No response

@odeke-em odeke-em added T:Bug T: Performance Performance improvements S:orijtech Squad: OrijTech labels Oct 10, 2023
odeke-em added a commit that referenced this issue Oct 10, 2023
This change makes Keeper.Slash return early if there are no tokens
to burn! This change also skips invoking the
.Hooks().BeforeValidatorSlashed hook because literally there is no
slashing that can happen if there are no tokens to burn.

Given that the result of burning zero tokens SHOULD be a no-operation
(noop) but we go through the whole routine, assuming no zero tokens
would be burnt, one could argue on a protocol implementation/conformation
that with zero tokens to burn, invoking Keeper.RemoveValidatorTokens which
invokes:

  Keeper.DeleteValidatorByPowerIndex
    -> (Keeper.DeleteValidatorByPowerIndex)
    -> validator.RemoveTokens
    -> Keeper.SetValidator
    -> Keeper.SetValidatorByPowerIndex

was causing a potential self inflicted Byzantine risk because that operation
is non-atomic (it ran through a series of operations that could fail on
their own yet could not be rolled back and not idempotent), will rely
on network operations yet the actual result will have the operations back
to the original: more investigation is needed to examine the risk/impact
of previously letting zero tokens be burnt and run through that process.

Also while here, employed a Go pattern to reuse condition variables just
as a code hygiene improvement and also given that as a Go reviewer, the
unnecessary allocation of variables however small must be avoided.

Fixes #18034
odeke-em added a commit that referenced this issue Oct 16, 2023
This change makes Keeper.Slash return early if there are no tokens
to burn! This change also skips invoking the
.Hooks().BeforeValidatorSlashed hook because literally there is no
slashing that can happen if there are no tokens to burn.

Given that the result of burning zero tokens SHOULD be a no-operation
(noop) but we go through the whole routine, assuming no zero tokens
would be burnt, one could argue on a protocol implementation/conformation
that with zero tokens to burn, invoking Keeper.RemoveValidatorTokens which
invokes:

  Keeper.DeleteValidatorByPowerIndex
    -> (Keeper.DeleteValidatorByPowerIndex)
    -> validator.RemoveTokens
    -> Keeper.SetValidator
    -> Keeper.SetValidatorByPowerIndex

was causing a potential self inflicted Byzantine risk because that operation
is non-atomic (it ran through a series of operations that could fail on
their own yet could not be rolled back and not idempotent), will rely
on network operations yet the actual result will have the operations back
to the original: more investigation is needed to examine the risk/impact
of previously letting zero tokens be burnt and run through that process.

Also while here, employed a Go pattern to reuse condition variables just
as a code hygiene improvement and also given that as a Go reviewer, the
unnecessary allocation of variables however small must be avoided.

Fixes #18034
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S:orijtech Squad: OrijTech T:Bug T: Performance Performance improvements
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant