Skip to content

Commit

Permalink
fix: CNS-delegators-slashing-hook-fix (#1356)
Browse files Browse the repository at this point in the history
* stop using the hook and instead run on beginblock

* cr changes

* lint

---------

Co-authored-by: Yarom Swisa <[email protected] git config --global user.name Yarom>
Co-authored-by: Elad Gildnur <[email protected]>
Co-authored-by: Yaroms <[email protected]>
  • Loading branch information
4 people authored Apr 17, 2024
1 parent 7c9c530 commit 976bbd1
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 84 deletions.
2 changes: 1 addition & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -755,14 +755,14 @@ func New(
crisistypes.ModuleName,
genutiltypes.ModuleName,
evidencetypes.ModuleName,
dualstakingmoduletypes.ModuleName,
ibctransfertypes.ModuleName,
ibcexported.ModuleName,
group.ModuleName,
authz.ModuleName,
icatypes.ModuleName,
specmoduletypes.ModuleName,
epochstoragemoduletypes.ModuleName,
dualstakingmoduletypes.ModuleName,
subscriptionmoduletypes.ModuleName,
conflictmoduletypes.ModuleName, // conflict needs to change state before pairing changes stakes
downtimemoduletypes.ModuleName, // downtime needs to run before pairing
Expand Down
5 changes: 5 additions & 0 deletions testutil/common/tester.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"time"

"cosmossdk.io/math"
abci "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
Expand Down Expand Up @@ -206,6 +207,10 @@ func (ts *Tester) SlashValidator(valAcc sigs.Account, fraction math.LegacyDec, p
valConsAddr := sdk.GetConsAddress(valAcc.PubKey)
ts.Keepers.SlashingKeeper.Slash(ts.Ctx, valConsAddr, fraction, power, ts.Ctx.BlockHeight())

var req abci.RequestBeginBlock
req.ByzantineValidators = []abci.Misbehavior{{Type: abci.MisbehaviorType_DUPLICATE_VOTE, Validator: abci.Validator{Address: valConsAddr}}}
ts.Keepers.Dualstaking.BeginBlock(ts.Ctx, req)

// calculate expected burned tokens
consensusPowerTokens := ts.Keepers.StakingKeeper.TokensFromConsensusPower(ts.Ctx, power)
return fraction.MulInt(consensusPowerTokens).TruncateInt()
Expand Down
9 changes: 9 additions & 0 deletions testutil/keeper/keepers_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"cosmossdk.io/math"
tmdb "github.com/cometbft/cometbft-db"
abci "github.com/cometbft/cometbft/abci/types"
"github.com/cometbft/cometbft/libs/log"
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
"github.com/cometbft/cometbft/rpc/core"
Expand Down Expand Up @@ -112,6 +113,10 @@ type Servers struct {
DistributionServer distributiontypes.MsgServer
}

type KeeperBeginBlockerWithRequest interface {
BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock)
}

type KeeperBeginBlocker interface {
BeginBlock(ctx sdk.Context)
}
Expand Down Expand Up @@ -490,6 +495,10 @@ func NewBlock(ctx sdk.Context, ks *Keepers) {
if beginBlocker, ok := fieldValue.Interface().(KeeperBeginBlocker); ok {
beginBlocker.BeginBlock(ctx)
}

if beginBlocker, ok := fieldValue.Interface().(KeeperBeginBlockerWithRequest); ok {
beginBlocker.BeginBlock(ctx, abci.RequestBeginBlock{})
}
}
}

Expand Down
46 changes: 46 additions & 0 deletions x/dualstaking/keeper/balance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package keeper

import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/lavanet/lava/utils"
"github.com/lavanet/lava/x/dualstaking/types"
)

func (k Keeper) BalanceDelegator(ctx sdk.Context, delegator sdk.AccAddress) (int, error) {
diff, providers, err := k.VerifyDelegatorBalance(ctx, delegator)
if err != nil {
return providers, err
}

// if diff is zero, do nothing, this is a redelegate
if diff.IsZero() {
return providers, nil
} else if diff.IsPositive() {
// less provider delegations,a delegation operation was done, delegate to empty provider
err = k.delegate(ctx, delegator.String(), types.EMPTY_PROVIDER, types.EMPTY_PROVIDER_CHAINID,
sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), diff))
if err != nil {
return providers, err
}
} else if diff.IsNegative() {
// more provider delegation, unbond operation was done, unbond from providers
err = k.UnbondUniformProviders(ctx, delegator.String(), sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), diff.Neg()))
if err != nil {
return providers, err
}
}

