Merge PR #3372: Limit unbonding delegation / redelegations

This commit is contained in:
frog power 4000 2019-01-24 17:44:31 -05:00 committed by Christopher Goes
parent ccefbc747c
commit dc75eb4eb8
8 changed files with 177 additions and 14 deletions

View File

@ -22,6 +22,7 @@ FEATURES
* Gaia
* SDK
* \#3270 [x/staking] limit number of ongoing unbonding delegations /redelegations per pair/trio
* Tendermint

View File

@ -101,6 +101,8 @@ This message is expected to fail if:
- the delegation doesn't exist
- the validator doesn't exist
- the delegation has less shares than `SharesAmount`
- existing `UnbondingDelegation` has maximum entries as defined by
params.MaxEntries
When this message is processed the following actions occur:
- validator's `DelegatorShares` and the delegation's `Shares` are both reduced
@ -143,6 +145,8 @@ This message is expected to fail if:
- the delegation has less shares than `SharesAmount`
- the source validator has a receiving redelegation which
is not matured (aka. the redelegation may be transitive)
- existing `Redelegation` has maximum entries as defined by
params.MaxEntries
When this message is processed the following actions occur:
- the source validator's `DelegatorShares` and the delegations `Shares` are

View File

@ -28,6 +28,7 @@ and defines overall functioning of the staking module.
type Params struct {
UnbondingTime time.Duration // time duration of unbonding
MaxValidators uint16 // maximum number of validators
MaxEntries uint16 // max entries for either unbonding delegation or redelegation (per pair/trio)
BondDenom string // bondable coin denomination
}
```

View File

@ -153,6 +153,17 @@ func (k Keeper) IterateUnbondingDelegations(ctx sdk.Context, fn func(index int64
}
}
// HasMaxUnbondingDelegationEntries - unbonding delegation has maximum number of entries
func (k Keeper) HasMaxUnbondingDelegationEntries(ctx sdk.Context,
delegatorAddr sdk.AccAddress, validatorAddr sdk.ValAddress) bool {
ubd, found := k.GetUnbondingDelegation(ctx, delegatorAddr, validatorAddr)
if !found {
return false
}
return len(ubd.Entries) >= int(k.MaxEntries(ctx))
}
// set the unbonding delegation and associated index
func (k Keeper) SetUnbondingDelegation(ctx sdk.Context, ubd types.UnbondingDelegation) {
store := ctx.KVStore(k.storeKey)
@ -313,6 +324,18 @@ func (k Keeper) HasReceivingRedelegation(ctx sdk.Context,
return false
}
// HasMaxRedelegationEntries - redelegation has maximum number of entries
func (k Keeper) HasMaxRedelegationEntries(ctx sdk.Context,
delegatorAddr sdk.AccAddress, validatorSrcAddr,
validatorDstAddr sdk.ValAddress) bool {
red, found := k.GetRedelegation(ctx, delegatorAddr, validatorSrcAddr, validatorDstAddr)
if !found {
return false
}
return len(red.Entries) >= int(k.MaxEntries(ctx))
}
// set a redelegation and associated index
func (k Keeper) SetRedelegation(ctx sdk.Context, red types.Redelegation) {
store := ctx.KVStore(k.storeKey)
@ -580,6 +603,10 @@ func (k Keeper) Undelegate(ctx sdk.Context, delAddr sdk.AccAddress,
return completionTime, nil
}
if k.HasMaxUnbondingDelegationEntries(ctx, delAddr, valAddr) {
return time.Time{}, types.ErrMaxUnbondingDelegationEntries(k.Codespace())
}
ubd := k.SetUnbondingDelegationEntry(ctx, delAddr,
valAddr, height, completionTime, balance)
@ -637,6 +664,10 @@ func (k Keeper) BeginRedelegation(ctx sdk.Context, delAddr sdk.AccAddress,
return time.Time{}, types.ErrTransitiveRedelegation(k.Codespace())
}
if k.HasMaxRedelegationEntries(ctx, delAddr, valSrcAddr, valDstAddr) {
return time.Time{}, types.ErrMaxRedelegationEntries(k.Codespace())
}
returnAmount, err := k.unbond(ctx, delAddr, valSrcAddr, sharesAmount)
if err != nil {
return time.Time{}, err

View File

@ -198,7 +198,7 @@ func TestUnbondDelegation(t *testing.T) {
amount, err := keeper.unbond(ctx, addrDels[0], addrVals[0], sdk.NewDec(6))
require.NoError(t, err)
require.Equal(t, int64(6), amount.Int64()) // shares to be added to an unbonding delegation / redelegation
require.Equal(t, int64(6), amount.Int64()) // shares to be added to an unbonding delegation
delegation, found := keeper.GetDelegation(ctx, addrDels[0], addrVals[0])
require.True(t, found)
@ -212,6 +212,53 @@ func TestUnbondDelegation(t *testing.T) {
require.Equal(t, int64(4), pool.BondedTokens.Int64())
}
func TestUnbondingDelegationsMaxEntries(t *testing.T) {
ctx, _, keeper := CreateTestInput(t, false, 0)
pool := keeper.GetPool(ctx)
pool.NotBondedTokens = sdk.NewInt(10)
// create a validator and a delegator to that validator
validator := types.NewValidator(addrVals[0], PKs[0], types.Description{})
validator, pool, issuedShares := validator.AddTokensFromDel(pool, sdk.NewInt(10))
require.Equal(t, int64(10), issuedShares.RoundInt64())
keeper.SetPool(ctx, pool)
validator = TestingUpdateValidator(keeper, ctx, validator, true)
pool = keeper.GetPool(ctx)
require.Equal(t, int64(10), pool.BondedTokens.Int64())
require.Equal(t, int64(10), validator.BondedTokens().Int64())
delegation := types.Delegation{
DelegatorAddr: addrDels[0],
ValidatorAddr: addrVals[0],
Shares: issuedShares,
}
keeper.SetDelegation(ctx, delegation)
maxEntries := keeper.MaxEntries(ctx)
// should all pass
var completionTime time.Time
for i := uint16(0); i < maxEntries; i++ {
var err error
completionTime, err = keeper.Undelegate(ctx, addrDels[0], addrVals[0], sdk.NewDec(1))
require.NoError(t, err)
}
// an additional unbond should fail due to max entries
_, err := keeper.Undelegate(ctx, addrDels[0], addrVals[0], sdk.NewDec(1))
require.Error(t, err)
// mature unbonding delegations
ctx = ctx.WithBlockTime(completionTime)
err = keeper.CompleteUnbonding(ctx, addrDels[0], addrVals[0])
require.NoError(t, err)
// unbonding should work again
_, err = keeper.Undelegate(ctx, addrDels[0], addrVals[0], sdk.NewDec(1))
require.NoError(t, err)
}
// test removing all self delegation from a validator which should
// shift it from the bonded to unbonded state
func TestUndelegateSelfDelegation(t *testing.T) {
@ -593,6 +640,59 @@ func TestRedelegateToSameValidator(t *testing.T) {
}
func TestRedelegationMaxEntries(t *testing.T) {
ctx, _, keeper := CreateTestInput(t, false, 0)
pool := keeper.GetPool(ctx)
pool.NotBondedTokens = sdk.NewInt(20)
// create a validator with a self-delegation
validator := types.NewValidator(addrVals[0], PKs[0], types.Description{})
validator, pool, issuedShares := validator.AddTokensFromDel(pool, sdk.NewInt(10))
require.Equal(t, int64(10), issuedShares.RoundInt64())
keeper.SetPool(ctx, pool)
validator = TestingUpdateValidator(keeper, ctx, validator, true)
pool = keeper.GetPool(ctx)
val0AccAddr := sdk.AccAddress(addrVals[0].Bytes())
selfDelegation := types.Delegation{
DelegatorAddr: val0AccAddr,
ValidatorAddr: addrVals[0],
Shares: issuedShares,
}
keeper.SetDelegation(ctx, selfDelegation)
// create a second validator
validator2 := types.NewValidator(addrVals[1], PKs[1], types.Description{})
validator2, pool, issuedShares = validator2.AddTokensFromDel(pool, sdk.NewInt(10))
require.Equal(t, int64(10), issuedShares.RoundInt64())
pool.BondedTokens = pool.BondedTokens.Add(sdk.NewInt(10))
keeper.SetPool(ctx, pool)
validator2 = TestingUpdateValidator(keeper, ctx, validator2, true)
require.Equal(t, sdk.Bonded, validator2.Status)
maxEntries := keeper.MaxEntries(ctx)
// redelegations should pass
var completionTime time.Time
for i := uint16(0); i < maxEntries; i++ {
var err error
completionTime, err = keeper.BeginRedelegation(ctx, val0AccAddr, addrVals[0], addrVals[1], sdk.NewDec(1))
require.NoError(t, err)
}
// an additional redelegation should fail due to max entries
_, err := keeper.BeginRedelegation(ctx, val0AccAddr, addrVals[0], addrVals[1], sdk.NewDec(1))
require.Error(t, err)
// mature redelegations
ctx = ctx.WithBlockTime(completionTime)
err = keeper.CompleteRedelegation(ctx, val0AccAddr, addrVals[0], addrVals[1])
require.NoError(t, err)
// redelegation should work again
_, err = keeper.BeginRedelegation(ctx, val0AccAddr, addrVals[0], addrVals[1], sdk.NewDec(1))
require.NoError(t, err)
}
func TestRedelegateSelfDelegation(t *testing.T) {
ctx, _, keeper := CreateTestInput(t, false, 0)
pool := keeper.GetPool(ctx)

View File

@ -30,6 +30,13 @@ func (k Keeper) MaxValidators(ctx sdk.Context) (res uint16) {
return
}
// MaxEntries - Maximum number of simultaneous unbonding
// delegations or redelegations (per pair/trio)
func (k Keeper) MaxEntries(ctx sdk.Context) (res uint16) {
k.paramstore.Get(ctx, types.KeyMaxEntries, &res)
return
}
// BondDenom - Bondable coin denomination
func (k Keeper) BondDenom(ctx sdk.Context) (res string) {
k.paramstore.Get(ctx, types.KeyBondDenom, &res)
@ -37,11 +44,13 @@ func (k Keeper) BondDenom(ctx sdk.Context) (res string) {
}
// Get all parameteras as types.Params
func (k Keeper) GetParams(ctx sdk.Context) (res types.Params) {
res.UnbondingTime = k.UnbondingTime(ctx)
res.MaxValidators = k.MaxValidators(ctx)
res.BondDenom = k.BondDenom(ctx)
return
func (k Keeper) GetParams(ctx sdk.Context) types.Params {
return types.NewParams(
k.UnbondingTime(ctx),
k.MaxValidators(ctx),
k.MaxEntries(ctx),
k.BondDenom(ctx),
)
}
// set the params

View File

@ -149,8 +149,9 @@ func ErrNoUnbondingDelegation(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidDelegation, "no unbonding delegation found")
}
func ErrExistingUnbondingDelegation(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidDelegation, "existing unbonding delegation found")
func ErrMaxUnbondingDelegationEntries(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidDelegation,
"too many unbonding delegation entries in this delegator/validator duo, please wait for some entries to mature")
}
func ErrBadRedelegationAddr(codespace sdk.CodespaceType) sdk.Error {
@ -178,6 +179,11 @@ 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 ErrMaxRedelegationEntries(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidDelegation,
"too many redelegation entries in this delegator/src-validator/dst-validator trio, please wait for some entries to mature")
}
func ErrDelegatorShareExRateInvalid(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidDelegation,
"cannot delegate to validators with invalid (zero) ex-rate")

View File

@ -28,6 +28,7 @@ const (
var (
KeyUnbondingTime = []byte("UnbondingTime")
KeyMaxValidators = []byte("MaxValidators")
KeyMaxEntries = []byte("KeyMaxEntries")
KeyBondDenom = []byte("BondDenom")
)
@ -37,14 +38,27 @@ var _ params.ParamSet = (*Params)(nil)
type Params struct {
UnbondingTime time.Duration `json:"unbonding_time"` // time duration of unbonding
MaxValidators uint16 `json:"max_validators"` // maximum number of validators
MaxEntries uint16 `json:"max_entries"` // max entries for either unbonding delegation or redelegation (per pair/trio)
BondDenom string `json:"bond_denom"` // bondable coin denomination
}
func NewParams(unbondingTime time.Duration, maxValidators, maxEntries uint16,
bondDenom string) Params {
return Params{
UnbondingTime: unbondingTime,
MaxValidators: maxValidators,
MaxEntries: maxEntries,
BondDenom: bondDenom,
}
}
// Implements params.ParamSet
func (p *Params) KeyValuePairs() params.KeyValuePairs {
return params.KeyValuePairs{
{KeyUnbondingTime, &p.UnbondingTime},
{KeyMaxValidators, &p.MaxValidators},
{KeyMaxEntries, &p.MaxEntries},
{KeyBondDenom, &p.BondDenom},
}
}
@ -58,11 +72,7 @@ func (p Params) Equal(p2 Params) bool {
// DefaultParams returns a default set of parameters.
func DefaultParams() Params {
return Params{
UnbondingTime: defaultUnbondingTime,
MaxValidators: 100,
BondDenom: DefaultBondDenom,
}
return NewParams(defaultUnbondingTime, 100, 7, DefaultBondDenom)
}
// String returns a human readable string representation of the parameters.
@ -70,8 +80,9 @@ func (p Params) String() string {
return fmt.Sprintf(`Params:
Unbonding Time: %s)
Max Validators: %d)
Max Entries: %d)
Bonded Coin Denomination: %s`, p.UnbondingTime,
p.MaxValidators, p.BondDenom)
p.MaxValidators, p.MaxEntries, p.BondDenom)
}
// unmarshal the current staking params value from store key or panic