Skip to content

Commit

Permalink
feat(sequencer): Allow a sequencer to decrease their bond (#1031)
Browse files Browse the repository at this point in the history
  • Loading branch information
spoo-bar authored Aug 9, 2024
1 parent 04bd9df commit acd9d13
Show file tree
Hide file tree
Showing 19 changed files with 1,324 additions and 87 deletions.
10 changes: 10 additions & 0 deletions proto/dymensionxyz/dymension/sequencer/sequencer.proto
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,13 @@ message Sequencer {
// unbond_time defines, if unbonding, the min time for the sequencer to complete unbonding.
google.protobuf.Timestamp unbond_time = 10 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true];
}

// BondReduction defines an object which holds the information about the sequencer and its queued unbonding amount
message BondReduction {
// sequencerAddress is the bech32-encoded address of the sequencer account which is the account that the message was sent from.
string sequencer_address = 1;
// unbondAmount is the amount of tokens to be unbonded.
cosmos.base.v1beta1.Coin unbond_amount = 2 [(gogoproto.nullable) = false];
// unbond_time defines, if unbonding, the min time for the sequencer to complete unbonding.
google.protobuf.Timestamp unbond_time = 3 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true];
}
20 changes: 19 additions & 1 deletion proto/dymensionxyz/dymension/sequencer/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ service Msg {

// IncreaseBond defines a method for increasing a sequencer's bond amount
rpc IncreaseBond (MsgIncreaseBond) returns (MsgIncreaseBondResponse);

// DecreaseBond defines a method for decreasing the bond of a sequencer.
rpc DecreaseBond (MsgDecreaseBond) returns (MsgDecreaseBondResponse);
}

message MsgCreateSequencer {
Expand Down Expand Up @@ -77,4 +80,19 @@ message MsgIncreaseBond {
}

// MsgIncreaseBondResponse defines the Msg/IncreaseBond response type.
message MsgIncreaseBondResponse {}
message MsgIncreaseBondResponse {}

// MsgDecreaseBond defines a SDK message for decreasing the bond of a sequencer.
message MsgDecreaseBond {
option (cosmos.msg.v1.signer) = "creator";
// creator is the bech32-encoded address of the sequencer account which is the account that the message was sent from.
string creator = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];

// decrease_amount is the amount of coins to decrease the bond by.
cosmos.base.v1beta1.Coin decrease_amount = 2 [(gogoproto.nullable) = false];
}

// MsgDecreaseBondResponse defines the Msg/DecreaseBond response type.
message MsgDecreaseBondResponse {
google.protobuf.Timestamp completion_time = 1 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true];
}
3 changes: 3 additions & 0 deletions x/sequencer/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ func NewHandler(k keeper.Keeper) sdk.Handler {
case *types.MsgIncreaseBond:
res, err := msgServer.IncreaseBond(sdk.WrapSDKContext(ctx), msg)
return sdk.WrapServiceResult(ctx, res, err)
case *types.MsgDecreaseBond:
res, err := msgServer.DecreaseBond(sdk.WrapSDKContext(ctx), msg)
return sdk.WrapServiceResult(ctx, res, err)
default:
errMsg := fmt.Sprintf("unrecognized %s message type: %T", types.ModuleName, msg)
return nil, errorsmod.Wrap(types.ErrUnknownRequest, errMsg)
Expand Down
16 changes: 14 additions & 2 deletions x/sequencer/keeper/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"time"

"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/dymensionxyz/dymension/v3/x/sequencer/types"
)
Expand All @@ -21,15 +22,22 @@ func (suite *SequencerTestSuite) TestFraudSubmittedHook() {

// create 5 sequencers for rollapp1
seqAddrs := make([]string, numOfSequencers)
seqAddrs[0] = suite.CreateDefaultSequencer(suite.Ctx, rollappId, pk)
seqAddrs[0] = suite.CreateSequencerWithBond(suite.Ctx, rollappId, bond.AddAmount(sdk.NewInt(20)), pk)

for i := 1; i < numOfSequencers; i++ {
pki := ed25519.GenPrivKey().PubKey()
seqAddrs[i] = suite.CreateDefaultSequencer(suite.Ctx, rollappId, pki)
}
proposer := seqAddrs[0]

err := keeper.RollappHooks().FraudSubmitted(suite.Ctx, rollappId, 0, proposer)
// queue the third sequencer to reduce bond
unbondMsg := types.MsgDecreaseBond{Creator: seqAddrs[0], DecreaseAmount: sdk.NewInt64Coin(bond.Denom, 10)}
resp, err := suite.msgServer.DecreaseBond(suite.Ctx, &unbondMsg)
suite.Require().NoError(err)
bds := keeper.GetMatureDecreasingBondSequencers(suite.Ctx, resp.GetCompletionTime())
suite.Require().Len(bds, 1)

err = keeper.RollappHooks().FraudSubmitted(suite.Ctx, rollappId, 0, proposer)
suite.Require().NoError(err)

// check if proposer is slashed
Expand All @@ -45,4 +53,8 @@ func (suite *SequencerTestSuite) TestFraudSubmittedHook() {
suite.Require().False(sequencer.Proposer)
suite.Require().Equal(sequencer.Status, types.Unbonded)
}

// check if bond reduction queue is pruned
bds = keeper.GetMatureDecreasingBondSequencers(suite.Ctx, resp.GetCompletionTime())
suite.Require().Len(bds, 0)
}
49 changes: 49 additions & 0 deletions x/sequencer/keeper/msg_server_decrease_bond.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package keeper