diff, _, err = k.VerifyDelegatorBalance(ctx, delegator)
if err != nil {
return providers, err
}
// now it needs to be zero
if !diff.IsZero() {
return providers, utils.LavaFormatError("validator and provider balances are not balanced", nil,
utils.Attribute{Key: "delegator", Value: delegator.String()},
utils.Attribute{Key: "diff", Value: diff.String()},
)
}

return providers, nil
}
82 changes: 2 additions & 80 deletions x/dualstaking/keeper/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@ package keeper
import (
"fmt"

"cosmossdk.io/math"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
commontypes "github.com/lavanet/lava/common/types"
"github.com/lavanet/lava/utils"
"github.com/lavanet/lava/x/dualstaking/types"
"golang.org/x/exp/slices"
)

// Wrapper struct
Expand Down Expand Up @@ -65,87 +62,12 @@ func (h Hooks) AfterDelegationModified(ctx sdk.Context, delAddr sdk.AccAddress,
return nil
}

var diff math.Int
var err error
diff, providers, err = h.k.VerifyDelegatorBalance(ctx, delAddr)
if err != nil {
return err
}

// if diff is zero, do nothing, this is a redelegate
if diff.IsZero() {
return nil
} else if diff.IsPositive() {
// less provider delegations,a delegation operation was done, delegate to empty provider
err = h.k.delegate(ctx, delAddr.String(), types.EMPTY_PROVIDER, types.EMPTY_PROVIDER_CHAINID,
sdk.NewCoin(h.k.stakingKeeper.BondDenom(ctx), diff))
if err != nil {
return err
}
} else if diff.IsNegative() {
// more provider delegation, unbond operation was done, unbond from providers
err = h.k.UnbondUniformProviders(ctx, delAddr.String(), sdk.NewCoin(h.k.stakingKeeper.BondDenom(ctx), diff.Neg()))
if err != nil {
return err
}
}

diff, _, err = h.k.VerifyDelegatorBalance(ctx, delAddr)
if err != nil {
return err
}
// now it needs to be zero
if !diff.IsZero() {
return utils.LavaFormatError("validator and provider balances are not balanced", nil,
utils.Attribute{Key: "delegator", Value: delAddr.String()},
utils.Attribute{Key: "diff", Value: diff.String()},
)
}
return nil
providers, err = h.k.BalanceDelegator(ctx, delAddr)
return err
}

// BeforeValidatorSlashed hook unbonds funds from providers so the providers-validators delegations balance will preserve
func (h Hooks) BeforeValidatorSlashed(ctx sdk.Context, valAddr sdk.ValAddress, fraction sdk.Dec) error {
val, found := h.k.stakingKeeper.GetValidator(ctx, valAddr)
if !found {
return utils.LavaFormatError("slash hook failed", fmt.Errorf("validator not found"),
utils.Attribute{Key: "validator_address", Value: valAddr.String()},
)
}

// unbond from providers according to slash
// sort the delegations from lowest to highest so if there's a remainder,
// remove it from the highest delegation in the last iteration
remainingTokensToSlash := fraction.MulInt(val.Tokens).TruncateInt()
delegations := h.k.stakingKeeper.GetValidatorDelegations(ctx, valAddr)
slices.SortFunc(delegations, func(i, j stakingtypes.Delegation) bool {
return val.TokensFromShares(i.Shares).LT(val.TokensFromShares(j.Shares))
})
for i, d := range delegations {
tokens := val.TokensFromShares(d.Shares)
tokensToSlash := fraction.Mul(tokens).TruncateInt()
if i == len(delegations)-1 {
tokensToSlash = remainingTokensToSlash
}
if tokensToSlash.IsPositive() {
err := h.k.UnbondUniformProviders(ctx, d.DelegatorAddress, sdk.NewCoin(commontypes.TokenDenom, tokensToSlash))
if err != nil {
utils.LavaFormatError("slash hook failed", err,
utils.Attribute{Key: "validator_address", Value: valAddr.String()},
utils.Attribute{Key: "delegator_address", Value: d.DelegatorAddress},
utils.Attribute{Key: "slash_amount", Value: tokensToSlash.String()},
)
}

remainingTokensToSlash = remainingTokensToSlash.Sub(tokensToSlash)
}
}

details := make(map[string]string)
details["validator_address"] = valAddr.String()
details["slash_fraction"] = fraction.String()

utils.LogLavaEvent(ctx, h.k.Logger(ctx), types.ValidatorSlashEventName, details, "Validator slash hook event")
return nil
}

