diff --git a/app/app.go b/app/app.go index 755c924811..208e196e6d 100644 --- a/app/app.go +++ b/app/app.go @@ -755,6 +755,7 @@ func New( crisistypes.ModuleName, genutiltypes.ModuleName, evidencetypes.ModuleName, + dualstakingmoduletypes.ModuleName, ibctransfertypes.ModuleName, ibcexported.ModuleName, group.ModuleName, @@ -762,7 +763,6 @@ func New( 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 diff --git a/testutil/common/tester.go b/testutil/common/tester.go index 9660e00c75..9d5b7cb48c 100644 --- a/testutil/common/tester.go +++ b/testutil/common/tester.go @@ -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" @@ -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() diff --git a/testutil/keeper/keepers_init.go b/testutil/keeper/keepers_init.go index 8bc7615683..e364ca4a6d 100644 --- a/testutil/keeper/keepers_init.go +++ b/testutil/keeper/keepers_init.go @@ -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" @@ -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) } @@ -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{}) + } } } diff --git a/x/dualstaking/keeper/balance.go b/x/dualstaking/keeper/balance.go new file mode 100644 index 0000000000..9c42ff2bd9 --- /dev/null +++ b/x/dualstaking/keeper/balance.go @@ -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 +} diff --git a/x/dualstaking/keeper/hooks.go b/x/dualstaking/keeper/hooks.go index 7113ca4679..4a77947588 100644 --- a/x/dualstaking/keeper/hooks.go +++ b/x/dualstaking/keeper/hooks.go @@ -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 @@ -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 } diff --git a/x/dualstaking/keeper/hooks_test.go b/x/dualstaking/keeper/hooks_test.go index 3a5b76de4f..4d7c5e4e3a 100644 --- a/x/dualstaking/keeper/hooks_test.go +++ b/x/dualstaking/keeper/hooks_test.go @@ -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 diff --git a/x/dualstaking/keeper/keeper.go b/x/dualstaking/keeper/keeper.go index a6fabb47c8..7993dc1d4d 100644 --- a/x/dualstaking/keeper/keeper.go +++ b/x/dualstaking/keeper/keeper.go @@ -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" @@ -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) +} diff --git a/x/dualstaking/keeper/slashing.go b/x/dualstaking/keeper/slashing.go new file mode 100644 index 0000000000..ab2aaf5d16 --- /dev/null +++ b/x/dualstaking/keeper/slashing.go @@ -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) + } +} diff --git a/x/dualstaking/module.go b/x/dualstaking/module.go index e389e3a762..a85f30ebc1 100644 --- a/x/dualstaking/module.go +++ b/x/dualstaking/module.go @@ -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 { diff --git a/x/dualstaking/types/expected_keepers.go b/x/dualstaking/types/expected_keepers.go index bdbf2064cb..3ade1ebecb 100644 --- a/x/dualstaking/types/expected_keepers.go +++ b/x/dualstaking/types/expected_keepers.go @@ -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)