diff --git a/PENDING.md b/PENDING.md index 5f63f98d2e5d..aee683108fb7 100644 --- a/PENDING.md +++ b/PENDING.md @@ -27,9 +27,10 @@ BREAKING CHANGES * [\#3064](https://github.com/cosmos/cosmos-sdk/issues/3064) Sanitize `sdk.Coin` denom. Coins denoms are now case insensitive, i.e. 100fooToken equals to 100FOOTOKEN. * [\#3195](https://github.com/cosmos/cosmos-sdk/issues/3195) Allows custom configuration for syncable strategy * [\#3242](https://github.com/cosmos/cosmos-sdk/issues/3242) Fix infinite gas - meter utilization during aborted ante handler executions. - * [\#2222] [x/staking] `/stake` -> `/staking` module rename - + meter utilization during aborted ante handler executions. + * [staking] \#2222 `/stake` -> `/staking` module rename + * [staking] \#1402 Redelegation and unbonding-delegation structs changed to include multiple an array of entries + * Tendermint * \#3279 Upgrade to Tendermint 0.28.0-dev0 @@ -83,6 +84,7 @@ IMPROVEMENTS slashing, and staking modules. * [\#3093](https://github.com/cosmos/cosmos-sdk/issues/3093) Ante handler does no longer read all accounts in one go when processing signatures as signature verification may fail before last signature is checked. + * [x/stake] \#1402 Add for multiple simultaneous redelegations or unbonding-delegations within an unbonding period * Tendermint diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index bb099711fd4a..68a749a322ef 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -467,8 +467,9 @@ func TestBonding(t *testing.T) { require.Len(t, txs, 1) require.Equal(t, resultTx.Height, txs[0].Height) - unbonding := getUndelegation(t, port, addr, operAddrs[0]) - require.Equal(t, int64(30), unbonding.Balance.Amount.Int64()) + ubd := getUnbondingDelegation(t, port, addr, operAddrs[0]) + require.Len(t, ubd.Entries, 1) + require.Equal(t, int64(30), ubd.Entries[0].Balance.Amount.Int64()) // test redelegation resultTx = doBeginRedelegation(t, port, name1, pw, addr, operAddrs[0], operAddrs[1], 30, fees) @@ -502,23 +503,28 @@ func TestBonding(t *testing.T) { redelegation := getRedelegations(t, port, addr, operAddrs[0], operAddrs[1]) require.Len(t, redelegation, 1) - require.Equal(t, "30", redelegation[0].Balance.Amount.String()) + require.Len(t, redelegation[0].Entries, 1) + require.Equal(t, "30", redelegation[0].Entries[0].Balance.Amount.String()) delegatorUbds := getDelegatorUnbondingDelegations(t, port, addr) require.Len(t, delegatorUbds, 1) - require.Equal(t, "30", delegatorUbds[0].Balance.Amount.String()) + require.Len(t, delegatorUbds[0].Entries, 1) + require.Equal(t, "30", delegatorUbds[0].Entries[0].Balance.Amount.String()) delegatorReds := getRedelegations(t, port, addr, nil, nil) require.Len(t, delegatorReds, 1) - require.Equal(t, "30", delegatorReds[0].Balance.Amount.String()) + require.Len(t, delegatorReds[0].Entries, 1) + require.Equal(t, "30", delegatorReds[0].Entries[0].Balance.Amount.String()) validatorUbds := getValidatorUnbondingDelegations(t, port, operAddrs[0]) require.Len(t, validatorUbds, 1) - require.Equal(t, "30", validatorUbds[0].Balance.Amount.String()) + require.Len(t, validatorUbds[0].Entries, 1) + require.Equal(t, "30", validatorUbds[0].Entries[0].Balance.Amount.String()) validatorReds := getRedelegations(t, port, nil, operAddrs[0], nil) require.Len(t, validatorReds, 1) - require.Equal(t, "30", validatorReds[0].Balance.Amount.String()) + require.Len(t, validatorReds[0].Entries, 1) + require.Equal(t, "30", validatorReds[0].Entries[0].Balance.Amount.String()) // TODO Undonding status not currently implemented // require.Equal(t, sdk.Unbonding, bondedValidators[0].Status) diff --git a/client/lcd/test_helpers.go b/client/lcd/test_helpers.go index 428989ffdd83..799e9e9693ee 100644 --- a/client/lcd/test_helpers.go +++ b/client/lcd/test_helpers.go @@ -942,8 +942,13 @@ func getDelegation(t *testing.T, port string, delegatorAddr sdk.AccAddress, vali } // GET /staking/delegators/{delegatorAddr}/unbonding_delegations/{validatorAddr} Query all unbonding delegations between a delegator and a validator -func getUndelegation(t *testing.T, port string, delegatorAddr sdk.AccAddress, validatorAddr sdk.ValAddress) staking.UnbondingDelegation { - res, body := Request(t, port, "GET", fmt.Sprintf("/staking/delegators/%s/unbonding_delegations/%s", delegatorAddr, validatorAddr), nil) +func getUnbondingDelegation(t *testing.T, port string, delegatorAddr sdk.AccAddress, + validatorAddr sdk.ValAddress) staking.UnbondingDelegation { + + res, body := Request(t, port, "GET", + fmt.Sprintf("/staking/delegators/%s/unbonding_delegations/%s", + delegatorAddr, validatorAddr), nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) var unbond staking.UnbondingDelegation diff --git a/cmd/gaia/app/export.go b/cmd/gaia/app/export.go index 05e8e29a348c..18413041a5d4 100644 --- a/cmd/gaia/app/export.go +++ b/cmd/gaia/app/export.go @@ -116,14 +116,18 @@ func (app *GaiaApp) prepForZeroHeightGenesis(ctx sdk.Context) { // iterate through redelegations, reset creation height app.stakingKeeper.IterateRedelegations(ctx, func(_ int64, red staking.Redelegation) (stop bool) { - red.CreationHeight = 0 + for i := range red.Entries { + red.Entries[i].CreationHeight = 0 + } app.stakingKeeper.SetRedelegation(ctx, red) return false }) // iterate through unbonding delegations, reset creation height app.stakingKeeper.IterateUnbondingDelegations(ctx, func(_ int64, ubd staking.UnbondingDelegation) (stop bool) { - ubd.CreationHeight = 0 + for i := range ubd.Entries { + ubd.Entries[i].CreationHeight = 0 + } app.stakingKeeper.SetUnbondingDelegation(ctx, ubd) return false }) diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index c77ed5a2ed4e..1c8f080c1738 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -303,7 +303,8 @@ func TestGaiaCLICreateValidator(t *testing.T) { // Get unbonding delegations from the validator validatorUbds := f.QueryStakingUnbondingDelegationsFrom(barVal) require.Len(t, validatorUbds, 1) - require.Equal(t, "1", validatorUbds[0].Balance.Amount.String()) + require.Len(t, validatorUbds[0].Entries, 1) + require.Equal(t, "1", validatorUbds[0].Entries[0].Balance.Amount.String()) // Query staking parameters params := f.QueryStakingParameters() diff --git a/docs/spec/staking/state.md b/docs/spec/staking/state.md index 1336e1e4ca67..9cecf1c3616e 100644 --- a/docs/spec/staking/state.md +++ b/docs/spec/staking/state.md @@ -96,9 +96,9 @@ type Description struct { ### Delegation Delegations are identified by combining `DelegatorAddr` (the address of the delegator) -with the `OperatorAddr` Delegators are indexed in the store as follows: +with the `ValidatorAddr` Delegators are indexed in the store as follows: -- Delegation: ` 0x0A | DelegatorAddr | OperatorAddr -> amino(delegation)` +- Delegation: ` 0x0A | DelegatorAddr | ValidatorAddr -> amino(delegation)` Atom holders may delegate coins to validators; under this circumstance their funds are held in a `Delegation` data structure. It is owned by one @@ -107,26 +107,29 @@ the transaction is the owner of the bond. ```golang type Delegation struct { - Shares sdk.Dec // delegation shares received - Height int64 // last height bond updated + DelegatorAddr sdk.AccAddress + ValidatorAddr sdk.ValAddress + Shares sdk.Dec // delegation shares received } ``` ### UnbondingDelegation -Shares in a `Delegation` can be unbonded, but they must for some time exist as an `UnbondingDelegation`, where shares can be reduced if Byzantine behavior is detected. +Shares in a `Delegation` can be unbonded, but they must for some time exist as +an `UnbondingDelegation`, where shares can be reduced if Byzantine behavior is +detected. `UnbondingDelegation` are indexed in the store as: -- UnbondingDelegationByDelegator: ` 0x0B | DelegatorAddr | OperatorAddr -> +- UnbondingDelegationByDelegator: ` 0x0B | DelegatorAddr | ValidatorAddr -> amino(unbondingDelegation)` -- UnbondingDelegationByValOwner: ` 0x0C | OperatorAddr | DelegatorAddr | OperatorAddr -> +- UnbondingDelegationByValOwner: ` 0x0C | ValidatorAddr | DelegatorAddr | ValidatorAddr -> nil` - The first map here is used in queries, to lookup all unbonding delegations for - a given delegator, while the second map is used in slashing, to lookup all - unbonding delegations associated with a given validator that need to be - slashed. +The first map here is used in queries, to lookup all unbonding delegations for +a given delegator, while the second map is used in slashing, to lookup all +unbonding delegations associated with a given validator that need to be +slashed. A UnbondingDelegation object is created every time an unbonding is initiated. The unbond must be completed with a second transaction provided by the @@ -134,8 +137,16 @@ delegation owner after the unbonding period has passed. ```golang type UnbondingDelegation struct { - Tokens sdk.Coins // the value in Atoms of the amount of shares which are unbonding - CompleteTime int64 // unix time to complete redelegation + DelegatorAddr sdk.AccAddress // delegator + ValidatorAddr sdk.ValAddress // validator unbonding from operator addr + Entries []UnbondingDelegationEntry // unbonding delegation entries +} + +type UnbondingDelegationEntry struct { + CreationHeight int64 // height which the unbonding took place + CompletionTime time.Time // unix time for unbonding completion + InitialBalance sdk.Coin // atoms initially scheduled to receive at completion + Balance sdk.Coin // atoms to receive at completion } ``` @@ -143,20 +154,20 @@ type UnbondingDelegation struct { Shares in a `Delegation` can be rebonded to a different validator, but they must for some time exist as a `Redelegation`, where shares can be reduced if Byzantine -behavior is detected. This is tracked as moving a delegation from a `FromOperatorAddr` -to a `ToOperatorAddr`. +behavior is detected. This is tracked as moving a delegation from a `ValidatorSrcAddr` +to a `ValidatorDstAddr`. `Redelegation` are indexed in the store as: - - Redelegations: `0x0D | DelegatorAddr | FromOperatorAddr | ToOperatorAddr -> + - Redelegations: `0x0D | DelegatorAddr | ValidatorSrcAddr | ValidatorDstAddr -> amino(redelegation)` - - RedelegationsBySrc: `0x0E | FromOperatorAddr | ToOperatorAddr | + - RedelegationsBySrc: `0x0E | ValidatorSrcAddr | ValidatorDstAddr | DelegatorAddr -> nil` - - RedelegationsByDst: `0x0F | ToOperatorAddr | FromOperatorAddr | DelegatorAddr + - RedelegationsByDst: `0x0F | ValidatorDstAddr | ValidatorSrcAddr | DelegatorAddr -> nil` The first map here is used for queries, to lookup all redelegations for a given -delegator. The second map is used for slashing based on the `FromOperatorAddr`, +delegator. The second map is used for slashing based on the `ValidatorSrcAddr`, while the third map is for slashing based on the ToValOwnerAddr. A redelegation object is created every time a redelegation occurs. The @@ -167,8 +178,18 @@ the original redelegation has been completed. ```golang type Redelegation struct { - SourceShares sdk.Dec // amount of source shares redelegating - DestinationShares sdk.Dec // amount of destination shares created at redelegation - CompleteTime int64 // unix time to complete redelegation + DelegatorAddr sdk.AccAddress // delegator + ValidatorSrcAddr sdk.ValAddress // validator redelegation source operator addr + ValidatorDstAddr sdk.ValAddress // validator redelegation destination operator addr + Entries []RedelegationEntry // redelegation entries +} + +type RedelegationEntry struct { + CreationHeight int64 // height which the redelegation took place + CompletionTime time.Time // unix time for redelegation completion + InitialBalance sdk.Coin // initial balance when redelegation started + Balance sdk.Coin // current balance (current value held in destination validator) + SharesSrc sdk.Dec // amount of source-validator shares removed by redelegation + SharesDst sdk.Dec // amount of destination-validator shares created by redelegation } ``` diff --git a/x/staking/genesis.go b/x/staking/genesis.go index 62ee49efd449..736513db29a8 100644 --- a/x/staking/genesis.go +++ b/x/staking/genesis.go @@ -2,7 +2,6 @@ package staking import ( "fmt" - "sort" abci "github.com/tendermint/tendermint/abci/types" tmtypes "github.com/tendermint/tendermint/types" @@ -18,9 +17,11 @@ import ( // Returns final validator set after applying all declaration and delegations func InitGenesis(ctx sdk.Context, keeper Keeper, data types.GenesisState) (res []abci.ValidatorUpdate, err error) { - // We need to pretend to be "n blocks before genesis", where "n" is the validator update delay, - // so that e.g. slashing periods are correctly initialized for the validator set - // e.g. with a one-block offset - the first TM block is at height 1, so state updates applied from genesis.json are in block 0. + // We need to pretend to be "n blocks before genesis", where "n" is the + // validator update delay, so that e.g. slashing periods are correctly + // initialized for the validator set e.g. with a one-block offset - the + // first TM block is at height 1, so state updates applied from + // genesis.json are in block 0. ctx = ctx.WithBlockHeight(1 - types.ValidatorUpdateDelay) keeper.SetPool(ctx, data.Pool) @@ -46,20 +47,18 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data types.GenesisState) (res [ keeper.SetDelegation(ctx, delegation) } - sort.SliceStable(data.UnbondingDelegations[:], func(i, j int) bool { - return data.UnbondingDelegations[i].CreationHeight < data.UnbondingDelegations[j].CreationHeight - }) for _, ubd := range data.UnbondingDelegations { keeper.SetUnbondingDelegation(ctx, ubd) - keeper.InsertUnbondingQueue(ctx, ubd) + for _, entry := range ubd.Entries { + keeper.InsertUBDQueue(ctx, ubd, entry.CompletionTime) + } } - sort.SliceStable(data.Redelegations[:], func(i, j int) bool { - return data.Redelegations[i].CreationHeight < data.Redelegations[j].CreationHeight - }) for _, red := range data.Redelegations { keeper.SetRedelegation(ctx, red) - keeper.InsertRedelegationQueue(ctx, red) + for _, entry := range red.Entries { + keeper.InsertRedelegationQueue(ctx, red, entry.CompletionTime) + } } // don't need to run Tendermint updates if we exported diff --git a/x/staking/handler.go b/x/staking/handler.go index a4c856f6f4c0..40e8ea839521 100644 --- a/x/staking/handler.go +++ b/x/staking/handler.go @@ -53,7 +53,7 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) ([]abci.ValidatorUpdate, sdk.T k.UnbondAllMatureValidatorQueue(ctx) // Remove all mature unbonding delegations from the ubd queue. - matureUnbonds := k.DequeueAllMatureUnbondingQueue(ctx, ctx.BlockHeader().Time) + matureUnbonds := k.DequeueAllMatureUBDQueue(ctx, ctx.BlockHeader().Time) for _, dvPair := range matureUnbonds { err := k.CompleteUnbonding(ctx, dvPair.DelegatorAddr, dvPair.ValidatorAddr) if err != nil { @@ -70,7 +70,8 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) ([]abci.ValidatorUpdate, sdk.T // Remove all mature redelegations from the red queue. matureRedelegations := k.DequeueAllMatureRedelegationQueue(ctx, ctx.BlockHeader().Time) for _, dvvTriplet := range matureRedelegations { - err := k.CompleteRedelegation(ctx, dvvTriplet.DelegatorAddr, dvvTriplet.ValidatorSrcAddr, dvvTriplet.ValidatorDstAddr) + err := k.CompleteRedelegation(ctx, dvvTriplet.DelegatorAddr, + dvvTriplet.ValidatorSrcAddr, dvvTriplet.ValidatorDstAddr) if err != nil { continue } @@ -216,34 +217,34 @@ func handleMsgDelegate(ctx sdk.Context, msg types.MsgDelegate, k keeper.Keeper) } func handleMsgBeginUnbonding(ctx sdk.Context, msg types.MsgBeginUnbonding, k keeper.Keeper) sdk.Result { - ubd, err := k.BeginUnbonding(ctx, msg.DelegatorAddr, msg.ValidatorAddr, msg.SharesAmount) + completionTime, err := k.BeginUnbonding(ctx, msg.DelegatorAddr, msg.ValidatorAddr, msg.SharesAmount) if err != nil { return err.Result() } - finishTime := types.MsgCdc.MustMarshalBinaryLengthPrefixed(ubd.MinTime) + finishTime := types.MsgCdc.MustMarshalBinaryLengthPrefixed(completionTime) tags := sdk.NewTags( tags.Delegator, []byte(msg.DelegatorAddr.String()), tags.SrcValidator, []byte(msg.ValidatorAddr.String()), - tags.EndTime, []byte(ubd.MinTime.Format(time.RFC3339)), + tags.EndTime, []byte(completionTime.Format(time.RFC3339)), ) return sdk.Result{Data: finishTime, Tags: tags} } func handleMsgBeginRedelegate(ctx sdk.Context, msg types.MsgBeginRedelegate, k keeper.Keeper) sdk.Result { - red, err := k.BeginRedelegation(ctx, msg.DelegatorAddr, msg.ValidatorSrcAddr, + completionTime, err := k.BeginRedelegation(ctx, msg.DelegatorAddr, msg.ValidatorSrcAddr, msg.ValidatorDstAddr, msg.SharesAmount) if err != nil { return err.Result() } - finishTime := types.MsgCdc.MustMarshalBinaryLengthPrefixed(red.MinTime) + finishTime := types.MsgCdc.MustMarshalBinaryLengthPrefixed(completionTime) resTags := sdk.NewTags( tags.Delegator, []byte(msg.DelegatorAddr.String()), tags.SrcValidator, []byte(msg.ValidatorSrcAddr.String()), tags.DstValidator, []byte(msg.ValidatorDstAddr.String()), - tags.EndTime, []byte(red.MinTime.Format(time.RFC3339)), + tags.EndTime, []byte(completionTime.Format(time.RFC3339)), ) return sdk.Result{Data: finishTime, Tags: resTags} diff --git a/x/staking/handler_test.go b/x/staking/handler_test.go index 96f804fb4abc..16a763be75b5 100644 --- a/x/staking/handler_test.go +++ b/x/staking/handler_test.go @@ -416,6 +416,7 @@ func TestIncrementsMsgUnbond(t *testing.T) { msgBeginUnbonding := NewMsgBeginUnbonding(delegatorAddr, validatorAddr, unbondShares) numUnbonds := 5 for i := 0; i < numUnbonds; i++ { + got := handleMsgBeginUnbonding(ctx, msgBeginUnbonding, keeper) require.True(t, got.IsOK(), "expected msg %d to be ok, got %v", i, got) var finishTime time.Time @@ -423,7 +424,7 @@ func TestIncrementsMsgUnbond(t *testing.T) { ctx = ctx.WithBlockTime(finishTime) EndBlocker(ctx, keeper) - //Check that the accounts and the bond account have the appropriate values + // check that the accounts and the bond account have the appropriate values validator, found = keeper.GetValidator(ctx, validatorAddr) require.True(t, found) bond, found := keeper.GetDelegation(ctx, delegatorAddr, validatorAddr) @@ -849,46 +850,205 @@ func TestTransitiveRedelegation(t *testing.T) { require.True(t, got.IsOK(), "expected no error") } -func TestConflictingRedelegation(t *testing.T) { +func TestMultipleRedelegationAtSameTime(t *testing.T) { ctx, _, keeper := keep.CreateTestInput(t, false, 1000) - validatorAddr := sdk.ValAddress(keep.Addrs[0]) - validatorAddr2 := sdk.ValAddress(keep.Addrs[1]) + valAddr := sdk.ValAddress(keep.Addrs[0]) + valAddr2 := sdk.ValAddress(keep.Addrs[1]) // set the unbonding time params := keeper.GetParams(ctx) - params.UnbondingTime = 1 + params.UnbondingTime = 1 * time.Second keeper.SetParams(ctx, params) // create the validators - msgCreateValidator := NewTestMsgCreateValidator(validatorAddr, keep.PKs[0], 10) + msgCreateValidator := NewTestMsgCreateValidator(valAddr, keep.PKs[0], 10) got := handleMsgCreateValidator(ctx, msgCreateValidator, keeper) require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator") - msgCreateValidator = NewTestMsgCreateValidator(validatorAddr2, keep.PKs[1], 10) + msgCreateValidator = NewTestMsgCreateValidator(valAddr2, keep.PKs[1], 10) got = handleMsgCreateValidator(ctx, msgCreateValidator, keeper) require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator") // end block to bond them EndBlocker(ctx, keeper) - // begin redelegate - msgBeginRedelegate := NewMsgBeginRedelegate(sdk.AccAddress(validatorAddr), validatorAddr, validatorAddr2, sdk.NewDec(5)) + // begin a redelegate + selfDelAddr := sdk.AccAddress(valAddr) // (the validator is it's own delegator) + msgBeginRedelegate := NewMsgBeginRedelegate(selfDelAddr, + valAddr, valAddr2, sdk.NewDec(5)) got = handleMsgBeginRedelegate(ctx, msgBeginRedelegate, keeper) require.True(t, got.IsOK(), "expected no error, %v", got) - // cannot redelegate again while first redelegation still exists + // there should only be one entry in the redelegation object + rd, found := keeper.GetRedelegation(ctx, selfDelAddr, valAddr, valAddr2) + require.True(t, found) + require.Len(t, rd.Entries, 1) + + // start a second redelegation at this same time as the first got = handleMsgBeginRedelegate(ctx, msgBeginRedelegate, keeper) - require.True(t, !got.IsOK(), "expected an error, msg: %v", msgBeginRedelegate) + require.True(t, got.IsOK(), "expected no error, msg: %v", msgBeginRedelegate) - // progress forward in time - ctx = ctx.WithBlockTime(ctx.BlockHeader().Time.Add(10 * time.Second)) + // now there should be two entries + rd, found = keeper.GetRedelegation(ctx, selfDelAddr, valAddr, valAddr2) + require.True(t, found) + require.Len(t, rd.Entries, 2) - // complete first redelegation + // move forward in time, should complete both redelegations + ctx = ctx.WithBlockTime(ctx.BlockHeader().Time.Add(1 * time.Second)) EndBlocker(ctx, keeper) - // now should be able to redelegate again + rd, found = keeper.GetRedelegation(ctx, selfDelAddr, valAddr, valAddr2) + require.False(t, found) +} + +func TestMultipleRedelegationAtUniqueTimes(t *testing.T) { + ctx, _, keeper := keep.CreateTestInput(t, false, 1000) + valAddr := sdk.ValAddress(keep.Addrs[0]) + valAddr2 := sdk.ValAddress(keep.Addrs[1]) + + // set the unbonding time + params := keeper.GetParams(ctx) + params.UnbondingTime = 10 * time.Second + keeper.SetParams(ctx, params) + + // create the validators + msgCreateValidator := NewTestMsgCreateValidator(valAddr, keep.PKs[0], 10) + got := handleMsgCreateValidator(ctx, msgCreateValidator, keeper) + require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator") + + msgCreateValidator = NewTestMsgCreateValidator(valAddr2, keep.PKs[1], 10) + got = handleMsgCreateValidator(ctx, msgCreateValidator, keeper) + require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator") + + // end block to bond them + EndBlocker(ctx, keeper) + + // begin a redelegate + selfDelAddr := sdk.AccAddress(valAddr) // (the validator is it's own delegator) + msgBeginRedelegate := NewMsgBeginRedelegate(selfDelAddr, + valAddr, valAddr2, sdk.NewDec(5)) got = handleMsgBeginRedelegate(ctx, msgBeginRedelegate, keeper) - require.True(t, got.IsOK(), "expected no error") + require.True(t, got.IsOK(), "expected no error, %v", got) + + // move forward in time and start a second redelegation + ctx = ctx.WithBlockTime(ctx.BlockHeader().Time.Add(5 * time.Second)) + got = handleMsgBeginRedelegate(ctx, msgBeginRedelegate, keeper) + require.True(t, got.IsOK(), "expected no error, msg: %v", msgBeginRedelegate) + + // now there should be two entries + rd, found := keeper.GetRedelegation(ctx, selfDelAddr, valAddr, valAddr2) + require.True(t, found) + require.Len(t, rd.Entries, 2) + + // move forward in time, should complete the first redelegation, but not the second + ctx = ctx.WithBlockTime(ctx.BlockHeader().Time.Add(5 * time.Second)) + EndBlocker(ctx, keeper) + rd, found = keeper.GetRedelegation(ctx, selfDelAddr, valAddr, valAddr2) + require.True(t, found) + require.Len(t, rd.Entries, 1) + + // move forward in time, should complete the second redelegation + ctx = ctx.WithBlockTime(ctx.BlockHeader().Time.Add(5 * time.Second)) + EndBlocker(ctx, keeper) + rd, found = keeper.GetRedelegation(ctx, selfDelAddr, valAddr, valAddr2) + require.False(t, found) +} + +func TestMultipleUnbondingDelegationAtSameTime(t *testing.T) { + ctx, _, keeper := keep.CreateTestInput(t, false, 1000) + valAddr := sdk.ValAddress(keep.Addrs[0]) + + // set the unbonding time + params := keeper.GetParams(ctx) + params.UnbondingTime = 1 * time.Second + keeper.SetParams(ctx, params) + + // create the validator + msgCreateValidator := NewTestMsgCreateValidator(valAddr, keep.PKs[0], 10) + got := handleMsgCreateValidator(ctx, msgCreateValidator, keeper) + require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator") + + // end block to bond + EndBlocker(ctx, keeper) + + // begin an unbonding delegation + selfDelAddr := sdk.AccAddress(valAddr) // (the validator is it's own delegator) + msgBeginUnbonding := NewMsgBeginUnbonding(selfDelAddr, valAddr, sdk.NewDec(5)) + got = handleMsgBeginUnbonding(ctx, msgBeginUnbonding, keeper) + require.True(t, got.IsOK(), "expected no error, %v", got) + + // there should only be one entry in the ubd object + ubd, found := keeper.GetUnbondingDelegation(ctx, selfDelAddr, valAddr) + require.True(t, found) + require.Len(t, ubd.Entries, 1) + + // start a second ubd at this same time as the first + got = handleMsgBeginUnbonding(ctx, msgBeginUnbonding, keeper) + require.True(t, got.IsOK(), "expected no error, msg: %v", msgBeginUnbonding) + + // now there should be two entries + ubd, found = keeper.GetUnbondingDelegation(ctx, selfDelAddr, valAddr) + require.True(t, found) + require.Len(t, ubd.Entries, 2) + + // move forwaubd in time, should complete both ubds + ctx = ctx.WithBlockTime(ctx.BlockHeader().Time.Add(1 * time.Second)) + EndBlocker(ctx, keeper) + + ubd, found = keeper.GetUnbondingDelegation(ctx, selfDelAddr, valAddr) + require.False(t, found) +} + +func TestMultipleUnbondingDelegationAtUniqueTimes(t *testing.T) { + ctx, _, keeper := keep.CreateTestInput(t, false, 1000) + valAddr := sdk.ValAddress(keep.Addrs[0]) + + // set the unbonding time + params := keeper.GetParams(ctx) + params.UnbondingTime = 10 * time.Second + keeper.SetParams(ctx, params) + + // create the validator + msgCreateValidator := NewTestMsgCreateValidator(valAddr, keep.PKs[0], 10) + got := handleMsgCreateValidator(ctx, msgCreateValidator, keeper) + require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator") + + // end block to bond + EndBlocker(ctx, keeper) + + // begin an unbonding delegation + selfDelAddr := sdk.AccAddress(valAddr) // (the validator is it's own delegator) + msgBeginUnbonding := NewMsgBeginUnbonding(selfDelAddr, valAddr, sdk.NewDec(5)) + got = handleMsgBeginUnbonding(ctx, msgBeginUnbonding, keeper) + require.True(t, got.IsOK(), "expected no error, %v", got) + + // there should only be one entry in the ubd object + ubd, found := keeper.GetUnbondingDelegation(ctx, selfDelAddr, valAddr) + require.True(t, found) + require.Len(t, ubd.Entries, 1) + + // move forwaubd in time and start a second redelegation + ctx = ctx.WithBlockTime(ctx.BlockHeader().Time.Add(5 * time.Second)) + got = handleMsgBeginUnbonding(ctx, msgBeginUnbonding, keeper) + require.True(t, got.IsOK(), "expected no error, msg: %v", msgBeginUnbonding) + + // now there should be two entries + ubd, found = keeper.GetUnbondingDelegation(ctx, selfDelAddr, valAddr) + require.True(t, found) + require.Len(t, ubd.Entries, 2) + + // move forwaubd in time, should complete the first redelegation, but not the second + ctx = ctx.WithBlockTime(ctx.BlockHeader().Time.Add(5 * time.Second)) + EndBlocker(ctx, keeper) + ubd, found = keeper.GetUnbondingDelegation(ctx, selfDelAddr, valAddr) + require.True(t, found) + require.Len(t, ubd.Entries, 1) + + // move forwaubd in time, should complete the second redelegation + ctx = ctx.WithBlockTime(ctx.BlockHeader().Time.Add(5 * time.Second)) + EndBlocker(ctx, keeper) + ubd, found = keeper.GetUnbondingDelegation(ctx, selfDelAddr, valAddr) + require.False(t, found) } func TestUnbondingWhenExcessValidators(t *testing.T) { @@ -991,14 +1151,16 @@ func TestBondUnbondRedelegateSlashTwice(t *testing.T) { keeper.Slash(ctx, consAddr0, 0, 20, sdk.NewDecWithPrec(5, 1)) // unbonding delegation should have been slashed by half - unbonding, found := keeper.GetUnbondingDelegation(ctx, del, valA) + ubd, found := keeper.GetUnbondingDelegation(ctx, del, valA) require.True(t, found) - require.Equal(t, int64(2), unbonding.Balance.Amount.Int64()) + require.Len(t, ubd.Entries, 1) + require.Equal(t, int64(2), ubd.Entries[0].Balance.Amount.Int64()) // redelegation should have been slashed by half redelegation, found := keeper.GetRedelegation(ctx, del, valA, valB) require.True(t, found) - require.Equal(t, int64(3), redelegation.Balance.Amount.Int64()) + require.Len(t, redelegation.Entries, 1) + require.Equal(t, int64(3), redelegation.Entries[0].Balance.Amount.Int64()) // destination delegation should have been slashed by half delegation, found = keeper.GetDelegation(ctx, del, valB) @@ -1015,14 +1177,16 @@ func TestBondUnbondRedelegateSlashTwice(t *testing.T) { keeper.Slash(ctx, consAddr0, 2, 10, sdk.NewDecWithPrec(5, 1)) // unbonding delegation should be unchanged - unbonding, found = keeper.GetUnbondingDelegation(ctx, del, valA) + ubd, found = keeper.GetUnbondingDelegation(ctx, del, valA) require.True(t, found) - require.Equal(t, int64(2), unbonding.Balance.Amount.Int64()) + require.Len(t, ubd.Entries, 1) + require.Equal(t, int64(2), ubd.Entries[0].Balance.Amount.Int64()) // redelegation should be unchanged redelegation, found = keeper.GetRedelegation(ctx, del, valA, valB) require.True(t, found) - require.Equal(t, int64(3), redelegation.Balance.Amount.Int64()) + require.Len(t, redelegation.Entries, 1) + require.Equal(t, int64(3), redelegation.Entries[0].Balance.Amount.Int64()) // destination delegation should be unchanged delegation, found = keeper.GetDelegation(ctx, del, valB) diff --git a/x/staking/keeper/delegation.go b/x/staking/keeper/delegation.go index 7efb8b7048e7..e4c61eb65d07 100644 --- a/x/staking/keeper/delegation.go +++ b/x/staking/keeper/delegation.go @@ -170,9 +170,28 @@ func (k Keeper) RemoveUnbondingDelegation(ctx sdk.Context, ubd types.UnbondingDe store.Delete(GetUBDByValIndexKey(ubd.DelegatorAddr, ubd.ValidatorAddr)) } -// gets a specific unbonding queue timeslice. A timeslice is a slice of DVPairs corresponding to unbonding delegations -// that expire at a certain time. -func (k Keeper) GetUnbondingQueueTimeSlice(ctx sdk.Context, timestamp time.Time) (dvPairs []types.DVPair) { +// SetUnbondingDelegationEntry adds an entry to the unbonding delegation at +// the given addresses. It creates the unbonding delegation if it does not exist +func (k Keeper) SetUnbondingDelegationEntry(ctx sdk.Context, + delegatorAddr sdk.AccAddress, validatorAddr sdk.ValAddress, + creationHeight int64, minTime time.Time, balance sdk.Coin) types.UnbondingDelegation { + + ubd, found := k.GetUnbondingDelegation(ctx, delegatorAddr, validatorAddr) + if found { + ubd.AddEntry(creationHeight, minTime, balance) + } else { + ubd = types.NewUnbondingDelegation(delegatorAddr, validatorAddr, creationHeight, minTime, balance) + } + k.SetUnbondingDelegation(ctx, ubd) + return ubd +} + +//________________________________________________ +// unbonding delegation queue timeslice operations + +// gets a specific unbonding queue timeslice. A timeslice is a slice of DVPairs +// corresponding to unbonding delegations that expire at a certain time. +func (k Keeper) GetUBDQueueTimeSlice(ctx sdk.Context, timestamp time.Time) (dvPairs []types.DVPair) { store := ctx.KVStore(k.storeKey) bz := store.Get(GetUnbondingDelegationTimeKey(timestamp)) if bz == nil { @@ -183,38 +202,45 @@ func (k Keeper) GetUnbondingQueueTimeSlice(ctx sdk.Context, timestamp time.Time) } // Sets a specific unbonding queue timeslice. -func (k Keeper) SetUnbondingQueueTimeSlice(ctx sdk.Context, timestamp time.Time, keys []types.DVPair) { +func (k Keeper) SetUBDQueueTimeSlice(ctx sdk.Context, timestamp time.Time, keys []types.DVPair) { store := ctx.KVStore(k.storeKey) bz := k.cdc.MustMarshalBinaryLengthPrefixed(keys) store.Set(GetUnbondingDelegationTimeKey(timestamp), bz) } // Insert an unbonding delegation to the appropriate timeslice in the unbonding queue -func (k Keeper) InsertUnbondingQueue(ctx sdk.Context, ubd types.UnbondingDelegation) { - timeSlice := k.GetUnbondingQueueTimeSlice(ctx, ubd.MinTime) +func (k Keeper) InsertUBDQueue(ctx sdk.Context, ubd types.UnbondingDelegation, + completionTime time.Time) { + + timeSlice := k.GetUBDQueueTimeSlice(ctx, completionTime) dvPair := types.DVPair{ubd.DelegatorAddr, ubd.ValidatorAddr} if len(timeSlice) == 0 { - k.SetUnbondingQueueTimeSlice(ctx, ubd.MinTime, []types.DVPair{dvPair}) + k.SetUBDQueueTimeSlice(ctx, completionTime, []types.DVPair{dvPair}) } else { timeSlice = append(timeSlice, dvPair) - k.SetUnbondingQueueTimeSlice(ctx, ubd.MinTime, timeSlice) + k.SetUBDQueueTimeSlice(ctx, completionTime, timeSlice) } } // Returns all the unbonding queue timeslices from time 0 until endTime -func (k Keeper) UnbondingQueueIterator(ctx sdk.Context, endTime time.Time) sdk.Iterator { +func (k Keeper) UBDQueueIterator(ctx sdk.Context, endTime time.Time) sdk.Iterator { store := ctx.KVStore(k.storeKey) - return store.Iterator(UnbondingQueueKey, sdk.InclusiveEndBytes(GetUnbondingDelegationTimeKey(endTime))) + return store.Iterator(UnbondingQueueKey, + sdk.InclusiveEndBytes(GetUnbondingDelegationTimeKey(endTime))) } -// Returns a concatenated list of all the timeslices before currTime, and deletes the timeslices from the queue -func (k Keeper) DequeueAllMatureUnbondingQueue(ctx sdk.Context, currTime time.Time) (matureUnbonds []types.DVPair) { +// Returns a concatenated list of all the timeslices inclusively previous to +// currTime, and deletes the timeslices from the queue +func (k Keeper) DequeueAllMatureUBDQueue(ctx sdk.Context, + currTime time.Time) (matureUnbonds []types.DVPair) { + store := ctx.KVStore(k.storeKey) // gets an iterator for all timeslices from time 0 until the current Blockheader time - unbondingTimesliceIterator := k.UnbondingQueueIterator(ctx, ctx.BlockHeader().Time) + unbondingTimesliceIterator := k.UBDQueueIterator(ctx, ctx.BlockHeader().Time) for ; unbondingTimesliceIterator.Valid(); unbondingTimesliceIterator.Next() { timeslice := []types.DVPair{} - k.cdc.MustUnmarshalBinaryLengthPrefixed(unbondingTimesliceIterator.Value(), ×lice) + value := unbondingTimesliceIterator.Value() + k.cdc.MustUnmarshalBinaryLengthPrefixed(value, ×lice) matureUnbonds = append(matureUnbonds, timeslice...) store.Delete(unbondingTimesliceIterator.Key()) } @@ -298,6 +324,26 @@ func (k Keeper) SetRedelegation(ctx sdk.Context, red types.Redelegation) { store.Set(GetREDByValDstIndexKey(red.DelegatorAddr, red.ValidatorSrcAddr, red.ValidatorDstAddr), []byte{}) } +// SetUnbondingDelegationEntry adds an entry to the unbonding delegation at +// the given addresses. It creates the unbonding delegation if it does not exist +func (k Keeper) SetRedelegationEntry(ctx sdk.Context, + delegatorAddr sdk.AccAddress, validatorSrcAddr, + validatorDstAddr sdk.ValAddress, creationHeight int64, + minTime time.Time, balance sdk.Coin, + sharesSrc, sharesDst sdk.Dec) types.Redelegation { + + red, found := k.GetRedelegation(ctx, delegatorAddr, validatorSrcAddr, validatorDstAddr) + if found { + red.AddEntry(creationHeight, minTime, balance, sharesSrc, sharesDst) + } else { + red = types.NewRedelegation(delegatorAddr, validatorSrcAddr, + validatorDstAddr, creationHeight, minTime, balance, sharesSrc, + sharesDst) + } + k.SetRedelegation(ctx, red) + return red +} + // iterate through all redelegations func (k Keeper) IterateRedelegations(ctx sdk.Context, fn func(index int64, red types.Redelegation) (stop bool)) { store := ctx.KVStore(k.storeKey) @@ -322,6 +368,9 @@ func (k Keeper) RemoveRedelegation(ctx sdk.Context, red types.Redelegation) { store.Delete(GetREDByValDstIndexKey(red.DelegatorAddr, red.ValidatorSrcAddr, red.ValidatorDstAddr)) } +//________________________________________________ +// redelegation queue timeslice operations + // Gets a specific redelegation queue timeslice. A timeslice is a slice of DVVTriplets corresponding to redelegations // that expire at a certain time. func (k Keeper) GetRedelegationQueueTimeSlice(ctx sdk.Context, timestamp time.Time) (dvvTriplets []types.DVVTriplet) { @@ -342,14 +391,16 @@ func (k Keeper) SetRedelegationQueueTimeSlice(ctx sdk.Context, timestamp time.Ti } // Insert an redelegation delegation to the appropriate timeslice in the redelegation queue -func (k Keeper) InsertRedelegationQueue(ctx sdk.Context, red types.Redelegation) { - timeSlice := k.GetRedelegationQueueTimeSlice(ctx, red.MinTime) +func (k Keeper) InsertRedelegationQueue(ctx sdk.Context, red types.Redelegation, + completionTime time.Time) { + + timeSlice := k.GetRedelegationQueueTimeSlice(ctx, completionTime) dvvTriplet := types.DVVTriplet{red.DelegatorAddr, red.ValidatorSrcAddr, red.ValidatorDstAddr} if len(timeSlice) == 0 { - k.SetRedelegationQueueTimeSlice(ctx, red.MinTime, []types.DVVTriplet{dvvTriplet}) + k.SetRedelegationQueueTimeSlice(ctx, completionTime, []types.DVVTriplet{dvvTriplet}) } else { timeSlice = append(timeSlice, dvvTriplet) - k.SetRedelegationQueueTimeSlice(ctx, red.MinTime, timeSlice) + k.SetRedelegationQueueTimeSlice(ctx, completionTime, timeSlice) } } @@ -359,14 +410,16 @@ func (k Keeper) RedelegationQueueIterator(ctx sdk.Context, endTime time.Time) sd return store.Iterator(RedelegationQueueKey, sdk.InclusiveEndBytes(GetRedelegationTimeKey(endTime))) } -// Returns a concatenated list of all the timeslices before currTime, and deletes the timeslices from the queue +// Returns a concatenated list of all the timeslices inclusively previous to +// currTime, and deletes the timeslices from the queue func (k Keeper) DequeueAllMatureRedelegationQueue(ctx sdk.Context, currTime time.Time) (matureRedelegations []types.DVVTriplet) { store := ctx.KVStore(k.storeKey) // gets an iterator for all timeslices from time 0 until the current Blockheader time redelegationTimesliceIterator := k.RedelegationQueueIterator(ctx, ctx.BlockHeader().Time) for ; redelegationTimesliceIterator.Valid(); redelegationTimesliceIterator.Next() { timeslice := []types.DVVTriplet{} - k.cdc.MustUnmarshalBinaryLengthPrefixed(redelegationTimesliceIterator.Value(), ×lice) + value := redelegationTimesliceIterator.Value() + k.cdc.MustUnmarshalBinaryLengthPrefixed(value, ×lice) matureRedelegations = append(matureRedelegations, timeslice...) store.Delete(redelegationTimesliceIterator.Key()) } @@ -474,9 +527,9 @@ func (k Keeper) unbond(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValA //______________________________________________________________________________________________________ -// get info for begin functions: MinTime and CreationHeight +// get info for begin functions: completionTime and CreationHeight func (k Keeper) getBeginInfo(ctx sdk.Context, valSrcAddr sdk.ValAddress) ( - minTime time.Time, height int64, completeNow bool) { + completionTime time.Time, height int64, completeNow bool) { validator, found := k.GetValidator(ctx, valSrcAddr) @@ -484,17 +537,17 @@ func (k Keeper) getBeginInfo(ctx sdk.Context, valSrcAddr sdk.ValAddress) ( case !found || validator.Status == sdk.Bonded: // the longest wait - just unbonding period from now - minTime = ctx.BlockHeader().Time.Add(k.UnbondingTime(ctx)) + completionTime = ctx.BlockHeader().Time.Add(k.UnbondingTime(ctx)) height = ctx.BlockHeight() - return minTime, height, false + return completionTime, height, false case validator.Status == sdk.Unbonded: - return minTime, height, true + return completionTime, height, true case validator.Status == sdk.Unbonding: - minTime = validator.UnbondingMinTime + completionTime = validator.UnbondingMinTime height = validator.UnbondingHeight - return minTime, height, false + return completionTime, height, false default: panic("unknown validator status") @@ -502,21 +555,15 @@ func (k Keeper) getBeginInfo(ctx sdk.Context, valSrcAddr sdk.ValAddress) ( } // begin unbonding an unbonding record -func (k Keeper) BeginUnbonding(ctx sdk.Context, - delAddr sdk.AccAddress, valAddr sdk.ValAddress, sharesAmount sdk.Dec) (types.UnbondingDelegation, sdk.Error) { - - // TODO quick fix, instead we should use an index, see https://github.com/cosmos/cosmos-sdk/issues/1402 - _, found := k.GetUnbondingDelegation(ctx, delAddr, valAddr) - if found { - return types.UnbondingDelegation{}, types.ErrExistingUnbondingDelegation(k.Codespace()) - } +func (k Keeper) BeginUnbonding(ctx sdk.Context, delAddr sdk.AccAddress, + valAddr sdk.ValAddress, sharesAmount sdk.Dec) (completionTime time.Time, sdkErr sdk.Error) { // create the unbonding delegation - minTime, height, completeNow := k.getBeginInfo(ctx, valAddr) + completionTime, height, completeNow := k.getBeginInfo(ctx, valAddr) returnAmount, err := k.unbond(ctx, delAddr, valAddr, sharesAmount) if err != nil { - return types.UnbondingDelegation{}, err + return completionTime, err } balance := sdk.NewCoin(k.BondDenom(ctx), returnAmount) @@ -524,107 +571,103 @@ func (k Keeper) BeginUnbonding(ctx sdk.Context, if completeNow { _, err := k.bankKeeper.UndelegateCoins(ctx, delAddr, sdk.Coins{balance}) if err != nil { - return types.UnbondingDelegation{}, err + return completionTime, err } - - return types.UnbondingDelegation{MinTime: minTime}, nil + return completionTime, nil } - ubd := types.UnbondingDelegation{ - DelegatorAddr: delAddr, - ValidatorAddr: valAddr, - CreationHeight: height, - MinTime: minTime, - Balance: balance, - InitialBalance: balance, - } - k.SetUnbondingDelegation(ctx, ubd) - k.InsertUnbondingQueue(ctx, ubd) + ubd := k.SetUnbondingDelegationEntry(ctx, delAddr, + valAddr, height, completionTime, balance) - return ubd, nil + k.InsertUBDQueue(ctx, ubd, completionTime) + return completionTime, nil } -// complete unbonding an unbonding record -// CONTRACT: Expects unbonding passed in has finished the unbonding period -func (k Keeper) CompleteUnbonding(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) sdk.Error { +// CompleteUnbonding completes the unbonding of all mature entries in the +// retrieved unbonding delegation object. +func (k Keeper) CompleteUnbonding(ctx sdk.Context, delAddr sdk.AccAddress, + valAddr sdk.ValAddress) sdk.Error { ubd, found := k.GetUnbondingDelegation(ctx, delAddr, valAddr) if !found { return types.ErrNoUnbondingDelegation(k.Codespace()) } - _, _, err := k.bankKeeper.AddCoins(ctx, ubd.DelegatorAddr, sdk.Coins{ubd.Balance}) - if err != nil { - return err + ctxTime := ctx.BlockHeader().Time + + // loop through all the entries and complete unbonding mature entries + for i := 0; i < len(ubd.Entries); i++ { + entry := ubd.Entries[i] + if entry.IsMature(ctxTime) { + ubd.RemoveEntry(int64(i)) + i-- + + _, _, err := k.bankKeeper.AddCoins(ctx, ubd.DelegatorAddr, sdk.Coins{entry.Balance}) + if err != nil { + return err + } + } + } + + // set the unbonding delegation or remove it if there are no more entries + if len(ubd.Entries) == 0 { + k.RemoveUnbondingDelegation(ctx, ubd) + } else { + k.SetUnbondingDelegation(ctx, ubd) } - k.RemoveUnbondingDelegation(ctx, ubd) + return nil } // begin unbonding / redelegation; create a redelegation record func (k Keeper) BeginRedelegation(ctx sdk.Context, delAddr sdk.AccAddress, - valSrcAddr, valDstAddr sdk.ValAddress, sharesAmount sdk.Dec) (types.Redelegation, sdk.Error) { + valSrcAddr, valDstAddr sdk.ValAddress, sharesAmount sdk.Dec) ( + completionTime time.Time, errSdk sdk.Error) { if bytes.Equal(valSrcAddr, valDstAddr) { - return types.Redelegation{}, types.ErrSelfRedelegation(k.Codespace()) - } - - // check if there is already a redelgation in progress from src to dst - // TODO quick fix, instead we should use an index, see https://github.com/cosmos/cosmos-sdk/issues/1402 - _, found := k.GetRedelegation(ctx, delAddr, valSrcAddr, valDstAddr) - if found { - return types.Redelegation{}, types.ErrConflictingRedelegation(k.Codespace()) + return time.Time{}, types.ErrSelfRedelegation(k.Codespace()) } // check if this is a transitive redelegation if k.HasReceivingRedelegation(ctx, delAddr, valSrcAddr) { - return types.Redelegation{}, types.ErrTransitiveRedelegation(k.Codespace()) + return time.Time{}, types.ErrTransitiveRedelegation(k.Codespace()) } returnAmount, err := k.unbond(ctx, delAddr, valSrcAddr, sharesAmount) if err != nil { - return types.Redelegation{}, err + return time.Time{}, err } if returnAmount.IsZero() { - return types.Redelegation{}, types.ErrVerySmallRedelegation(k.Codespace()) + return time.Time{}, types.ErrVerySmallRedelegation(k.Codespace()) } returnCoin := sdk.NewCoin(k.BondDenom(ctx), returnAmount) dstValidator, found := k.GetValidator(ctx, valDstAddr) if !found { - return types.Redelegation{}, types.ErrBadRedelegationDst(k.Codespace()) + return time.Time{}, types.ErrBadRedelegationDst(k.Codespace()) } sharesCreated, err := k.Delegate(ctx, delAddr, returnCoin, dstValidator, false) if err != nil { - return types.Redelegation{}, err + return time.Time{}, err } // create the unbonding delegation - minTime, height, completeNow := k.getBeginInfo(ctx, valSrcAddr) + completionTime, height, completeNow := k.getBeginInfo(ctx, valSrcAddr) if completeNow { // no need to create the redelegation object - return types.Redelegation{MinTime: minTime}, nil + return completionTime, nil } - red := types.Redelegation{ - DelegatorAddr: delAddr, - ValidatorSrcAddr: valSrcAddr, - ValidatorDstAddr: valDstAddr, - CreationHeight: height, - MinTime: minTime, - SharesDst: sharesCreated, - SharesSrc: sharesAmount, - Balance: returnCoin, - InitialBalance: returnCoin, - } - k.SetRedelegation(ctx, red) - k.InsertRedelegationQueue(ctx, red) - return red, nil + red := k.SetRedelegationEntry(ctx, delAddr, valSrcAddr, valDstAddr, + height, completionTime, returnCoin, sharesAmount, sharesCreated) + k.InsertRedelegationQueue(ctx, red, completionTime) + return completionTime, nil } -// complete unbonding an ongoing redelegation +// CompleteRedelegation completes the unbonding of all mature entries in the +// retrieved unbonding delegation object. func (k Keeper) CompleteRedelegation(ctx sdk.Context, delAddr sdk.AccAddress, valSrcAddr, valDstAddr sdk.ValAddress) sdk.Error { @@ -633,12 +676,23 @@ func (k Keeper) CompleteRedelegation(ctx sdk.Context, delAddr sdk.AccAddress, return types.ErrNoRedelegation(k.Codespace()) } - // ensure that enough time has passed ctxTime := ctx.BlockHeader().Time - if red.MinTime.After(ctxTime) { - return types.ErrNotMature(k.Codespace(), "redelegation", "unit-time", red.MinTime, ctxTime) + + // loop through all the entries and complete mature redelegation entries + for i := 0; i < len(red.Entries); i++ { + entry := red.Entries[i] + if entry.IsMature(ctxTime) { + red.RemoveEntry(int64(i)) + i-- + } + } + + // set the redelegation or remove it if there are no more entries + if len(red.Entries) == 0 { + k.RemoveRedelegation(ctx, red) + } else { + k.SetRedelegation(ctx, red) } - k.RemoveRedelegation(ctx, red) return nil } diff --git a/x/staking/keeper/delegation_test.go b/x/staking/keeper/delegation_test.go index 7a00b3ee47ba..5c11a91adabf 100644 --- a/x/staking/keeper/delegation_test.go +++ b/x/staking/keeper/delegation_test.go @@ -137,13 +137,8 @@ func TestDelegation(t *testing.T) { func TestUnbondingDelegation(t *testing.T) { ctx, _, keeper := CreateTestInput(t, false, 0) - ubd := types.UnbondingDelegation{ - DelegatorAddr: addrDels[0], - ValidatorAddr: addrVals[0], - CreationHeight: 0, - MinTime: time.Unix(0, 0), - Balance: sdk.NewInt64Coin(types.DefaultBondDenom, 5), - } + ubd := types.NewUnbondingDelegation(addrDels[0], addrVals[0], 0, + time.Unix(0, 0), sdk.NewInt64Coin(types.DefaultBondDenom, 5)) // set and retrieve a record keeper.SetUnbondingDelegation(ctx, ubd) @@ -152,7 +147,7 @@ func TestUnbondingDelegation(t *testing.T) { require.True(t, ubd.Equal(resUnbond)) // modify a records, save, and retrieve - ubd.Balance = sdk.NewInt64Coin(types.DefaultBondDenom, 21) + ubd.Entries[0].Balance = sdk.NewInt64Coin(types.DefaultBondDenom, 21) keeper.SetUnbondingDelegation(ctx, ubd) resUnbonds := keeper.GetUnbondingDelegations(ctx, addrDels[0], 5) @@ -338,9 +333,10 @@ func TestUndelegateFromUnbondingValidator(t *testing.T) { // retrieve the unbonding delegation ubd, found := keeper.GetUnbondingDelegation(ctx, addrDels[0], addrVals[0]) require.True(t, found) - require.True(t, ubd.Balance.IsEqual(sdk.NewInt64Coin(params.BondDenom, 6))) - assert.Equal(t, blockHeight, ubd.CreationHeight) - assert.True(t, blockTime.Add(params.UnbondingTime).Equal(ubd.MinTime)) + require.Len(t, ubd.Entries, 1) + require.True(t, ubd.Entries[0].Balance.IsEqual(sdk.NewInt64Coin(params.BondDenom, 6))) + assert.Equal(t, blockHeight, ubd.Entries[0].CreationHeight) + assert.True(t, blockTime.Add(params.UnbondingTime).Equal(ubd.Entries[0].CompletionTime)) } func TestUndelegateFromUnbondedValidator(t *testing.T) { @@ -490,15 +486,9 @@ func TestUnbondingAllDelegationFromValidator(t *testing.T) { func TestGetRedelegationsFromValidator(t *testing.T) { ctx, _, keeper := CreateTestInput(t, false, 0) - rd := types.Redelegation{ - DelegatorAddr: addrDels[0], - ValidatorSrcAddr: addrVals[0], - ValidatorDstAddr: addrVals[1], - CreationHeight: 0, - MinTime: time.Unix(0, 0), - SharesSrc: sdk.NewDec(5), - SharesDst: sdk.NewDec(5), - } + rd := types.NewRedelegation(addrDels[0], addrVals[0], addrVals[1], 0, + time.Unix(0, 0), sdk.NewInt64Coin(types.DefaultBondDenom, 5), + sdk.NewDec(5), sdk.NewDec(5)) // set and retrieve a record keeper.SetRedelegation(ctx, rd) @@ -520,15 +510,9 @@ func TestGetRedelegationsFromValidator(t *testing.T) { func TestRedelegation(t *testing.T) { ctx, _, keeper := CreateTestInput(t, false, 0) - rd := types.Redelegation{ - DelegatorAddr: addrDels[0], - ValidatorSrcAddr: addrVals[0], - ValidatorDstAddr: addrVals[1], - CreationHeight: 0, - MinTime: time.Unix(0, 0), - SharesSrc: sdk.NewDec(5), - SharesDst: sdk.NewDec(5), - } + rd := types.NewRedelegation(addrDels[0], addrVals[0], addrVals[1], 0, + time.Unix(0, 0), sdk.NewInt64Coin(types.DefaultBondDenom, 5), + sdk.NewDec(5), sdk.NewDec(5)) // test shouldn't have and redelegations has := keeper.HasReceivingRedelegation(ctx, addrDels[0], addrVals[1]) @@ -556,8 +540,8 @@ func TestRedelegation(t *testing.T) { require.True(t, has) // modify a records, save, and retrieve - rd.SharesSrc = sdk.NewDec(21) - rd.SharesDst = sdk.NewDec(21) + rd.Entries[0].SharesSrc = sdk.NewDec(21) + rd.Entries[0].SharesDst = sdk.NewDec(21) keeper.SetRedelegation(ctx, rd) resRed, found = keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) @@ -742,9 +726,10 @@ func TestRedelegateFromUnbondingValidator(t *testing.T) { // retrieve the unbonding delegation ubd, found := keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) require.True(t, found) - require.True(t, ubd.Balance.IsEqual(sdk.NewInt64Coin(params.BondDenom, 6))) - assert.Equal(t, blockHeight, ubd.CreationHeight) - assert.True(t, blockTime.Add(params.UnbondingTime).Equal(ubd.MinTime)) + require.Len(t, ubd.Entries, 1) + require.True(t, ubd.Entries[0].Balance.IsEqual(sdk.NewInt64Coin(params.BondDenom, 6))) + assert.Equal(t, blockHeight, ubd.Entries[0].CreationHeight) + assert.True(t, blockTime.Add(params.UnbondingTime).Equal(ubd.Entries[0].CompletionTime)) } func TestRedelegateFromUnbondedValidator(t *testing.T) { diff --git a/x/staking/keeper/slash.go b/x/staking/keeper/slash.go index afe58742c0fa..3c698382e71a 100644 --- a/x/staking/keeper/slash.go +++ b/x/staking/keeper/slash.go @@ -145,34 +145,41 @@ func (k Keeper) Unjail(ctx sdk.Context, consAddr sdk.ConsAddress) { // (the amount actually slashed may be less if there's // insufficient stake remaining) func (k Keeper) slashUnbondingDelegation(ctx sdk.Context, unbondingDelegation types.UnbondingDelegation, - infractionHeight int64, slashFactor sdk.Dec) (slashAmount sdk.Int) { + infractionHeight int64, slashFactor sdk.Dec) (totalSlashAmount sdk.Int) { now := ctx.BlockHeader().Time + totalSlashAmount = sdk.ZeroInt() - // If unbonding started before this height, stake didn't contribute to infraction - if unbondingDelegation.CreationHeight < infractionHeight { - return sdk.ZeroInt() - } + // perform slashing on all entries within the unbonding delegation + for i, entry := range unbondingDelegation.Entries { - if unbondingDelegation.MinTime.Before(now) { - // Unbonding delegation no longer eligible for slashing, skip it - // TODO Settle and delete it automatically? - return sdk.ZeroInt() - } + // If unbonding started before this height, stake didn't contribute to infraction + if entry.CreationHeight < infractionHeight { + continue + } - // Calculate slash amount proportional to stake contributing to infraction - slashAmountDec := slashFactor.MulInt(unbondingDelegation.InitialBalance.Amount) - slashAmount = slashAmountDec.TruncateInt() + if entry.IsMature(now) { + // Unbonding delegation no longer eligible for slashing, skip it + continue + } - // Don't slash more tokens than held - // Possible since the unbonding delegation may already - // have been slashed, and slash amounts are calculated - // according to stake held at time of infraction - unbondingSlashAmount := sdk.MinInt(slashAmount, unbondingDelegation.Balance.Amount) + // Calculate slash amount proportional to stake contributing to infraction + slashAmountDec := slashFactor.MulInt(entry.InitialBalance.Amount) + slashAmount := slashAmountDec.TruncateInt() + totalSlashAmount = totalSlashAmount.Add(slashAmount) - // Update unbonding delegation if necessary - if !unbondingSlashAmount.IsZero() { - unbondingDelegation.Balance.Amount = unbondingDelegation.Balance.Amount.Sub(unbondingSlashAmount) + // Don't slash more tokens than held + // Possible since the unbonding delegation may already + // have been slashed, and slash amounts are calculated + // according to stake held at time of infraction + unbondingSlashAmount := sdk.MinInt(slashAmount, entry.Balance.Amount) + + // Update unbonding delegation if necessary + if unbondingSlashAmount.IsZero() { + continue + } + entry.Balance.Amount = entry.Balance.Amount.Sub(unbondingSlashAmount) + unbondingDelegation.Entries[i] = entry k.SetUnbondingDelegation(ctx, unbondingDelegation) pool := k.GetPool(ctx) @@ -182,7 +189,7 @@ func (k Keeper) slashUnbondingDelegation(ctx sdk.Context, unbondingDelegation ty k.SetPool(ctx, pool) } - return slashAmount + return totalSlashAmount } // slash a redelegation and update the pool @@ -192,44 +199,51 @@ func (k Keeper) slashUnbondingDelegation(ctx sdk.Context, unbondingDelegation ty // insufficient stake remaining) // nolint: unparam func (k Keeper) slashRedelegation(ctx sdk.Context, validator types.Validator, redelegation types.Redelegation, - infractionHeight int64, slashFactor sdk.Dec) (slashAmount sdk.Int) { + infractionHeight int64, slashFactor sdk.Dec) (totalSlashAmount sdk.Int) { now := ctx.BlockHeader().Time + totalSlashAmount = sdk.ZeroInt() - // If redelegation started before this height, stake didn't contribute to infraction - if redelegation.CreationHeight < infractionHeight { - return sdk.ZeroInt() - } - - if redelegation.MinTime.Before(now) { - // Redelegation no longer eligible for slashing, skip it - // TODO Delete it automatically? - return sdk.ZeroInt() - } + // perform slashing on all entries within the redelegation + for i, entry := range redelegation.Entries { - // Calculate slash amount proportional to stake contributing to infraction - slashAmountDec := slashFactor.MulInt(redelegation.InitialBalance.Amount) - slashAmount = slashAmountDec.TruncateInt() + // If redelegation started before this height, stake didn't contribute to infraction + if entry.CreationHeight < infractionHeight { + continue + } - // Don't slash more tokens than held - // Possible since the redelegation may already - // have been slashed, and slash amounts are calculated - // according to stake held at time of infraction - redelegationSlashAmount := sdk.MinInt(slashAmount, redelegation.Balance.Amount) + if entry.IsMature(now) { + // Redelegation no longer eligible for slashing, skip it + continue + } - // Update redelegation if necessary - if !redelegationSlashAmount.IsZero() { - redelegation.Balance.Amount = redelegation.Balance.Amount.Sub(redelegationSlashAmount) - k.SetRedelegation(ctx, redelegation) - } + // Calculate slash amount proportional to stake contributing to infraction + slashAmountDec := slashFactor.MulInt(entry.InitialBalance.Amount) + slashAmount := slashAmountDec.TruncateInt() + totalSlashAmount = totalSlashAmount.Add(slashAmount) + + // Don't slash more tokens than held + // Possible since the redelegation may already + // have been slashed, and slash amounts are calculated + // according to stake held at time of infraction + redelegationSlashAmount := sdk.MinInt(slashAmount, entry.Balance.Amount) + + // Update entry if necessary + if !redelegationSlashAmount.IsZero() { + entry.Balance.Amount = entry.Balance.Amount.Sub(redelegationSlashAmount) + redelegation.Entries[i] = entry + k.SetRedelegation(ctx, redelegation) + } - // Unbond from target validator - sharesToUnbond := slashFactor.Mul(redelegation.SharesDst) - if !sharesToUnbond.IsZero() { + // Unbond from target validator + sharesToUnbond := slashFactor.Mul(entry.SharesDst) + if sharesToUnbond.IsZero() { + continue + } delegation, found := k.GetDelegation(ctx, redelegation.DelegatorAddr, redelegation.ValidatorDstAddr) if !found { // If deleted, delegation has zero shares, and we can't unbond any more - return slashAmount + continue } if sharesToUnbond.GT(delegation.Shares) { sharesToUnbond = delegation.Shares @@ -246,5 +260,5 @@ func (k Keeper) slashRedelegation(ctx sdk.Context, validator types.Validator, re k.SetPool(ctx, pool) } - return slashAmount + return totalSlashAmount } diff --git a/x/staking/keeper/slash_test.go b/x/staking/keeper/slash_test.go index a39b5b0a4b12..3ce69b780300 100644 --- a/x/staking/keeper/slash_test.go +++ b/x/staking/keeper/slash_test.go @@ -70,16 +70,11 @@ func TestSlashUnbondingDelegation(t *testing.T) { ctx, keeper, params := setupHelper(t, 10) fraction := sdk.NewDecWithPrec(5, 1) - // set an unbonding delegation - ubd := types.UnbondingDelegation{ - DelegatorAddr: addrDels[0], - ValidatorAddr: addrVals[0], - CreationHeight: 0, - // expiration timestamp (beyond which the unbonding delegation shouldn't be slashed) - MinTime: time.Unix(0, 0), - InitialBalance: sdk.NewInt64Coin(params.BondDenom, 10), - Balance: sdk.NewInt64Coin(params.BondDenom, 10), - } + // set an unbonding delegation with expiration timestamp (beyond which the + // unbonding delegation shouldn't be slashed) + ubd := types.NewUnbondingDelegation(addrDels[0], addrVals[0], 0, + time.Unix(5, 0), sdk.NewInt64Coin(params.BondDenom, 10)) + keeper.SetUnbondingDelegation(ctx, ubd) // unbonding started prior to the infraction height, stakw didn't contribute @@ -100,12 +95,13 @@ func TestSlashUnbondingDelegation(t *testing.T) { require.Equal(t, int64(5), slashAmount.Int64()) ubd, found := keeper.GetUnbondingDelegation(ctx, addrDels[0], addrVals[0]) require.True(t, found) + require.Len(t, ubd.Entries, 1) - // initialbalance unchanged - require.Equal(t, sdk.NewInt64Coin(params.BondDenom, 10), ubd.InitialBalance) + // initial balance unchanged + require.Equal(t, sdk.NewInt64Coin(params.BondDenom, 10), ubd.Entries[0].InitialBalance) // balance decreased - require.Equal(t, sdk.NewInt64Coin(params.BondDenom, 5), ubd.Balance) + require.Equal(t, sdk.NewInt64Coin(params.BondDenom, 5), ubd.Entries[0].Balance) newPool := keeper.GetPool(ctx) require.Equal(t, int64(5), oldPool.LooseTokens.Sub(newPool.LooseTokens).Int64()) } @@ -115,19 +111,12 @@ func TestSlashRedelegation(t *testing.T) { ctx, keeper, params := setupHelper(t, 10) fraction := sdk.NewDecWithPrec(5, 1) - // set a redelegation - rd := types.Redelegation{ - DelegatorAddr: addrDels[0], - ValidatorSrcAddr: addrVals[0], - ValidatorDstAddr: addrVals[1], - CreationHeight: 0, - // expiration timestamp (beyond which the redelegation shouldn't be slashed) - MinTime: time.Unix(0, 0), - SharesSrc: sdk.NewDec(10), - SharesDst: sdk.NewDec(10), - InitialBalance: sdk.NewInt64Coin(params.BondDenom, 10), - Balance: sdk.NewInt64Coin(params.BondDenom, 10), - } + // set a redelegation with an expiration timestamp beyond which the + // redelegation shouldn't be slashed + rd := types.NewRedelegation(addrDels[0], addrVals[0], addrVals[1], 0, + time.Unix(5, 0), sdk.NewInt64Coin(params.BondDenom, 10), sdk.NewDec(10), + sdk.NewDec(10)) + keeper.SetRedelegation(ctx, rd) // set the associated delegation @@ -162,16 +151,17 @@ func TestSlashRedelegation(t *testing.T) { require.Equal(t, int64(5), slashAmount.Int64()) rd, found = keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) require.True(t, found) + require.Len(t, rd.Entries, 1) // end block updates := keeper.ApplyAndReturnValidatorSetUpdates(ctx) require.Equal(t, 1, len(updates)) // initialbalance unchanged - require.Equal(t, sdk.NewInt64Coin(params.BondDenom, 10), rd.InitialBalance) + require.Equal(t, sdk.NewInt64Coin(params.BondDenom, 10), rd.Entries[0].InitialBalance) // balance decreased - require.Equal(t, sdk.NewInt64Coin(params.BondDenom, 5), rd.Balance) + require.Equal(t, sdk.NewInt64Coin(params.BondDenom, 5), rd.Entries[0].Balance) // shares decreased del, found = keeper.GetDelegation(ctx, addrDels[0], addrVals[1]) @@ -252,16 +242,10 @@ func TestSlashWithUnbondingDelegation(t *testing.T) { consAddr := sdk.ConsAddress(PKs[0].Address()) fraction := sdk.NewDecWithPrec(5, 1) - // set an unbonding delegation - ubd := types.UnbondingDelegation{ - DelegatorAddr: addrDels[0], - ValidatorAddr: addrVals[0], - CreationHeight: 11, - // expiration timestamp (beyond which the unbonding delegation shouldn't be slashed) - MinTime: time.Unix(0, 0), - InitialBalance: sdk.NewInt64Coin(params.BondDenom, 4), - Balance: sdk.NewInt64Coin(params.BondDenom, 4), - } + // set an unbonding delegation with expiration timestamp beyond which the + // unbonding delegation shouldn't be slashed + ubd := types.NewUnbondingDelegation(addrDels[0], addrVals[0], 11, + time.Unix(0, 0), sdk.NewInt64Coin(params.BondDenom, 4)) keeper.SetUnbondingDelegation(ctx, ubd) // slash validator for the first time @@ -278,8 +262,9 @@ func TestSlashWithUnbondingDelegation(t *testing.T) { // read updating unbonding delegation ubd, found = keeper.GetUnbondingDelegation(ctx, addrDels[0], addrVals[0]) require.True(t, found) + require.Len(t, ubd.Entries, 1) // balance decreased - require.Equal(t, sdk.NewInt(2), ubd.Balance.Amount) + require.Equal(t, sdk.NewInt(2), ubd.Entries[0].Balance.Amount) // read updated pool newPool := keeper.GetPool(ctx) // bonded tokens burned @@ -298,8 +283,9 @@ func TestSlashWithUnbondingDelegation(t *testing.T) { keeper.Slash(ctx, consAddr, 9, 10, fraction) ubd, found = keeper.GetUnbondingDelegation(ctx, addrDels[0], addrVals[0]) require.True(t, found) + require.Len(t, ubd.Entries, 1) // balance decreased again - require.Equal(t, sdk.NewInt(0), ubd.Balance.Amount) + require.Equal(t, sdk.NewInt(0), ubd.Entries[0].Balance.Amount) // read updated pool newPool = keeper.GetPool(ctx) // bonded tokens burned again @@ -318,8 +304,9 @@ func TestSlashWithUnbondingDelegation(t *testing.T) { keeper.Slash(ctx, consAddr, 9, 10, fraction) ubd, found = keeper.GetUnbondingDelegation(ctx, addrDels[0], addrVals[0]) require.True(t, found) + require.Len(t, ubd.Entries, 1) // balance unchanged - require.Equal(t, sdk.NewInt(0), ubd.Balance.Amount) + require.Equal(t, sdk.NewInt(0), ubd.Entries[0].Balance.Amount) // read updated pool newPool = keeper.GetPool(ctx) // bonded tokens burned again @@ -338,8 +325,9 @@ func TestSlashWithUnbondingDelegation(t *testing.T) { keeper.Slash(ctx, consAddr, 9, 10, fraction) ubd, found = keeper.GetUnbondingDelegation(ctx, addrDels[0], addrVals[0]) require.True(t, found) + require.Len(t, ubd.Entries, 1) // balance unchanged - require.Equal(t, sdk.NewInt(0), ubd.Balance.Amount) + require.Equal(t, sdk.NewInt(0), ubd.Entries[0].Balance.Amount) // read updated pool newPool = keeper.GetPool(ctx) // just 1 bonded token burned again since that's all the validator now has @@ -360,17 +348,9 @@ func TestSlashWithRedelegation(t *testing.T) { fraction := sdk.NewDecWithPrec(5, 1) // set a redelegation - rd := types.Redelegation{ - DelegatorAddr: addrDels[0], - ValidatorSrcAddr: addrVals[0], - ValidatorDstAddr: addrVals[1], - CreationHeight: 11, - MinTime: time.Unix(0, 0), - SharesSrc: sdk.NewDec(6), - SharesDst: sdk.NewDec(6), - InitialBalance: sdk.NewInt64Coin(params.BondDenom, 6), - Balance: sdk.NewInt64Coin(params.BondDenom, 6), - } + rd := types.NewRedelegation(addrDels[0], addrVals[0], addrVals[1], 11, + time.Unix(0, 0), sdk.NewInt64Coin(params.BondDenom, 6), sdk.NewDec(6), + sdk.NewDec(6)) keeper.SetRedelegation(ctx, rd) // set the associated delegation @@ -396,8 +376,9 @@ func TestSlashWithRedelegation(t *testing.T) { // read updating redelegation rd, found = keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) require.True(t, found) + require.Len(t, rd.Entries, 1) // balance decreased - require.Equal(t, sdk.NewInt(3), rd.Balance.Amount) + require.Equal(t, sdk.NewInt(3), rd.Entries[0].Balance.Amount) // read updated pool newPool := keeper.GetPool(ctx) // bonded tokens burned @@ -420,8 +401,9 @@ func TestSlashWithRedelegation(t *testing.T) { // read updating redelegation rd, found = keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) require.True(t, found) + require.Len(t, rd.Entries, 1) // balance decreased, now zero - require.Equal(t, sdk.NewInt(0), rd.Balance.Amount) + require.Equal(t, sdk.NewInt(0), rd.Entries[0].Balance.Amount) // read updated pool newPool = keeper.GetPool(ctx) // seven bonded tokens burned @@ -441,8 +423,9 @@ func TestSlashWithRedelegation(t *testing.T) { // read updating redelegation rd, found = keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) require.True(t, found) + require.Len(t, rd.Entries, 1) // balance still zero - require.Equal(t, sdk.NewInt(0), rd.Balance.Amount) + require.Equal(t, sdk.NewInt(0), rd.Entries[0].Balance.Amount) // read updated pool newPool = keeper.GetPool(ctx) // four more bonded tokens burned @@ -465,8 +448,9 @@ func TestSlashWithRedelegation(t *testing.T) { // read updating redelegation rd, found = keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) require.True(t, found) + require.Len(t, rd.Entries, 1) // balance still zero - require.Equal(t, sdk.NewInt(0), rd.Balance.Amount) + require.Equal(t, sdk.NewInt(0), rd.Entries[0].Balance.Amount) // read updated pool newPool = keeper.GetPool(ctx) // no more bonded tokens burned @@ -482,19 +466,11 @@ func TestSlashBoth(t *testing.T) { ctx, keeper, params := setupHelper(t, 10) fraction := sdk.NewDecWithPrec(5, 1) - // set a redelegation - rdA := types.Redelegation{ - DelegatorAddr: addrDels[0], - ValidatorSrcAddr: addrVals[0], - ValidatorDstAddr: addrVals[1], - CreationHeight: 11, - // expiration timestamp (beyond which the redelegation shouldn't be slashed) - MinTime: time.Unix(0, 0), - SharesSrc: sdk.NewDec(6), - SharesDst: sdk.NewDec(6), - InitialBalance: sdk.NewInt64Coin(params.BondDenom, 6), - Balance: sdk.NewInt64Coin(params.BondDenom, 6), - } + // set a redelegation with expiration timestamp beyond which the + // redelegation shouldn't be slashed + rdA := types.NewRedelegation(addrDels[0], addrVals[0], addrVals[1], 11, + time.Unix(0, 0), sdk.NewInt64Coin(params.BondDenom, 6), sdk.NewDec(6), + sdk.NewDec(6)) keeper.SetRedelegation(ctx, rdA) // set the associated delegation @@ -505,16 +481,10 @@ func TestSlashBoth(t *testing.T) { } keeper.SetDelegation(ctx, delA) - // set an unbonding delegation - ubdA := types.UnbondingDelegation{ - DelegatorAddr: addrDels[0], - ValidatorAddr: addrVals[0], - CreationHeight: 11, - // expiration timestamp (beyond which the unbonding delegation shouldn't be slashed) - MinTime: time.Unix(0, 0), - InitialBalance: sdk.NewInt64Coin(params.BondDenom, 4), - Balance: sdk.NewInt64Coin(params.BondDenom, 4), - } + // set an unbonding delegation with expiration timestamp (beyond which the + // unbonding delegation shouldn't be slashed) + ubdA := types.NewUnbondingDelegation(addrDels[0], addrVals[0], 11, + time.Unix(0, 0), sdk.NewInt64Coin(params.BondDenom, 4)) keeper.SetUnbondingDelegation(ctx, ubdA) // slash validator @@ -528,8 +498,9 @@ func TestSlashBoth(t *testing.T) { // read updating redelegation rdA, found = keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) require.True(t, found) + require.Len(t, rdA.Entries, 1) // balance decreased - require.Equal(t, sdk.NewInt(3), rdA.Balance.Amount) + require.Equal(t, sdk.NewInt(3), rdA.Entries[0].Balance.Amount) // read updated pool newPool := keeper.GetPool(ctx) // loose tokens burned diff --git a/x/staking/keeper/test_common.go b/x/staking/keeper/test_common.go index 04323b2d590c..7555b65db84d 100644 --- a/x/staking/keeper/test_common.go +++ b/x/staking/keeper/test_common.go @@ -94,7 +94,13 @@ func CreateTestInput(t *testing.T, isCheckTx bool, initCoins int64) (sdk.Context require.Nil(t, err) ctx := sdk.NewContext(ms, abci.Header{ChainID: "foochainid"}, isCheckTx, log.NewNopLogger()) - ctx = ctx.WithConsensusParams(&abci.ConsensusParams{Validator: &abci.ValidatorParams{PubKeyTypes: []string{tmtypes.ABCIPubKeyTypeEd25519}}}) + ctx = ctx.WithConsensusParams( + &abci.ConsensusParams{ + Validator: &abci.ValidatorParams{ + PubKeyTypes: []string{tmtypes.ABCIPubKeyTypeEd25519}, + }, + }, + ) cdc := MakeTestCodec() pk := params.NewKeeper(cdc, keyParams, tkeyParams) diff --git a/x/staking/querier/querier_test.go b/x/staking/querier/querier_test.go index ab9f243809b8..c20fc3ce52b1 100644 --- a/x/staking/querier/querier_test.go +++ b/x/staking/querier/querier_test.go @@ -296,7 +296,8 @@ func TestQueryDelegation(t *testing.T) { require.Equal(t, delegationsRes[0], delegation) // Query unbonging delegation - keeper.BeginUnbonding(ctx, addrAcc2, val1.OperatorAddr, sdk.NewDec(10)) + _, err = keeper.BeginUnbonding(ctx, addrAcc2, val1.OperatorAddr, sdk.NewDec(10)) + require.Nil(t, err) queryBondParams = NewQueryBondsParams(addrAcc2, addrVal1) bz, errRes = cdc.MarshalJSON(queryBondParams) @@ -347,8 +348,10 @@ func TestQueryDelegation(t *testing.T) { require.NotNil(t, err) // Query redelegation - redel, err := keeper.BeginRedelegation(ctx, addrAcc2, val1.OperatorAddr, val2.OperatorAddr, sdk.NewDec(10)) + _, err = keeper.BeginRedelegation(ctx, addrAcc2, val1.OperatorAddr, val2.OperatorAddr, sdk.NewDec(10)) require.Nil(t, err) + redel, found := keeper.GetRedelegation(ctx, addrAcc2, val1.OperatorAddr, val2.OperatorAddr) + require.True(t, found) bz, errRes = cdc.MarshalJSON(NewQueryRedelegationParams(addrAcc2, val1.OperatorAddr, val2.OperatorAddr)) require.Nil(t, errRes) @@ -379,7 +382,7 @@ func TestQueryRedelegations(t *testing.T) { keeper.SetValidator(ctx, val2) keeper.Delegate(ctx, addrAcc2, sdk.NewCoin(types.DefaultBondDenom, sdk.NewInt(100)), val1, true) - keeper.ApplyAndReturnValidatorSetUpdates(ctx) + _ = keeper.ApplyAndReturnValidatorSetUpdates(ctx) keeper.BeginRedelegation(ctx, addrAcc2, val1.GetOperator(), val2.GetOperator(), sdk.NewDec(20)) keeper.ApplyAndReturnValidatorSetUpdates(ctx) diff --git a/x/staking/simulation/invariants.go b/x/staking/simulation/invariants.go index 1bba6ccb23d1..8557a0b4febe 100644 --- a/x/staking/simulation/invariants.go +++ b/x/staking/simulation/invariants.go @@ -59,7 +59,9 @@ func SupplyInvariants(ck bank.Keeper, k staking.Keeper, return false }) k.IterateUnbondingDelegations(ctx, func(_ int64, ubd staking.UnbondingDelegation) bool { - loose = loose.Add(sdk.NewDecFromInt(ubd.Balance.Amount)) + for _, entry := range ubd.Entries { + loose = loose.Add(sdk.NewDecFromInt(entry.Balance.Amount)) + } return false }) k.IterateValidators(ctx, func(_ int64, validator sdk.Validator) bool { diff --git a/x/staking/types/delegation.go b/x/staking/types/delegation.go index 46fb74e1dc2e..87a4759932c3 100644 --- a/x/staking/types/delegation.go +++ b/x/staking/types/delegation.go @@ -26,6 +26,8 @@ type DVVTriplet struct { ValidatorDstAddr sdk.ValAddress } +//_______________________________________________________________________ + // Delegation represents the bond with tokens held by an account. It is // owned by one delegator, and is associated with the voting power of one // pubKey. @@ -35,6 +37,17 @@ type Delegation struct { Shares sdk.Dec `json:"shares"` } +// NewDelegation creates a new delegation object +func NewDelegation(delegatorAddr sdk.AccAddress, validatorAddr sdk.ValAddress, + shares sdk.Dec) Delegation { + + return Delegation{ + DelegatorAddr: delegatorAddr, + ValidatorAddr: validatorAddr, + Shares: shares, + } +} + // return the delegation func MustMarshalDelegation(cdc *codec.Codec, delegation Delegation) []byte { return cdc.MustMarshalBinaryLengthPrefixed(delegation) @@ -82,14 +95,65 @@ func (d Delegation) HumanReadableString() (string, error) { return resp, nil } +//________________________________________________________________________ + // UnbondingDelegation reflects a delegation's passive unbonding queue. +// it may hold multiple entries between the same delegator/validator type UnbondingDelegation struct { - DelegatorAddr sdk.AccAddress `json:"delegator_addr"` // delegator - ValidatorAddr sdk.ValAddress `json:"validator_addr"` // validator unbonding from operator addr - CreationHeight int64 `json:"creation_height"` // height which the unbonding took place - MinTime time.Time `json:"min_time"` // unix time for unbonding completion - InitialBalance sdk.Coin `json:"initial_balance"` // atoms initially scheduled to receive at completion - Balance sdk.Coin `json:"balance"` // atoms to receive at completion + DelegatorAddr sdk.AccAddress `json:"delegator_addr"` // delegator + ValidatorAddr sdk.ValAddress `json:"validator_addr"` // validator unbonding from operator addr + Entries []UnbondingDelegationEntry `json:"entries"` // unbonding delegation entries +} + +// UnbondingDelegationEntry - entry to an UnbondingDelegation +type UnbondingDelegationEntry struct { + CreationHeight int64 `json:"creation_height"` // height which the unbonding took place + CompletionTime time.Time `json:"completion_time"` // unix time for unbonding completion + InitialBalance sdk.Coin `json:"initial_balance"` // atoms initially scheduled to receive at completion + Balance sdk.Coin `json:"balance"` // atoms to receive at completion +} + +// IsMature - is the current entry mature +func (e UnbondingDelegationEntry) IsMature(currentTime time.Time) bool { + return !e.CompletionTime.After(currentTime) +} + +// NewUnbondingDelegation - create a new unbonding delegation object +func NewUnbondingDelegation(delegatorAddr sdk.AccAddress, + validatorAddr sdk.ValAddress, creationHeight int64, minTime time.Time, + balance sdk.Coin) UnbondingDelegation { + + entry := NewUnbondingDelegationEntry(creationHeight, minTime, balance) + return UnbondingDelegation{ + DelegatorAddr: delegatorAddr, + ValidatorAddr: validatorAddr, + Entries: []UnbondingDelegationEntry{entry}, + } +} + +// NewUnbondingDelegation - create a new unbonding delegation object +func NewUnbondingDelegationEntry(creationHeight int64, completionTime time.Time, + balance sdk.Coin) UnbondingDelegationEntry { + + return UnbondingDelegationEntry{ + CreationHeight: creationHeight, + CompletionTime: completionTime, + InitialBalance: balance, + Balance: balance, + } +} + +// AddEntry - append entry to the unbonding delegation +func (d *UnbondingDelegation) AddEntry(creationHeight int64, + minTime time.Time, balance sdk.Coin) { + + entry := NewUnbondingDelegationEntry(creationHeight, minTime, balance) + d.Entries = append(d.Entries, entry) +} + +// RemoveEntry - remove entry at index i to the unbonding delegation +func (d *UnbondingDelegation) RemoveEntry(i int64) { + d.Entries = append(d.Entries[:i], d.Entries[i+1:]...) } // return the unbonding delegation @@ -126,25 +190,83 @@ func (d UnbondingDelegation) HumanReadableString() (string, error) { resp := "Unbonding Delegation \n" resp += fmt.Sprintf("Delegator: %s\n", d.DelegatorAddr) resp += fmt.Sprintf("Validator: %s\n", d.ValidatorAddr) - resp += fmt.Sprintf("Creation height: %v\n", d.CreationHeight) - resp += fmt.Sprintf("Min time to unbond (unix): %v\n", d.MinTime) - resp += fmt.Sprintf("Expected balance: %s", d.Balance.String()) + for _, entry := range d.Entries { + resp += "Unbonding Delegation Entry\n" + resp += fmt.Sprintf("Creation height: %v\n", entry.CreationHeight) + resp += fmt.Sprintf("Min time to unbond (unix): %v\n", entry.CompletionTime) + resp += fmt.Sprintf("Expected balance: %s", entry.Balance.String()) + } return resp, nil - } // Redelegation reflects a delegation's passive re-delegation queue. type Redelegation struct { - DelegatorAddr sdk.AccAddress `json:"delegator_addr"` // delegator - ValidatorSrcAddr sdk.ValAddress `json:"validator_src_addr"` // validator redelegation source operator addr - ValidatorDstAddr sdk.ValAddress `json:"validator_dst_addr"` // validator redelegation destination operator addr - CreationHeight int64 `json:"creation_height"` // height which the redelegation took place - MinTime time.Time `json:"min_time"` // unix time for redelegation completion - InitialBalance sdk.Coin `json:"initial_balance"` // initial balance when redelegation started - Balance sdk.Coin `json:"balance"` // current balance - SharesSrc sdk.Dec `json:"shares_src"` // amount of source shares redelegating - SharesDst sdk.Dec `json:"shares_dst"` // amount of destination shares redelegating + DelegatorAddr sdk.AccAddress `json:"delegator_addr"` // delegator + ValidatorSrcAddr sdk.ValAddress `json:"validator_src_addr"` // validator redelegation source operator addr + ValidatorDstAddr sdk.ValAddress `json:"validator_dst_addr"` // validator redelegation destination operator addr + Entries []RedelegationEntry `json:"entries"` // redelegation entries +} + +// RedelegationEntry - entry to a Redelegation +type RedelegationEntry struct { + CreationHeight int64 `json:"creation_height"` // height which the redelegation took place + CompletionTime time.Time `json:"completion_time"` // unix time for redelegation completion + InitialBalance sdk.Coin `json:"initial_balance"` // initial balance when redelegation started + Balance sdk.Coin `json:"balance"` // current balance (current value held in destination validator) + SharesSrc sdk.Dec `json:"shares_src"` // amount of source-validator shares removed by redelegation + SharesDst sdk.Dec `json:"shares_dst"` // amount of destination-validator shares created by redelegation +} + +// NewRedelegation - create a new redelegation object +func NewRedelegation(delegatorAddr sdk.AccAddress, validatorSrcAddr, + validatorDstAddr sdk.ValAddress, creationHeight int64, + minTime time.Time, balance sdk.Coin, + sharesSrc, sharesDst sdk.Dec) Redelegation { + + entry := NewRedelegationEntry(creationHeight, + minTime, balance, sharesSrc, sharesDst) + + return Redelegation{ + DelegatorAddr: delegatorAddr, + ValidatorSrcAddr: validatorSrcAddr, + ValidatorDstAddr: validatorDstAddr, + Entries: []RedelegationEntry{entry}, + } +} + +// NewRedelegation - create a new redelegation object +func NewRedelegationEntry(creationHeight int64, + completionTime time.Time, balance sdk.Coin, + sharesSrc, sharesDst sdk.Dec) RedelegationEntry { + + return RedelegationEntry{ + CreationHeight: creationHeight, + CompletionTime: completionTime, + InitialBalance: balance, + Balance: balance, + SharesSrc: sharesSrc, + SharesDst: sharesDst, + } +} + +// IsMature - is the current entry mature +func (e RedelegationEntry) IsMature(currentTime time.Time) bool { + return !e.CompletionTime.After(currentTime) +} + +// AddEntry - append entry to the unbonding delegation +func (d *Redelegation) AddEntry(creationHeight int64, + minTime time.Time, balance sdk.Coin, + sharesSrc, sharesDst sdk.Dec) { + + entry := NewRedelegationEntry(creationHeight, minTime, balance, sharesSrc, sharesDst) + d.Entries = append(d.Entries, entry) +} + +// RemoveEntry - remove entry at index i to the unbonding delegation +func (d *Redelegation) RemoveEntry(i int64) { + d.Entries = append(d.Entries[:i], d.Entries[i+1:]...) } // return the redelegation @@ -182,11 +304,11 @@ func (d Redelegation) HumanReadableString() (string, error) { resp += fmt.Sprintf("Delegator: %s\n", d.DelegatorAddr) resp += fmt.Sprintf("Source Validator: %s\n", d.ValidatorSrcAddr) resp += fmt.Sprintf("Destination Validator: %s\n", d.ValidatorDstAddr) - resp += fmt.Sprintf("Creation height: %v\n", d.CreationHeight) - resp += fmt.Sprintf("Min time to unbond (unix): %v\n", d.MinTime) - resp += fmt.Sprintf("Source shares: %s\n", d.SharesSrc.String()) - resp += fmt.Sprintf("Destination shares: %s", d.SharesDst.String()) - + for _, entry := range d.Entries { + resp += fmt.Sprintf("Creation height: %v\n", entry.CreationHeight) + resp += fmt.Sprintf("Min time to unbond (unix): %v\n", entry.CompletionTime) + resp += fmt.Sprintf("Source shares: %s\n", entry.SharesSrc.String()) + resp += fmt.Sprintf("Destination shares: %s", entry.SharesDst.String()) + } return resp, nil - } diff --git a/x/staking/types/delegation_test.go b/x/staking/types/delegation_test.go index ee7d81bf5b5f..95f58d678252 100644 --- a/x/staking/types/delegation_test.go +++ b/x/staking/types/delegation_test.go @@ -10,16 +10,8 @@ import ( ) func TestDelegationEqual(t *testing.T) { - d1 := Delegation{ - DelegatorAddr: sdk.AccAddress(addr1), - ValidatorAddr: addr2, - Shares: sdk.NewDec(100), - } - d2 := Delegation{ - DelegatorAddr: sdk.AccAddress(addr1), - ValidatorAddr: addr2, - Shares: sdk.NewDec(100), - } + d1 := NewDelegation(sdk.AccAddress(addr1), addr2, sdk.NewDec(100)) + d2 := d1 ok := d1.Equal(d2) require.True(t, ok) @@ -32,11 +24,7 @@ func TestDelegationEqual(t *testing.T) { } func TestDelegationHumanReadableString(t *testing.T) { - d := Delegation{ - DelegatorAddr: sdk.AccAddress(addr1), - ValidatorAddr: addr2, - Shares: sdk.NewDec(100), - } + d := NewDelegation(sdk.AccAddress(addr1), addr2, sdk.NewDec(100)) // NOTE: Being that the validator's keypair is random, we cannot test the // actual contents of the string. @@ -46,69 +34,54 @@ func TestDelegationHumanReadableString(t *testing.T) { } func TestUnbondingDelegationEqual(t *testing.T) { - ud1 := UnbondingDelegation{ - DelegatorAddr: sdk.AccAddress(addr1), - ValidatorAddr: addr2, - } - ud2 := UnbondingDelegation{ - DelegatorAddr: sdk.AccAddress(addr1), - ValidatorAddr: addr2, - } - - ok := ud1.Equal(ud2) + ubd1 := NewUnbondingDelegation(sdk.AccAddress(addr1), addr2, 0, + time.Unix(0, 0), sdk.NewInt64Coin(DefaultBondDenom, 0)) + ubd2 := ubd1 + + ok := ubd1.Equal(ubd2) require.True(t, ok) - ud2.ValidatorAddr = addr3 + ubd2.ValidatorAddr = addr3 - ud2.MinTime = time.Unix(20*20*2, 0) - ok = ud1.Equal(ud2) + ubd2.Entries[0].CompletionTime = time.Unix(20*20*2, 0) + ok = ubd1.Equal(ubd2) require.False(t, ok) } func TestUnbondingDelegationHumanReadableString(t *testing.T) { - ud := UnbondingDelegation{ - DelegatorAddr: sdk.AccAddress(addr1), - ValidatorAddr: addr2, - } + ubd := NewUnbondingDelegation(sdk.AccAddress(addr1), addr2, 0, + time.Unix(0, 0), sdk.NewInt64Coin(DefaultBondDenom, 0)) // NOTE: Being that the validator's keypair is random, we cannot test the // actual contents of the string. - valStr, err := ud.HumanReadableString() + valStr, err := ubd.HumanReadableString() require.Nil(t, err) require.NotEmpty(t, valStr) } func TestRedelegationEqual(t *testing.T) { - r1 := Redelegation{ - DelegatorAddr: sdk.AccAddress(addr1), - ValidatorSrcAddr: addr2, - ValidatorDstAddr: addr3, - } - r2 := Redelegation{ - DelegatorAddr: sdk.AccAddress(addr1), - ValidatorSrcAddr: addr2, - ValidatorDstAddr: addr3, - } + r1 := NewRedelegation(sdk.AccAddress(addr1), addr2, addr3, 0, + time.Unix(0, 0), sdk.NewInt64Coin(DefaultBondDenom, 0), + sdk.NewDec(0), sdk.NewDec(0)) + r2 := NewRedelegation(sdk.AccAddress(addr1), addr2, addr3, 0, + time.Unix(0, 0), sdk.NewInt64Coin(DefaultBondDenom, 0), + sdk.NewDec(0), sdk.NewDec(0)) ok := r1.Equal(r2) require.True(t, ok) - r2.SharesDst = sdk.NewDec(10) - r2.SharesSrc = sdk.NewDec(20) - r2.MinTime = time.Unix(20*20*2, 0) + r2.Entries[0].SharesDst = sdk.NewDec(10) + r2.Entries[0].SharesSrc = sdk.NewDec(20) + r2.Entries[0].CompletionTime = time.Unix(20*20*2, 0) ok = r1.Equal(r2) require.False(t, ok) } func TestRedelegationHumanReadableString(t *testing.T) { - r := Redelegation{ - DelegatorAddr: sdk.AccAddress(addr1), - ValidatorSrcAddr: addr2, - ValidatorDstAddr: addr3, - SharesDst: sdk.NewDec(10), - SharesSrc: sdk.NewDec(20), - } + r := NewRedelegation(sdk.AccAddress(addr1), addr2, addr3, 0, + time.Unix(0, 0), sdk.NewInt64Coin(DefaultBondDenom, 0), + sdk.NewDec(10), sdk.NewDec(20)) // NOTE: Being that the validator's keypair is random, we cannot test the // actual contents of the string. diff --git a/x/staking/types/errors.go b/x/staking/types/errors.go index 1ba5f4ca2520..2ed4a2411f84 100644 --- a/x/staking/types/errors.go +++ b/x/staking/types/errors.go @@ -178,11 +178,6 @@ func ErrTransitiveRedelegation(codespace sdk.CodespaceType) sdk.Error { "redelegation to this validator already in progress, first redelegation to this validator must complete before next redelegation") } -func ErrConflictingRedelegation(codespace sdk.CodespaceType) sdk.Error { - return sdk.NewError(codespace, CodeInvalidDelegation, - "conflicting redelegation from this source validator to this dest validator already exists, you must wait for it to finish") -} - func ErrDelegatorShareExRateInvalid(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidDelegation, "cannot delegate to validators with invalid (zero) ex-rate")