Expand Down
4 changes: 2 additions & 2 deletions x/dualstaking/keeper/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,12 +412,12 @@ func TestValidatorAndProvidersSlash(t *testing.T) {
for _, d := range res.Delegations {
totalDelegations = totalDelegations.Add(d.Amount.Amount)
}
require.Equal(t, sdk.OneDec().Sub(fraction).MulInt(consensusPowerTokens.MulRaw(245)).TruncateInt(), totalDelegations)
require.Equal(t, sdk.OneDec().Sub(fraction).MulInt(consensusPowerTokens.MulRaw(245)).RoundInt(), totalDelegations)

// verify once again that the delegator's delegations balance is preserved
diff, _, err = ts.Keepers.Dualstaking.VerifyDelegatorBalance(ts.Ctx, delegatorAcc.Addr)
require.NoError(t, err)
require.Equal(t, sdk.OneInt(), diff)
require.True(t, diff.IsZero())
}

// TestCancelUnbond checks that the providers-validators delegations balance is preserved when
Expand Down
5 changes: 5 additions & 0 deletions x/dualstaking/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"strconv"

abci "github.com/cometbft/cometbft/abci/types"
"github.com/cometbft/cometbft/libs/log"
"github.com/cosmos/cosmos-sdk/codec"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
Expand Down Expand Up @@ -105,3 +106,7 @@ func (k Keeper) ChangeDelegationTimestampForTesting(ctx sdk.Context, index strin
k.delegationFS.ModifyEntry(ctx, index, entryBlock, &d)
return nil
}

func (k Keeper) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {
k.HandleSlashedValidators(ctx, req)
}
41 changes: 41 additions & 0 deletions x/dualstaking/keeper/slashing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package keeper

import (
"fmt"

abci "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
evidenceTypes "github.com/cosmos/cosmos-sdk/x/evidence/types"
)

// balance delegators dualstaking after potential validators slashing
func (k Keeper) HandleSlashedValidators(ctx sdk.Context, req abci.RequestBeginBlock) {
for _, tmEvidence := range req.ByzantineValidators {
switch tmEvidence.Type {
case abci.MisbehaviorType_DUPLICATE_VOTE, abci.MisbehaviorType_LIGHT_CLIENT_ATTACK:
evidence := evidenceTypes.FromABCIEvidence(tmEvidence)
evidenceEq, ok := evidence.(*evidenceTypes.Equivocation)
if ok {
k.BalanceValidatorsDelegators(ctx, evidenceEq)
}

default:
k.Logger(ctx).Error(fmt.Sprintf("ignored unknown evidence type: %s", tmEvidence.Type))
}
}
}

func (k Keeper) BalanceValidatorsDelegators(ctx sdk.Context, evidence *evidenceTypes.Equivocation) {
consAddr := evidence.GetConsensusAddress()

validator := k.stakingKeeper.ValidatorByConsAddr(ctx, consAddr)
if validator == nil || validator.GetOperator().Empty() {
return
}

delegators := k.stakingKeeper.GetValidatorDelegations(ctx, validator.GetOperator())
for _, delegator := range delegators {
delAddr := delegator.GetDelegatorAddr()
k.BalanceDelegator(ctx, delAddr)
}
}
4 changes: 3 additions & 1 deletion x/dualstaking/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw
func (AppModule) ConsensusVersion() uint64 { return 5 }

// BeginBlock contains the logic that is automatically triggered at the beginning of each block
func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {}
func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {
am.keeper.BeginBlock(ctx, req)
}

// EndBlock contains the logic that is automatically triggered at the end of each block
func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
Expand Down
1 change: 1 addition & 0 deletions x/dualstaking/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type SpecKeeper interface {
}

type StakingKeeper interface {
ValidatorByConsAddr(sdk.Context, sdk.ConsAddress) stakingtypes.ValidatorI
UnbondingTime(ctx sdk.Context) time.Duration
GetAllDelegatorDelegations(ctx sdk.Context, delegator sdk.AccAddress) []stakingtypes.Delegation
GetDelegatorValidator(ctx sdk.Context, delegatorAddr sdk.AccAddress, validatorAddr sdk.ValAddress) (validator stakingtypes.Validator, err error)
Expand Down

0 comments on commit 976bbd1

Please sign in to comment.