Skip to content

Commit

Permalink
feat(sequencer): kick rework (dishonor) (#1647)
Browse files Browse the repository at this point in the history
  • Loading branch information
danwt authored Dec 12, 2024
1 parent 16b96f8 commit 29d227f
Show file tree
Hide file tree
Showing 17 changed files with 315 additions and 186 deletions.
4 changes: 3 additions & 1 deletion app/upgrades/v4/upgrade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,9 @@ func (s *UpgradeTestSuite) validateSequencersMigration(numSeq int) error {
s.Require().False(p.Sentinel())
}
s.Require().Equal(sequencertypes.DefaultNoticePeriod, s.App.SequencerKeeper.GetParams(s.Ctx).NoticePeriod)
s.Require().Equal(sequencertypes.DefaultKickThreshold, s.App.SequencerKeeper.GetParams(s.Ctx).KickThreshold)
s.Require().Equal(sequencertypes.DefaultDishonorKickThreshold, s.App.SequencerKeeper.GetParams(s.Ctx).DishonorKickThreshold)
s.Require().Equal(sequencertypes.DefaultDishonorLiveness, s.App.SequencerKeeper.GetParams(s.Ctx).DishonorLiveness)
s.Require().Equal(sequencertypes.DefaultDishonorStateUpdate, s.App.SequencerKeeper.GetParams(s.Ctx).DishonorStateUpdate)
s.Require().Equal(sequencertypes.DefaultLivenessSlashMultiplier, s.App.SequencerKeeper.GetParams(s.Ctx).LivenessSlashMinMultiplier)
s.Require().Equal(sequencertypes.DefaultLivenessSlashMinAbsolute, s.App.SequencerKeeper.GetParams(s.Ctx).LivenessSlashMinAbsolute)

Expand Down
16 changes: 11 additions & 5 deletions proto/dymensionxyz/dymension/sequencer/params.proto
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@ message Params {

reserved 1;

// amt where the active sequencer can be kicked if he has less or equal bond
cosmos.base.v1beta1.Coin kick_threshold = 5 [
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "kick_threshold,omitempty"
];



reserved 5;
reserved 2;

// notice_period is the time duration of notice period.
Expand All @@ -38,4 +37,11 @@ message Params {
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "liveness_slash_min_absolute,omitempty"
];

// how much dishonor a sequencer gains on liveness events (+dishonor)
uint64 dishonor_liveness = 7;
// how much honor a sequencer gains on state updates (-dishonor)
uint64 dishonor_state_update = 8;
// the minimum dishonor at which a sequencer can be kicked (<=)
uint64 dishonor_kick_threshold = 9;
}
5 changes: 5 additions & 0 deletions proto/dymensionxyz/dymension/sequencer/sequencer.proto
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,10 @@ message Sequencer {
string reward_addr = 12;
// WhitelistedRelayers is an array of the whitelisted relayer addresses. Addresses are bech32-encoded strings.
repeated string whitelisted_relayers = 13;


// how badly behaved sequencer is, can incur penalties (kicking) when high
// 0 is good/default, more is worse
uint64 dishonor = 15;
}

5 changes: 4 additions & 1 deletion x/iro/types/iro.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 23 additions & 3 deletions x/sequencer/keeper/fraud.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,37 @@ func (k Keeper) SlashLiveness(ctx sdk.Context, rollappID string) error {

// correct formula is e.g. min(sequencer tokens, max(1, sequencer tokens * 0.01 ))

err := k.livenessSlash(ctx, &seq)
if err != nil {
return errorsmod.Wrap(err, "slash")
}
k.livenessDishonor(ctx, &seq)
k.SetSequencer(ctx, seq)
return nil
}

func (k Keeper) livenessSlash(ctx sdk.Context, seq *types.Sequencer) error {
mul := k.GetParams(ctx).LivenessSlashMinMultiplier
abs := k.GetParams(ctx).LivenessSlashMinAbsolute
tokens := seq.TokensCoin()
tokensMul := ucoin.MulDec(mul, tokens)
amt := ucoin.SimpleMin(tokens, ucoin.SimpleMax(abs, tokensMul[0]))
err := errorsmod.Wrap(k.slash(ctx, &seq, amt, sdk.ZeroDec(), nil), "slash")
k.SetSequencer(ctx, seq)
return err
return errorsmod.Wrap(k.slash(ctx, seq, amt, sdk.ZeroDec(), nil), "slash")
}

func (k Keeper) livenessHonor(ctx sdk.Context, seq *types.Sequencer) {
reward := k.GetParams(ctx).DishonorStateUpdate
reward = min(reward, seq.Dishonor)
seq.Dishonor -= reward
}

func (k Keeper) livenessDishonor(ctx sdk.Context, seq *types.Sequencer) {
penalty := k.GetParams(ctx).DishonorLiveness
seq.Dishonor += penalty
}

// Takes an optional rewardee addr who will receive some bounty
// Currently there is no dishonor penalty (anyway we slash 100%)
func (k Keeper) PunishSequencer(ctx sdk.Context, seqAddr string, rewardee *sdk.AccAddress) error {
var (
rewardMul = sdk.ZeroDec()
Expand Down
26 changes: 8 additions & 18 deletions x/sequencer/keeper/fraud_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,28 @@ import (
"github.com/dymensionxyz/sdk-utils/utils/ucoin"
)

// Can eventually slash to below kickable threshold
func (s *SequencerTestSuite) TestSlashLivenessFlow() {
// Can eventually get below kickable threshold
func (s *SequencerTestSuite) TestLivenessFlow() {
ra := s.createRollapp()
s.createSequencerWithBond(s.Ctx, ra.RollappId, alice, bond)
seq := s.seq(alice)
s.Require().True(s.k().IsProposer(s.Ctx, seq))

s.Require().False(s.k().Kickable(s.Ctx, seq))
ok := false
last := seq.TokensCoin()
lastTokens := seq.TokensCoin()
lastDishonor := seq.Dishonor
for range 100000000 {
err := s.k().SlashLiveness(s.Ctx, ra.RollappId)
s.Require().NoError(err)
s.Require().True(s.k().IsProposer(s.Ctx, seq))
seq = s.seq(alice)
mod := s.moduleBalance()
s.Require().True(mod.Equal(seq.TokensCoin()))
s.Require().True(seq.TokensCoin().IsLT(last))
s.Require().True(seq.TokensCoin().IsLT(lastTokens))
s.Require().True(seq.Dishonor > lastDishonor)
lastTokens = seq.TokensCoin()
lastDishonor = seq.Dishonor
if s.k().Kickable(s.Ctx, seq) {
ok = true
break
Expand All @@ -36,20 +40,6 @@ func (s *SequencerTestSuite) TestSlashLivenessFlow() {
func (s *SequencerTestSuite) TestPunishSequencer() {
ra := s.createRollapp()

s.Run("kickable after punish", func() {
s.createSequencerWithBond(s.Ctx, ra.RollappId, alice, bond)
seq := s.seq(alice)

s.k().SetProposer(s.Ctx, ra.RollappId, seq.Address)
s.Require().False(s.k().Kickable(s.Ctx, seq))

err := s.k().PunishSequencer(s.Ctx, seq.Address, nil)
s.Require().NoError(err)

seq = s.seq(alice)
// assert alice is now kickable
s.Require().True(s.k().Kickable(s.Ctx, seq))
})
s.Run("without rewardee", func() {
s.createSequencerWithBond(s.Ctx, ra.RollappId, bob, bond)
seq := s.seq(bob)
Expand Down
4 changes: 2 additions & 2 deletions x/sequencer/keeper/funds.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ func (k Keeper) sufficientBond(ctx sdk.Context, rollapp string, c sdk.Coin) erro
}

func (k Keeper) Kickable(ctx sdk.Context, proposer types.Sequencer) bool {
kickThreshold := k.GetParams(ctx).KickThreshold
return !proposer.Sentinel() && proposer.TokensCoin().IsLTE(kickThreshold)
kickThreshold := k.GetParams(ctx).DishonorKickThreshold
return !proposer.Sentinel() && kickThreshold <= proposer.Dishonor
}

func (k Keeper) burn(ctx sdk.Context, seq *types.Sequencer, amt sdk.Coin) error {
Expand Down
13 changes: 1 addition & 12 deletions x/sequencer/keeper/hook_listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,8 @@ func (hook rollappHook) BeforeUpdateState(ctx sdk.Context, seqAddr, rollappId st

// AfterUpdateState checks if rotation is completed and the nextProposer is changed
func (hook rollappHook) AfterUpdateState(ctx sdk.Context, stateInfo *rollapptypes.StateInfoMeta) error {
// no proposer changed - no op
if stateInfo.Sequencer == stateInfo.NextProposer {
return nil
}

// handle proposer rotation completion
proposer := hook.k.GetProposer(ctx, stateInfo.Rollapp)
err := hook.k.OnProposerLastBlock(ctx, proposer)
if err != nil {
return errorsmod.Wrap(err, "on proposer last block")
}

return nil
return hook.k.afterStateUpdate(ctx, proposer, stateInfo.Sequencer != stateInfo.NextProposer)
}

// OnHardFork implements the RollappHooks interface
Expand Down
7 changes: 6 additions & 1 deletion x/sequencer/keeper/msg_server_kick_proposer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func (s *SequencerTestSuite) TestKickProposerBasicFlow() {
s.Require().False(s.k().IsProposer(s.Ctx, seqBob))

// alice falls to threshold
seqAlice.SetTokensCoin(kick)
seqAlice.Dishonor = types.DefaultDishonorKickThreshold
s.k().SetSequencer(s.Ctx, seqAlice)
_, err = s.msgServer.KickProposer(s.Ctx, m)
s.Require().NoError(err)
Expand All @@ -46,4 +46,9 @@ func (s *SequencerTestSuite) TestKickProposerBasicFlow() {
s.Require().True(s.k().IsProposer(s.Ctx, seqBob))
seqAlice = s.k().GetSequencer(s.Ctx, seqAlice.Address)
s.Require().Equal(types.Unbonded, seqAlice.Status)

// alice can get tokens back (assuming no unfinalized states etc)
s.k().SetUnbondBlockers()
_, err = s.msgServer.Unbond(s.Ctx, &types.MsgUnbond{Creator: pkAddr(alice)})
s.Require().NoError(err)
}
21 changes: 4 additions & 17 deletions x/sequencer/keeper/params.go
Original file line number Diff line number Diff line change
@@ -1,38 +1,25 @@
package keeper

import (
errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/dymensionxyz/gerr-cosmos/gerrc"

"github.com/dymensionxyz/dymension/v3/x/sequencer/types"
"github.com/dymensionxyz/gerr-cosmos/gerrc"
)

// ValidateParams is a stateful validation for params.
// it validates that unbonding time is greater then x/rollapp's dispute period
// and that the correct denom is set.
// The unbonding time is set by governance hence it's more of a sanity/human error check which
// in theory should never fail as setting such unbonding time has wide protocol security implications beyond the dispute period.
func (k Keeper) ValidateParams(ctx sdk.Context, params types.Params) error {
// validate min bond denom
denom, err := sdk.GetBaseDenom()
if err != nil {
return errorsmod.Wrapf(gerrc.ErrInternal, "failed to get base denom: %v", err)
}
if params.KickThreshold.Denom != denom {
return errorsmod.Wrapf(gerrc.ErrInvalidArgument, "kick threshold denom must be equal to base denom")
func (k Keeper) ValidateParams(_ sdk.Context, params types.Params) error {
if params.DishonorKickThreshold == 0 {
return gerrc.ErrOutOfRange.Wrap("0 kick threshold")
}
return nil
}

// SetParams sets the auth module's parameters.
func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
store := ctx.KVStore(k.storeKey)
bz := k.cdc.MustMarshal(&params)
store.Set(types.ParamsKey, bz)
}

// GetParams gets the auth module's parameters.
func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.ParamsKey)
Expand Down
10 changes: 10 additions & 0 deletions x/sequencer/keeper/proposer.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ import (
"github.com/dymensionxyz/sdk-utils/utils/uevent"
)

// when the proposer did a state update
func (k Keeper) afterStateUpdate(ctx sdk.Context, prop types.Sequencer, last bool) error {
k.livenessHonor(ctx, &prop)
k.SetSequencer(ctx, prop)
if last {
return k.OnProposerLastBlock(ctx, prop)
}
return nil
}

func (k Keeper) abruptRemoveProposer(ctx sdk.Context, rollapp string) {
proposer := k.GetProposer(ctx, rollapp)
if proposer.Sentinel() {
Expand Down
1 change: 0 additions & 1 deletion x/sequencer/keeper/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ var (
// TODO: use separate cosmos/dymint pubkeys in tests https://github.com/dymensionxyz/dymension/issues/1360

bond = rollapptypes.DefaultMinSequencerBondGlobalCoin
kick = types.DefaultParams().KickThreshold
pks = []cryptotypes.PubKey{
randomTMPubKey(),
randomTMPubKey(),
Expand Down
1 change: 0 additions & 1 deletion x/sequencer/proposal_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ func NewSequencerProposalHandler(k keeper.Keeper) govtypes.Handler {
}
}

// HandlePunishSequencerProposal is a handler for executing a passed community spend proposal
func HandlePunishSequencerProposal(ctx sdk.Context, k keeper.Keeper, p *types.PunishSequencerProposal) error {
err := k.PunishSequencer(ctx, p.PunishSequencerAddress, p.MustRewardee())
if err != nil {
Expand Down
26 changes: 20 additions & 6 deletions x/sequencer/types/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,37 @@ import (
)

var (
// DefaultKickThreshold is the minimum bond required to be a validator
DefaultKickThreshold = commontypes.DYMCoin
// DefaultNoticePeriod is the time duration for notice period
DefaultNoticePeriod = time.Hour * 24 * 7 // 1 week
// DefaultLivenessSlashMultiplier gives the amount of tokens to slash if the sequencer is liable for a liveness failure
DefaultLivenessSlashMultiplier = sdk.MustNewDecFromStr("0.01")
// DefaultLivenessSlashMinAbsolute will be slashed if the multiplier amount is too small
DefaultLivenessSlashMinAbsolute = commontypes.DYMCoin

DefaultDishonorStateUpdate = uint64(1)
DefaultDishonorLiveness = uint64(300)
DefaultDishonorKickThreshold = uint64(900)
)

// NewParams creates a new Params instance
func NewParams(noticePeriod time.Duration, livenessSlashMul sdk.Dec, livenessSlashAbs sdk.Coin, kickThreshold sdk.Coin) Params {
func NewParams(noticePeriod time.Duration, livenessSlashMul sdk.Dec, livenessSlashAbs sdk.Coin,
dishonorStateUpdate uint64,
dishonorLiveness uint64,
dishonorKickThreshold uint64,
) Params {
return Params{
NoticePeriod: noticePeriod,
LivenessSlashMinMultiplier: livenessSlashMul,
LivenessSlashMinAbsolute: livenessSlashAbs,
KickThreshold: kickThreshold,
DishonorStateUpdate: dishonorStateUpdate,
DishonorLiveness: dishonorLiveness,
DishonorKickThreshold: dishonorKickThreshold,
}
}

// DefaultParams returns a default set of parameters
func DefaultParams() Params {
return NewParams(DefaultNoticePeriod, DefaultLivenessSlashMultiplier, DefaultLivenessSlashMinAbsolute, DefaultKickThreshold)
return NewParams(DefaultNoticePeriod, DefaultLivenessSlashMultiplier, DefaultLivenessSlashMinAbsolute, DefaultDishonorStateUpdate, DefaultDishonorLiveness, DefaultDishonorKickThreshold)
}

func validateTime(i interface{}) error {
Expand Down Expand Up @@ -67,7 +75,13 @@ func (p Params) ValidateBasic() error {
return err
}

if err := uparam.ValidateCoin(p.KickThreshold); err != nil {
if err := uparam.ValidateUint64(p.DishonorKickThreshold); err != nil {
return err
}
if err := uparam.ValidateUint64(p.DishonorLiveness); err != nil {
return err
}
if err := uparam.ValidateUint64(p.DishonorKickThreshold); err != nil {
return err
}

Expand Down
Loading

0 comments on commit 29d227f

Please sign in to comment.