import (
"context"

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

// DecreaseBond implements types.MsgServer.
func (k msgServer) DecreaseBond(goCtx context.Context, msg *types.MsgDecreaseBond) (*types.MsgDecreaseBondResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

sequencer, err := k.bondUpdateAllowed(ctx, msg.GetCreator())
if err != nil {
return nil, err
}

effectiveBond := sequencer.Tokens
if bds := k.getSequencerDecreasingBonds(ctx, msg.Creator); len(bds) > 0 {
for _, bd := range bds {
effectiveBond = effectiveBond.Sub(bd.UnbondAmount)
}
}

// Check if the sequencer has enough bond to decrease
if !effectiveBond.IsZero() && effectiveBond.IsAllLTE(sdk.NewCoins(msg.DecreaseAmount)) {
return nil, types.ErrInsufficientBond
}

// Check if the bond reduction will make the sequencer's bond less than the minimum bond value
minBondValue := k.GetParams(ctx).MinBond
if !minBondValue.IsNil() && !minBondValue.IsZero() {
decreasedBondValue := effectiveBond.Sub(msg.DecreaseAmount)
if decreasedBondValue.IsAllLT(sdk.NewCoins(minBondValue)) {
return nil, types.ErrInsufficientBond
}
}
completionTime := ctx.BlockHeader().Time.Add(k.UnbondingTime(ctx))
k.setDecreasingBondQueue(ctx, types.BondReduction{
SequencerAddress: msg.Creator,
UnbondAmount: msg.DecreaseAmount,
UnbondTime: completionTime,
})

return &types.MsgDecreaseBondResponse{
CompletionTime: completionTime,
}, nil
}
128 changes: 128 additions & 0 deletions x/sequencer/keeper/msg_server_decrease_bond_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package keeper_test

import (
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/dymensionxyz/dymension/v3/x/sequencer/types"
)

func (suite *SequencerTestSuite) TestDecreaseBond() {
suite.SetupTest()
bondDenom := types.DefaultParams().MinBond.Denom
rollappId, pk := suite.CreateDefaultRollapp()
// setup a default sequencer with has minBond + 20token
defaultSequencerAddress := suite.CreateSequencerWithBond(suite.Ctx, rollappId, bond.AddAmount(sdk.NewInt(20)), pk)
// setup an unbonded sequencer
unbondedPk := ed25519.GenPrivKey().PubKey()
unbondedSequencerAddress := suite.CreateDefaultSequencer(suite.Ctx, rollappId, unbondedPk)
unbondedSequencer, _ := suite.App.SequencerKeeper.GetSequencer(suite.Ctx, unbondedSequencerAddress)
unbondedSequencer.Status = types.Unbonded
suite.App.SequencerKeeper.UpdateSequencer(suite.Ctx, unbondedSequencer, unbondedSequencer.Status)
// setup a jailed sequencer
jailedPk := ed25519.GenPrivKey().PubKey()
jailedSequencerAddress := suite.CreateDefaultSequencer(suite.Ctx, rollappId, jailedPk)
jailedSequencer, _ := suite.App.SequencerKeeper.GetSequencer(suite.Ctx, jailedSequencerAddress)
jailedSequencer.Jailed = true
suite.App.SequencerKeeper.UpdateSequencer(suite.Ctx, jailedSequencer, jailedSequencer.Status)

testCase := []struct {
name string
msg types.MsgDecreaseBond
expectedErr error
}{
{
name: "invalid sequencer",
msg: types.MsgDecreaseBond{
Creator: "invalid_address",
DecreaseAmount: sdk.NewInt64Coin(bondDenom, 10),
},
expectedErr: types.ErrUnknownSequencer,
},
{
name: "sequencer is not bonded",
msg: types.MsgDecreaseBond{
Creator: unbondedSequencerAddress,
DecreaseAmount: sdk.NewInt64Coin(bondDenom, 10),
},
expectedErr: types.ErrInvalidSequencerStatus,
},
{
name: "sequencer is jailed",
msg: types.MsgDecreaseBond{
Creator: jailedSequencerAddress,
DecreaseAmount: sdk.NewInt64Coin(bondDenom, 10),
},
expectedErr: types.ErrSequencerJailed,
},
{
name: "decreased bond value to less than minimum bond value",
msg: types.MsgDecreaseBond{
Creator: defaultSequencerAddress,
DecreaseAmount: sdk.NewInt64Coin(bondDenom, 100),
},
expectedErr: types.ErrInsufficientBond,
},
{
name: "trying to decrease more bond than they have tokens bonded",
msg: types.MsgDecreaseBond{
Creator: defaultSequencerAddress,
DecreaseAmount: bond.AddAmount(sdk.NewInt(30)),
},
expectedErr: types.ErrInsufficientBond,
},
{
name: "valid decrease bond",
msg: types.MsgDecreaseBond{
Creator: defaultSequencerAddress,
DecreaseAmount: sdk.NewInt64Coin(bondDenom, 10),
},
},
}

for _, tc := range testCase {
suite.Run(tc.name, func() {
resp, err := suite.msgServer.DecreaseBond(suite.Ctx, &tc.msg)
if tc.expectedErr != nil {
suite.Require().ErrorIs(err, tc.expectedErr)
} else {
suite.Require().NoError(err)
suite.Require().NotNil(resp)
expectedCompletionTime := suite.Ctx.BlockHeader().Time.Add(suite.App.SequencerKeeper.UnbondingTime(suite.Ctx))
suite.Require().Equal(expectedCompletionTime, resp.CompletionTime)
// check if the unbonding is set correctly
unbondings := suite.App.SequencerKeeper.GetMatureDecreasingBondSequencers(suite.Ctx, expectedCompletionTime)
suite.Require().Len(unbondings, 1)
suite.Require().Equal(tc.msg.Creator, unbondings[0].SequencerAddress)
suite.Require().Equal(tc.msg.DecreaseAmount, unbondings[0].UnbondAmount)
}
})
}
}

func (suite *SequencerTestSuite) TestDecreaseBond_BondDecreaseInProgress() {
suite.SetupTest()
bondDenom := types.DefaultParams().MinBond.Denom
rollappId, pk := suite.CreateDefaultRollapp()
// setup a default sequencer with has minBond + 20token
defaultSequencerAddress := suite.CreateSequencerWithBond(suite.Ctx, rollappId, bond.AddAmount(sdk.NewInt(20)), pk)
// decrease the bond of the sequencer
_, err := suite.msgServer.DecreaseBond(suite.Ctx, &types.MsgDecreaseBond{
Creator: defaultSequencerAddress,
DecreaseAmount: sdk.NewInt64Coin(bondDenom, 10),
})
suite.Require().NoError(err)
// try to decrease the bond again - should be fine as still not below minbond
suite.Ctx = suite.Ctx.WithBlockHeight(suite.Ctx.BlockHeight() + 1).WithBlockTime(suite.Ctx.BlockTime().Add(10))
_, err = suite.msgServer.DecreaseBond(suite.Ctx, &types.MsgDecreaseBond{
Creator: defaultSequencerAddress,
DecreaseAmount: sdk.NewInt64Coin(bondDenom, 10),
})
suite.Require().NoError(err)
// try to decrease the bond again - should err as below minbond
suite.Ctx = suite.Ctx.WithBlockHeight(suite.Ctx.BlockHeight() + 1).WithBlockTime(suite.Ctx.BlockTime().Add(10))
_, err = suite.msgServer.DecreaseBond(suite.Ctx, &types.MsgDecreaseBond{
Creator: defaultSequencerAddress,
DecreaseAmount: sdk.NewInt64Coin(bondDenom, 10),
})
suite.Require().ErrorIs(err, types.ErrInsufficientBond)
}
6 changes: 3 additions & 3 deletions x/sequencer/keeper/msg_server_increase_bond.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
func (k msgServer) IncreaseBond(goCtx context.Context, msg *types.MsgIncreaseBond) (*types.MsgIncreaseBondResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

sequencer, err := k.bondUpdateAllowed(ctx, msg)
sequencer, err := k.bondUpdateAllowed(ctx, msg.GetCreator())
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -39,9 +39,9 @@ func (k msgServer) IncreaseBond(goCtx context.Context, msg *types.MsgIncreaseBon
return &types.MsgIncreaseBondResponse{}, err
}

func (k msgServer) bondUpdateAllowed(ctx sdk.Context, msg *types.MsgIncreaseBond) (types.Sequencer, error) {
func (k msgServer) bondUpdateAllowed(ctx sdk.Context, senderAddress string) (types.Sequencer, error) {
// check if the sequencer already exists
sequencer, found := k.GetSequencer(ctx, msg.Creator)
sequencer, found := k.GetSequencer(ctx, senderAddress)
if !found {
return types.Sequencer{}, types.ErrUnknownSequencer
}
Expand Down
52 changes: 52 additions & 0 deletions x/sequencer/keeper/sequencer.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,55 @@ func (k Keeper) removeUnbondingSequencer(ctx sdk.Context, sequencer types.Sequen
unbondingQueueKey := types.UnbondingSequencerKey(sequencer.Address, sequencer.UnbondTime)
store.Delete(unbondingQueueKey)
}

/* -------------------------------------------------------------------------- */
/* Decreasing Bond Queue */
/* -------------------------------------------------------------------------- */

// GetMatureDecreasingBondSequencers returns all decreasing bond items for the given time
func (k Keeper) GetMatureDecreasingBondSequencers(ctx sdk.Context, endTime time.Time) (unbondings []types.BondReduction) {
store := ctx.KVStore(k.storeKey)
iterator := store.Iterator(types.DecreasingBondQueueKey, sdk.PrefixEndBytes(types.DecreasingBondQueueByTimeKey(endTime)))
defer iterator.Close() // nolint: errcheck
for ; iterator.Valid(); iterator.Next() {
var b types.BondReduction
k.cdc.MustUnmarshal(iterator.Value(), &b)
unbondings = append(unbondings, b)
}
return
}

// setDecreasingBondQueue sets the bond reduction item in the decreasing bond queue
func (k Keeper) setDecreasingBondQueue(ctx sdk.Context, bondReduction types.BondReduction) {
store := ctx.KVStore(k.storeKey)
b := k.cdc.MustMarshal(&bondReduction)

unbondingQueueKey := types.GetDecreasingBondQueueKey(bondReduction.SequencerAddress, bondReduction.GetUnbondTime())
store.Set(unbondingQueueKey, b)
}

// removeDecreasingBondQueue removes the bond reduction item from the decreasing bond queue
func (k Keeper) removeDecreasingBondQueue(ctx sdk.Context, bondReduction types.BondReduction) {
store := ctx.KVStore(k.storeKey)
unbondingQueueKey := types.GetDecreasingBondQueueKey(bondReduction.SequencerAddress, bondReduction.GetUnbondTime())
store.Delete(unbondingQueueKey)
}

// getSequencerDecreasingBonds returns the bond reduction item given sequencer address
func (k Keeper) getSequencerDecreasingBonds(ctx sdk.Context, sequencerAddr string) (bds []types.BondReduction) {
prefixKey := types.DecreasingBondQueueKey
store := prefix.NewStore(ctx.KVStore(k.storeKey), prefixKey)
iterator := sdk.KVStorePrefixIterator(store, []byte{})

defer iterator.Close() // nolint: errcheck

for ; iterator.Valid(); iterator.Next() {
var bd types.BondReduction
k.cdc.MustUnmarshal(iterator.Value(), &bd)
if bd.SequencerAddress == sequencerAddr {
bds = append(bds, bd)
}
}

return
}
8 changes: 8 additions & 0 deletions x/sequencer/keeper/slashing.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ func (k Keeper) Jail(ctx sdk.Context, seq types.Sequencer) error {
// in case we are slashing an unbonding sequencer, we need to remove it from the unbonding queue
if oldStatus == types.Unbonding {
k.removeUnbondingSequencer(ctx, seq)
} else {
// in case the sequencer is currently reducing its bond, then we need to remove it from the decreasing bond queue
// all the tokens are burned, so we don't need to reduce the bond anymore
if bondReductions := k.getSequencerDecreasingBonds(ctx, seq.Address); len(bondReductions) > 0 {
for _, bondReduce := range bondReductions {
k.removeDecreasingBondQueue(ctx, bondReduce)
}
}
}

// set the status to unbonded
Expand Down
24 changes: 24 additions & 0 deletions x/sequencer/keeper/slashing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,27 @@ func (s *SequencerTestSuite) TestSlashBasic() {
s.Require().NoError(err)
})
}

func (suite *SequencerTestSuite) TestSlashingBondReducingSequencer() {
suite.SetupTest()
keeper := suite.App.SequencerKeeper

rollappId, pk := suite.CreateDefaultRollapp()
seqAddr := suite.CreateSequencerWithBond(suite.Ctx, rollappId, bond.AddAmount(sdk.NewInt(20)), pk)

Check failure on line 23 in x/sequencer/keeper/slashing_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: sdk

Check failure on line 23 in x/sequencer/keeper/slashing_test.go

View workflow job for this annotation

GitHub Actions / build

undefined: sdk

suite.Ctx = suite.Ctx.WithBlockHeight(20)
suite.Ctx = suite.Ctx.WithBlockTime(time.Now())

Check failure on line 26 in x/sequencer/keeper/slashing_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: time

Check failure on line 26 in x/sequencer/keeper/slashing_test.go

View workflow job for this annotation

GitHub Actions / build

undefined: time

reduceBondMsg := types.MsgDecreaseBond{Creator: seqAddr, DecreaseAmount: sdk.NewInt64Coin(bond.Denom, 10)}

Check failure on line 28 in x/sequencer/keeper/slashing_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: types

Check failure on line 28 in x/sequencer/keeper/slashing_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: sdk

Check failure on line 28 in x/sequencer/keeper/slashing_test.go

View workflow job for this annotation

GitHub Actions / build

undefined: types

Check failure on line 28 in x/sequencer/keeper/slashing_test.go

View workflow job for this annotation

GitHub Actions / build

undefined: sdk
resp, err := suite.msgServer.DecreaseBond(suite.Ctx, &reduceBondMsg)
suite.Require().NoError(err)
bondReductions := keeper.GetMatureDecreasingBondSequencers(suite.Ctx, resp.GetCompletionTime())
suite.Require().Len(bondReductions, 1)

err = keeper.Slashing(suite.Ctx, seqAddr)

Check failure on line 34 in x/sequencer/keeper/slashing_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

keeper.Slashing undefined (type "github.com/dymensionxyz/dymension/v3/x/sequencer/keeper".Keeper has no field or method Slashing) (typecheck)

Check failure on line 34 in x/sequencer/keeper/slashing_test.go

View workflow job for this annotation

GitHub Actions / build

keeper.Slashing undefined (type "github.com/dymensionxyz/dymension/v3/x/sequencer/keeper".Keeper has no field or method Slashing)
suite.NoError(err)

bondReductions = keeper.GetMatureDecreasingBondSequencers(suite.Ctx, resp.GetCompletionTime())
suite.Require().Len(bondReductions, 0)
suite.assertSlashed(seqAddr)
}
Loading

0 comments on commit acd9d13

Please sign in to comment.