cosmos-sdk/tests/integration/staking/keeper/unbonding_test.go

418 lines
17 KiB
Go

package keeper_test
import (
"testing"
"time"
"cosmossdk.io/math"
"cosmossdk.io/simapp"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
sdk "github.com/cosmos/cosmos-sdk/types"
banktestutil "github.com/cosmos/cosmos-sdk/x/bank/testutil"
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
"github.com/cosmos/cosmos-sdk/x/staking/testutil"
"github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)
// SetupUnbondingTests creates two validators and setup mocked staking hooks for testing unbonding
func SetupUnbondingTests(t *testing.T, app *simapp.SimApp, ctx sdk.Context, hookCalled *bool, ubdeID *uint64) (bondDenom string, addrDels []sdk.AccAddress, addrVals []sdk.ValAddress) {
// setup hooks
mockCtrl := gomock.NewController(t)
mockStackingHooks := testutil.NewMockStakingHooks(mockCtrl)
mockStackingHooks.EXPECT().AfterDelegationModified(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mockStackingHooks.EXPECT().AfterUnbondingInitiated(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx sdk.Context, id uint64) error {
*hookCalled = true
// save id
*ubdeID = id
// call back to stop unbonding
err := app.StakingKeeper.PutUnbondingOnHold(ctx, id)
require.NoError(t, err)
return nil
}).AnyTimes()
mockStackingHooks.EXPECT().AfterValidatorBeginUnbonding(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mockStackingHooks.EXPECT().AfterValidatorBonded(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mockStackingHooks.EXPECT().AfterValidatorCreated(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mockStackingHooks.EXPECT().AfterValidatorRemoved(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mockStackingHooks.EXPECT().BeforeDelegationCreated(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mockStackingHooks.EXPECT().BeforeDelegationRemoved(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mockStackingHooks.EXPECT().BeforeDelegationSharesModified(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mockStackingHooks.EXPECT().BeforeValidatorModified(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mockStackingHooks.EXPECT().BeforeValidatorSlashed(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
app.StakingKeeper.SetHooks(types.NewMultiStakingHooks(mockStackingHooks))
addrDels = simtestutil.AddTestAddrsIncremental(app.BankKeeper, app.StakingKeeper, ctx, 2, math.NewInt(10000))
addrVals = simtestutil.ConvertAddrsToValAddrs(addrDels)
valTokens := app.StakingKeeper.TokensFromConsensusPower(ctx, 10)
startTokens := app.StakingKeeper.TokensFromConsensusPower(ctx, 20)
bondDenom = app.StakingKeeper.BondDenom(ctx)
notBondedPool := app.StakingKeeper.GetNotBondedPool(ctx)
require.NoError(t, banktestutil.FundModuleAccount(app.BankKeeper, ctx, notBondedPool.GetName(), sdk.NewCoins(sdk.NewCoin(bondDenom, startTokens))))
app.BankKeeper.SendCoinsFromModuleToModule(ctx, types.BondedPoolName, types.NotBondedPoolName, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, startTokens)))
app.AccountKeeper.SetModuleAccount(ctx, notBondedPool)
// Create a validator
validator1 := testutil.NewValidator(t, addrVals[0], PKs[0])
validator1, issuedShares1 := validator1.AddTokensFromDel(valTokens)
require.Equal(t, valTokens, issuedShares1.RoundInt())
validator1 = stakingkeeper.TestingUpdateValidator(app.StakingKeeper, ctx, validator1, true)
require.True(math.IntEq(t, valTokens, validator1.BondedTokens()))
require.True(t, validator1.IsBonded())
// Create a delegator
delegation := types.NewDelegation(addrDels[0], addrVals[0], issuedShares1)
app.StakingKeeper.SetDelegation(ctx, delegation)
// Create a validator to redelegate to
validator2 := testutil.NewValidator(t, addrVals[1], PKs[1])
validator2, issuedShares2 := validator2.AddTokensFromDel(valTokens)
require.Equal(t, valTokens, issuedShares2.RoundInt())
validator2 = stakingkeeper.TestingUpdateValidator(app.StakingKeeper, ctx, validator2, true)
require.Equal(t, types.Bonded, validator2.Status)
require.True(t, validator2.IsBonded())
return bondDenom, addrDels, addrVals
}
func doUnbondingDelegation(
t *testing.T,
stakingKeeper *stakingkeeper.Keeper,
bankKeeper types.BankKeeper,
ctx sdk.Context,
bondDenom string,
addrDels []sdk.AccAddress,
addrVals []sdk.ValAddress,
hookCalled *bool,
) (completionTime time.Time, bondedAmt math.Int, notBondedAmt math.Int) {
// UNDELEGATE
// Save original bonded and unbonded amounts
bondedAmt1 := bankKeeper.GetBalance(ctx, stakingKeeper.GetBondedPool(ctx).GetAddress(), bondDenom).Amount
notBondedAmt1 := bankKeeper.GetBalance(ctx, stakingKeeper.GetNotBondedPool(ctx).GetAddress(), bondDenom).Amount
var err error
completionTime, err = stakingKeeper.Undelegate(ctx, addrDels[0], addrVals[0], sdk.NewDec(1))
require.NoError(t, err)
// check that the unbonding actually happened
bondedAmt2 := bankKeeper.GetBalance(ctx, stakingKeeper.GetBondedPool(ctx).GetAddress(), bondDenom).Amount
notBondedAmt2 := bankKeeper.GetBalance(ctx, stakingKeeper.GetNotBondedPool(ctx).GetAddress(), bondDenom).Amount
// Bonded amount is less
require.True(math.IntEq(t, bondedAmt1.SubRaw(1), bondedAmt2))
// Unbonded amount is more
require.True(math.IntEq(t, notBondedAmt1.AddRaw(1), notBondedAmt2))
// Check that the unbonding happened- we look up the entry and see that it has the correct number of shares
unbondingDelegations := stakingKeeper.GetUnbondingDelegationsFromValidator(ctx, addrVals[0])
require.Equal(t, math.NewInt(1), unbondingDelegations[0].Entries[0].Balance)
// check that our hook was called
require.True(t, *hookCalled)
return completionTime, bondedAmt2, notBondedAmt2
}
func doRedelegation(
t *testing.T,
stakingKeeper *stakingkeeper.Keeper,
ctx sdk.Context,
addrDels []sdk.AccAddress,
addrVals []sdk.ValAddress,
hookCalled *bool,
) (completionTime time.Time) {
var err error
completionTime, err = stakingKeeper.BeginRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1], sdk.NewDec(1))
require.NoError(t, err)
// Check that the redelegation happened- we look up the entry and see that it has the correct number of shares
redelegations := stakingKeeper.GetRedelegationsFromSrcValidator(ctx, addrVals[0])
require.Equal(t, 1, len(redelegations))
require.Equal(t, sdk.NewDec(1), redelegations[0].Entries[0].SharesDst)
// check that our hook was called
require.True(t, *hookCalled)
return completionTime
}
func doValidatorUnbonding(
t *testing.T,
stakingKeeper *stakingkeeper.Keeper,
ctx sdk.Context,
addrVal sdk.ValAddress,
hookCalled *bool,
) (validator types.Validator) {
validator, found := stakingKeeper.GetValidator(ctx, addrVal)
require.True(t, found)
// Check that status is bonded
require.Equal(t, types.BondStatus(3), validator.Status)
validator, err := stakingKeeper.BeginUnbondingValidator(ctx, validator)
require.NoError(t, err)
// Check that status is unbonding
require.Equal(t, types.BondStatus(2), validator.Status)
// check that our hook was called
require.True(t, *hookCalled)
return validator
}
func TestValidatorUnbondingOnHold1(t *testing.T) {
var (
hookCalled bool
ubdeID uint64
)
_, app, ctx := createTestInput(t)
_, _, addrVals := SetupUnbondingTests(t, app, ctx, &hookCalled, &ubdeID)
// Start unbonding first validator
validator := doValidatorUnbonding(t, app.StakingKeeper, ctx, addrVals[0], &hookCalled)
completionTime := validator.UnbondingTime
completionHeight := validator.UnbondingHeight
// CONSUMER CHAIN'S UNBONDING PERIOD ENDS - STOPPED UNBONDING CAN NOW COMPLETE
err := app.StakingKeeper.UnbondingCanComplete(ctx, ubdeID)
require.NoError(t, err)
// Try to unbond validator
app.StakingKeeper.UnbondAllMatureValidators(ctx)
// Check that validator unbonding is not complete (is not mature yet)
validator, found := app.StakingKeeper.GetValidator(ctx, addrVals[0])
require.True(t, found)
require.Equal(t, types.Unbonding, validator.Status)
unbondingVals := app.StakingKeeper.GetUnbondingValidators(ctx, completionTime, completionHeight)
require.Equal(t, 1, len(unbondingVals))
require.Equal(t, validator.OperatorAddress, unbondingVals[0])
// PROVIDER CHAIN'S UNBONDING PERIOD ENDS - BUT UNBONDING CANNOT COMPLETE
ctx = ctx.WithBlockTime(completionTime.Add(time.Duration(1)))
ctx = ctx.WithBlockHeight(completionHeight + 1)
app.StakingKeeper.UnbondAllMatureValidators(ctx)
// Check that validator unbonding is complete
validator, found = app.StakingKeeper.GetValidator(ctx, addrVals[0])
require.True(t, found)
require.Equal(t, types.Unbonded, validator.Status)
unbondingVals = app.StakingKeeper.GetUnbondingValidators(ctx, completionTime, completionHeight)
require.Equal(t, 0, len(unbondingVals))
}
func TestValidatorUnbondingOnHold2(t *testing.T) {
var (
hookCalled bool
ubdeID uint64
ubdeIDs []uint64
)
_, app, ctx := createTestInput(t)
_, _, addrVals := SetupUnbondingTests(t, app, ctx, &hookCalled, &ubdeID)
// Start unbonding first validator
validator1 := doValidatorUnbonding(t, app.StakingKeeper, ctx, addrVals[0], &hookCalled)
ubdeIDs = append(ubdeIDs, ubdeID)
// Reset hookCalled flag
hookCalled = false
// Start unbonding second validator
validator2 := doValidatorUnbonding(t, app.StakingKeeper, ctx, addrVals[1], &hookCalled)
ubdeIDs = append(ubdeIDs, ubdeID)
// Check that there are two unbonding operations
require.Equal(t, 2, len(ubdeIDs))
// Check that both validators have same unbonding time
require.Equal(t, validator1.UnbondingTime, validator2.UnbondingTime)
completionTime := validator1.UnbondingTime
completionHeight := validator1.UnbondingHeight
// PROVIDER CHAIN'S UNBONDING PERIOD ENDS - BUT UNBONDING CANNOT COMPLETE
ctx = ctx.WithBlockTime(completionTime.Add(time.Duration(1)))
ctx = ctx.WithBlockHeight(completionHeight + 1)
app.StakingKeeper.UnbondAllMatureValidators(ctx)
// Check that unbonding is not complete for both validators
validator1, found := app.StakingKeeper.GetValidator(ctx, addrVals[0])
require.True(t, found)
require.Equal(t, types.Unbonding, validator1.Status)
validator2, found = app.StakingKeeper.GetValidator(ctx, addrVals[1])
require.True(t, found)
require.Equal(t, types.Unbonding, validator2.Status)
unbondingVals := app.StakingKeeper.GetUnbondingValidators(ctx, completionTime, completionHeight)
require.Equal(t, 2, len(unbondingVals))
require.Equal(t, validator1.OperatorAddress, unbondingVals[0])
require.Equal(t, validator2.OperatorAddress, unbondingVals[1])
// CONSUMER CHAIN'S UNBONDING PERIOD ENDS - STOPPED UNBONDING CAN NOW COMPLETE
err := app.StakingKeeper.UnbondingCanComplete(ctx, ubdeIDs[0])
require.NoError(t, err)
// Try again to unbond validators
app.StakingKeeper.UnbondAllMatureValidators(ctx)
// Check that unbonding is complete for validator1, but not for validator2
validator1, found = app.StakingKeeper.GetValidator(ctx, addrVals[0])
require.True(t, found)
require.Equal(t, types.Unbonded, validator1.Status)
validator2, found = app.StakingKeeper.GetValidator(ctx, addrVals[1])
require.True(t, found)
require.Equal(t, types.Unbonding, validator2.Status)
unbondingVals = app.StakingKeeper.GetUnbondingValidators(ctx, completionTime, completionHeight)
require.Equal(t, 1, len(unbondingVals))
require.Equal(t, validator2.OperatorAddress, unbondingVals[0])
// Unbonding for validator2 can complete
err = app.StakingKeeper.UnbondingCanComplete(ctx, ubdeIDs[1])
require.NoError(t, err)
// Try again to unbond validators
app.StakingKeeper.UnbondAllMatureValidators(ctx)
// Check that unbonding is complete for validator2
validator2, found = app.StakingKeeper.GetValidator(ctx, addrVals[1])
require.True(t, found)
require.Equal(t, types.Unbonded, validator2.Status)
unbondingVals = app.StakingKeeper.GetUnbondingValidators(ctx, completionTime, completionHeight)
require.Equal(t, 0, len(unbondingVals))
}
func TestRedelegationOnHold1(t *testing.T) {
var (
hookCalled bool
ubdeID uint64
)
_, app, ctx := createTestInput(t)
_, addrDels, addrVals := SetupUnbondingTests(t, app, ctx, &hookCalled, &ubdeID)
completionTime := doRedelegation(t, app.StakingKeeper, ctx, addrDels, addrVals, &hookCalled)
// CONSUMER CHAIN'S UNBONDING PERIOD ENDS - BUT UNBONDING CANNOT COMPLETE
err := app.StakingKeeper.UnbondingCanComplete(ctx, ubdeID)
require.NoError(t, err)
// Redelegation is not complete - still exists
redelegations := app.StakingKeeper.GetRedelegationsFromSrcValidator(ctx, addrVals[0])
require.Equal(t, 1, len(redelegations))
// PROVIDER CHAIN'S UNBONDING PERIOD ENDS - STOPPED UNBONDING CAN NOW COMPLETE
ctx = ctx.WithBlockTime(completionTime)
_, err = app.StakingKeeper.CompleteRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1])
require.NoError(t, err)
// Redelegation is complete and record is gone
redelegations = app.StakingKeeper.GetRedelegationsFromSrcValidator(ctx, addrVals[0])
require.Equal(t, 0, len(redelegations))
}
func TestRedelegationOnHold2(t *testing.T) {
var (
hookCalled bool
ubdeID uint64
)
_, app, ctx := createTestInput(t)
_, addrDels, addrVals := SetupUnbondingTests(t, app, ctx, &hookCalled, &ubdeID)
completionTime := doRedelegation(t, app.StakingKeeper, ctx, addrDels, addrVals, &hookCalled)
// PROVIDER CHAIN'S UNBONDING PERIOD ENDS - BUT UNBONDING CANNOT COMPLETE
ctx = ctx.WithBlockTime(completionTime)
_, err := app.StakingKeeper.CompleteRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1])
require.NoError(t, err)
// Redelegation is not complete - still exists
redelegations := app.StakingKeeper.GetRedelegationsFromSrcValidator(ctx, addrVals[0])
require.Equal(t, 1, len(redelegations))
// CONSUMER CHAIN'S UNBONDING PERIOD ENDS - STOPPED UNBONDING CAN NOW COMPLETE
err = app.StakingKeeper.UnbondingCanComplete(ctx, ubdeID)
require.NoError(t, err)
// Redelegation is complete and record is gone
redelegations = app.StakingKeeper.GetRedelegationsFromSrcValidator(ctx, addrVals[0])
require.Equal(t, 0, len(redelegations))
}
func TestUnbondingDelegationOnHold1(t *testing.T) {
var (
hookCalled bool
ubdeID uint64
)
_, app, ctx := createTestInput(t)
bondDenom, addrDels, addrVals := SetupUnbondingTests(t, app, ctx, &hookCalled, &ubdeID)
completionTime, bondedAmt1, notBondedAmt1 := doUnbondingDelegation(t, app.StakingKeeper, app.BankKeeper, ctx, bondDenom, addrDels, addrVals, &hookCalled)
// CONSUMER CHAIN'S UNBONDING PERIOD ENDS - BUT UNBONDING CANNOT COMPLETE
err := app.StakingKeeper.UnbondingCanComplete(ctx, ubdeID)
require.NoError(t, err)
bondedAmt3 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetBondedPool(ctx).GetAddress(), bondDenom).Amount
notBondedAmt3 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetNotBondedPool(ctx).GetAddress(), bondDenom).Amount
// Bonded and unbonded amounts are the same as before because the completionTime has not yet passed and so the
// unbondingDelegation has not completed
require.True(math.IntEq(t, bondedAmt1, bondedAmt3))
require.True(math.IntEq(t, notBondedAmt1, notBondedAmt3))
// PROVIDER CHAIN'S UNBONDING PERIOD ENDS - STOPPED UNBONDING CAN NOW COMPLETE
ctx = ctx.WithBlockTime(completionTime)
_, err = app.StakingKeeper.CompleteUnbonding(ctx, addrDels[0], addrVals[0])
require.NoError(t, err)
// Check that the unbonding was finally completed
bondedAmt5 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetBondedPool(ctx).GetAddress(), bondDenom).Amount
notBondedAmt5 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetNotBondedPool(ctx).GetAddress(), bondDenom).Amount
require.True(math.IntEq(t, bondedAmt1, bondedAmt5))
// Not bonded amount back to what it was originaly
require.True(math.IntEq(t, notBondedAmt1.SubRaw(1), notBondedAmt5))
}
func TestUnbondingDelegationOnHold2(t *testing.T) {
var (
hookCalled bool
ubdeID uint64
)
_, app, ctx := createTestInput(t)
bondDenom, addrDels, addrVals := SetupUnbondingTests(t, app, ctx, &hookCalled, &ubdeID)
completionTime, bondedAmt1, notBondedAmt1 := doUnbondingDelegation(t, app.StakingKeeper, app.BankKeeper, ctx, bondDenom, addrDels, addrVals, &hookCalled)
// PROVIDER CHAIN'S UNBONDING PERIOD ENDS - BUT UNBONDING CANNOT COMPLETE
ctx = ctx.WithBlockTime(completionTime)
_, err := app.StakingKeeper.CompleteUnbonding(ctx, addrDels[0], addrVals[0])
require.NoError(t, err)
bondedAmt3 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetBondedPool(ctx).GetAddress(), bondDenom).Amount
notBondedAmt3 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetNotBondedPool(ctx).GetAddress(), bondDenom).Amount
// Bonded and unbonded amounts are the same as before because the completionTime has not yet passed and so the
// unbondingDelegation has not completed
require.True(math.IntEq(t, bondedAmt1, bondedAmt3))
require.True(math.IntEq(t, notBondedAmt1, notBondedAmt3))
// CONSUMER CHAIN'S UNBONDING PERIOD ENDS - STOPPED UNBONDING CAN NOW COMPLETE
err = app.StakingKeeper.UnbondingCanComplete(ctx, ubdeID)
require.NoError(t, err)
// Check that the unbonding was finally completed
bondedAmt5 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetBondedPool(ctx).GetAddress(), bondDenom).Amount
notBondedAmt5 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetNotBondedPool(ctx).GetAddress(), bondDenom).Amount
require.True(math.IntEq(t, bondedAmt1, bondedAmt5))
// Not bonded amount back to what it was originaly
require.True(math.IntEq(t, notBondedAmt1.SubRaw(1), notBondedAmt5))
}