Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

epoching: fuzz tests on epoched undelegations and redelegations #66

Merged
merged 4 commits into from
Jul 21, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ func SetupWithGenesisValSet(t *testing.T, valSet *tmtypes.ValidatorSet, genAccs
validators := make([]stakingtypes.Validator, 0, len(valSet.Validators))
delegations := make([]stakingtypes.Delegation, 0, len(valSet.Validators))

bondAmt := sdk.NewInt(1000000)
// sdk.DefaultPowerReduction is 1 unit of voting power, which by default needs 1000000 tokens
bondAmt := sdk.DefaultPowerReduction.MulRaw(1000)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is 1,000,000,000 tokens, enough to survive 1000 reductions, whereas the previous wiped out the validator in one go?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly. Cosmos SDK specifies DefaultPowerReduction = 1000000. In the previous implementation, each genesis validator is given 1000000 tokens and thus 1 unit of voting power. Any undelegation with more than >=1 tokens will make the voting power to be zero and thus wipes out this validator.


for _, val := range valSet.Validators {
pk, err := cryptocodec.FromTmPubKeyInterface(val.PubKey)
Expand Down
163 changes: 149 additions & 14 deletions x/epoching/keeper/epoch_msg_queue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ import (
)

var (
coin100 = sdk.NewInt64Coin(sdk.DefaultBondDenom, 100)
coin50 = sdk.NewInt64Coin(sdk.DefaultBondDenom, 50)
// val1 = sdk.ValAddress("_____validator1_____")
// val2 = sdk.ValAddress("_____validator2_____")
// val3 = sdk.ValAddress("_____validator3_____")
coinWithOnePower = sdk.NewInt64Coin(sdk.DefaultBondDenom, sdk.DefaultPowerReduction.Int64())
)

// FuzzEnqueueMsg tests EnqueueMsg. It enqueues some wrapped msgs, and check if the message queue includes the enqueued msgs or not
Expand Down Expand Up @@ -59,8 +55,8 @@ func FuzzEnqueueMsg(f *testing.F) {
})
}

// FuzzHandleQueueMsg tests HandleQueueMsg over MsgWrappedDelegate.
// It enqueues some MsgWrappedDelegate, enters a new epoch (which triggers HandleQueueMsg), and check if the message queue is consistent or not, and if the queue msgs are processed or not.
// FuzzHandleQueuedMsg_MsgWrappedDelegate tests HandleQueueMsg over MsgWrappedDelegate.
// It enqueues some MsgWrappedDelegate, enters a new epoch (which triggers HandleQueueMsg), and check if the newly delegated tokens take effect or not
func FuzzHandleQueuedMsg_MsgWrappedDelegate(f *testing.F) {
f.Add(int64(11111))
f.Add(int64(22222))
Expand All @@ -84,14 +80,14 @@ func FuzzHandleQueuedMsg_MsgWrappedDelegate(f *testing.F) {
require.NotEmpty(t, genAccs)
genAddr := genAccs[0].GetAddress()

// generate a random number of validators
numNewVals := rand.Intn(1000)
for i := 0; i < numNewVals; i++ {
helper.WrappedDelegate(genAddr, val, coin50.Amount)
// delegate a random amount of tokens to the validator
numNewVals := rand.Int63n(1000) + 1
for i := int64(0); i < numNewVals; i++ {
helper.WrappedDelegate(genAddr, val, coinWithOnePower.Amount)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if it's really worth doing this 1000 times at the extreme end. You could randomise the amount and the validator to delegate to instead. I mean, what do you learn if you do the exact same thing 10 times vs 900 times?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we can also randomise the amount to achieve the same goal. The only advantage of randomising the number of delegation requests is to also fuzz the delegation/undelegation/redelegation queues. This is also how I found the MaxEntries thing.

}
// ensure the msgs are queued
epochMsgs := keeper.GetEpochMsgs(ctx)
require.Equal(t, numNewVals, len(epochMsgs))
require.Equal(t, numNewVals, int64(len(epochMsgs)))

// enter the 1st block and thus epoch 1
// Note that we missed epoch 0's BeginBlock/EndBlock and thus EpochMsgs are not handled
Expand All @@ -110,7 +106,146 @@ func FuzzHandleQueuedMsg_MsgWrappedDelegate(f *testing.F) {
// ensure the voting power has been added w.r.t. the newly delegated tokens
valPower2, err := helper.EpochingKeeper.GetValidatorVotingPower(ctx, 2, val)
require.NoError(t, err)
addedPoWer := helper.StakingKeeper.TokensToConsensusPower(ctx, sdk.NewInt(int64(numNewVals*50)))
require.Equal(t, valPower+addedPoWer, valPower2)
addedPower := helper.StakingKeeper.TokensToConsensusPower(ctx, coinWithOnePower.Amount.MulRaw(numNewVals))
require.Equal(t, valPower+addedPower, valPower2)
})
}

// FuzzHandleQueuedMsg_MsgWrappedUndelegate tests HandleQueueMsg over MsgWrappedUndelegate.
// It enqueues some MsgWrappedUndelegate, enters a new epoch (which triggers HandleQueueMsg), and check if the tokens become unbonding or not
func FuzzHandleQueuedMsg_MsgWrappedUndelegate(f *testing.F) {
f.Add(int64(11111))
f.Add(int64(22222))
f.Add(int64(55555))
f.Add(int64(12312))

f.Fuzz(func(t *testing.T, seed int64) {
rand.Seed(seed)

helper := testepoching.NewHelperWithValSet(t)
ctx, keeper, genAccs := helper.Ctx, helper.EpochingKeeper, helper.GenAccs
valSet0 := helper.EpochingKeeper.GetValidatorSet(helper.Ctx, 0)

// validator to be undelegated
val := valSet0[0].Addr
valPower, err := helper.EpochingKeeper.GetValidatorVotingPower(ctx, 0, val)
require.NoError(t, err)

// get genesis account's address, whose holder will be the delegator
require.NotNil(t, genAccs)
require.NotEmpty(t, genAccs)
genAddr := genAccs[0].GetAddress()

// unbond a random amount of tokens from the validator
numNewVals := rand.Int63n(7) + 1 // numNewVals \in [1, 7] since UBD queue contains at most DefaultMaxEntries=7 validators
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh really? What happens after 7 entries? Can the unbonding validator abuse it somehow?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should have been more specific. MaxEntries means that for any pair of delegatorAddr and validatorAddr, there can be <=MaxEntries concurrent undelegations at any time slot. If this delegator submits more than MaxEntries delegation requests, then only MaxEntries of them will be processed and others will be rejected. This is processed in https://github.com/cosmos/cosmos-sdk/blob/v0.45.5/x/staking/keeper/delegation.go#L814-L816. Will update the comments to make it more clear.

for i := int64(0); i < numNewVals; i++ {
helper.WrappedUndelegate(genAddr, val, coinWithOnePower.Amount)
}
// ensure the msgs are queued
epochMsgs := keeper.GetEpochMsgs(ctx)
require.Equal(t, numNewVals, int64(len(epochMsgs)))

// enter the 1st block and thus epoch 1
// Note that we missed epoch 0's BeginBlock/EndBlock and thus EpochMsgs are not handled
ctx = helper.GenAndApplyEmptyBlock()
// enter epoch 2
for i := uint64(0); i < keeper.GetParams(ctx).EpochInterval; i++ {
ctx = helper.GenAndApplyEmptyBlock()
}

// ensure queued msgs have been handled
queueLen := keeper.GetQueueLength(ctx)
require.Equal(t, uint64(0), queueLen)
epochMsgs = keeper.GetEpochMsgs(ctx)
require.Equal(t, 0, len(epochMsgs))

// ensure the voting power has been reduced w.r.t. the unbonding tokens
valPower2, err := helper.EpochingKeeper.GetValidatorVotingPower(ctx, 2, val)
require.NoError(t, err)
reducedPower := helper.StakingKeeper.TokensToConsensusPower(ctx, coinWithOnePower.Amount.MulRaw(numNewVals))
require.Equal(t, valPower-reducedPower, valPower2)

// ensure the genesis account has these unbonding tokens
unbondingDels := helper.StakingKeeper.GetAllUnbondingDelegations(ctx, genAddr)
require.Equal(t, 1, len(unbondingDels)) // there is only 1 type of tokens
require.Equal(t, numNewVals, int64(len(unbondingDels[0].Entries))) // there are numNewVals entries
for _, entry := range unbondingDels[0].Entries {
require.Equal(t, coinWithOnePower.Amount, entry.Balance) // each unbonding delegation entry has tokens of 1 voting power
}
})
}

// FuzzHandleQueuedMsg_MsgWrappedBeginRedelegate tests HandleQueueMsg over MsgWrappedBeginRedelegate.
// It enqueues some MsgWrappedBeginRedelegate, enters a new epoch (which triggers HandleQueueMsg), and check if the tokens become unbonding or not
func FuzzHandleQueuedMsg_MsgWrappedBeginRedelegate(f *testing.F) {
f.Add(int64(11111))
f.Add(int64(22222))
f.Add(int64(55555))
f.Add(int64(12312))

f.Fuzz(func(t *testing.T, seed int64) {
rand.Seed(seed)

helper := testepoching.NewHelperWithValSet(t)
ctx, keeper, genAccs := helper.Ctx, helper.EpochingKeeper, helper.GenAccs
valSet0 := helper.EpochingKeeper.GetValidatorSet(helper.Ctx, 0)

// 2 validators, where the operator will redelegate some token from val1 to val2
val1 := valSet0[0].Addr
val1Power, err := helper.EpochingKeeper.GetValidatorVotingPower(ctx, 0, val1)
require.NoError(t, err)
val2 := valSet0[1].Addr
val2Power, err := helper.EpochingKeeper.GetValidatorVotingPower(ctx, 0, val2)
require.NoError(t, err)
require.Equal(t, val1Power, val2Power)

// get genesis account's address, whose holder will be the delegator
require.NotNil(t, genAccs)
require.NotEmpty(t, genAccs)
genAddr := genAccs[0].GetAddress()

// redelegate a random amount of tokens from val1 to val2
numNewVals := rand.Int63n(7) + 1 // numNewVals \in [1, 7] since UBD queue contains at most DefaultMaxEntries=7 validators
for i := int64(0); i < numNewVals; i++ {
helper.WrappedBeginRedelegate(genAddr, val1, val2, coinWithOnePower.Amount)
}
// ensure the msgs are queued
epochMsgs := keeper.GetEpochMsgs(ctx)
require.Equal(t, numNewVals, int64(len(epochMsgs)))

// enter the 1st block and thus epoch 1
// Note that we missed epoch 0's BeginBlock/EndBlock and thus EpochMsgs are not handled
ctx = helper.GenAndApplyEmptyBlock()
// enter epoch 2
for i := uint64(0); i < keeper.GetParams(ctx).EpochInterval; i++ {
ctx = helper.GenAndApplyEmptyBlock()
}

// ensure queued msgs have been handled
queueLen := keeper.GetQueueLength(ctx)
require.Equal(t, uint64(0), queueLen)
epochMsgs = keeper.GetEpochMsgs(ctx)
require.Equal(t, 0, len(epochMsgs))

// ensure the voting power has been redelegated from val1 to val2
// Note that in Babylon, redelegation happens unconditionally upon `EndEpoch`, rather than upon checkpointed. Meanwhile in Cosmos SDK, redelegation happens upon `EndBlock`.
// This is because slashable security only requires `unbonding` -> `unbonded` to depend on checkpoints, and redelegation does not unbond any stake from the system.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that no tokens enter the unbonding queue during a BeginRedelegate, because they just get moved between validators? If so, why this this comment still say that the unbonding queue can take at most 7 items?

Copy link
Member Author

@SebastianElvis SebastianElvis Jul 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that no tokens enter the unbonding queue during a BeginRedelegate, because they just get moved between validators?

Exactly. Upon redelegations, there is no token leaving the system, unlike upon undelegations. Thus, redelegations do not affect PoS security, in both PoS and checkpointed PoS.

If so, why this this comment still say that the unbonding queue can take at most 7 items?

I guess the MaxEntries thing aims at some sort of rate limiting, specifying that a validator operator should not spam undelegation/redelegation requests.

I will add more documentations to make this clear.

val1Power2, err := helper.EpochingKeeper.GetValidatorVotingPower(ctx, 2, val1)
require.NoError(t, err)
val2Power2, err := helper.EpochingKeeper.GetValidatorVotingPower(ctx, 2, val2)
require.NoError(t, err)
redelegatedPower := helper.StakingKeeper.TokensToConsensusPower(ctx, coinWithOnePower.Amount.MulRaw(numNewVals))
// ensure the voting power of val1 has reduced
require.Equal(t, val1Power-redelegatedPower, val1Power2)
// ensure the voting power of val2 has increased
require.Equal(t, val2Power+redelegatedPower, val2Power2)

// ensure the genesis account has these redelegating tokens
redelegations := helper.StakingKeeper.GetAllRedelegations(ctx, genAddr, val1, val2)
require.Equal(t, 1, len(redelegations)) // there is only 1 type of tokens
require.Equal(t, numNewVals, int64(len(redelegations[0].Entries))) // there are numNewVals entries
for _, entry := range redelegations[0].Entries {
require.Equal(t, coinWithOnePower.Amount, entry.InitialBalance) // each redelegating entry has tokens of 1 voting power
}
})
}
11 changes: 6 additions & 5 deletions x/epoching/testepoching/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ func NewHelperWithValSet(t *testing.T) *Helper {
// generate the genesis account
senderPrivKey := secp256k1.GenPrivKey()
acc := authtypes.NewBaseAccount(senderPrivKey.PubKey().Address().Bytes(), senderPrivKey.PubKey(), 0, 0)
// ensure the genesis account has a sufficient amount of tokens
balance := banktypes.Balance{
Address: acc.GetAddress().String(),
Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100000000000000))),
Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.DefaultPowerReduction.MulRaw(10000000))),
}
GenAccs := []authtypes.GenesisAccount{acc}

Expand Down Expand Up @@ -149,19 +150,19 @@ func (h *Helper) WrappedDelegateWithPower(delegator sdk.AccAddress, val sdk.ValA
}

// WrappedUndelegate calls handler to unbound some stake from a validator.
func (h *Helper) WrappedUndelegate(delegator sdk.AccAddress, val sdk.ValAddress, amount sdk.Int, ok bool) *sdk.Result {
func (h *Helper) WrappedUndelegate(delegator sdk.AccAddress, val sdk.ValAddress, amount sdk.Int) *sdk.Result {
unbondAmt := sdk.NewCoin(sdk.DefaultBondDenom, amount)
msg := stakingtypes.NewMsgUndelegate(delegator, val, unbondAmt)
wmsg := types.NewMsgWrappedUndelegate(msg)
return h.Handle(wmsg, ok)
return h.Handle(wmsg, true)
}

// WrappedBeginRedelegate calls handler to redelegate some stake from a validator to another
func (h *Helper) WrappedBeginRedelegate(delegator sdk.AccAddress, srcVal sdk.ValAddress, dstVal sdk.ValAddress, amount sdk.Int, ok bool) *sdk.Result {
func (h *Helper) WrappedBeginRedelegate(delegator sdk.AccAddress, srcVal sdk.ValAddress, dstVal sdk.ValAddress, amount sdk.Int) *sdk.Result {
unbondAmt := sdk.NewCoin(sdk.DefaultBondDenom, amount)
msg := stakingtypes.NewMsgBeginRedelegate(delegator, srcVal, dstVal, unbondAmt)
wmsg := types.NewMsgWrappedBeginRedelegate(msg)
return h.Handle(wmsg, ok)
return h.Handle(wmsg, true)
}

// Handle calls epoching handler on a given message
Expand Down
2 changes: 1 addition & 1 deletion x/epoching/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ var (
ErrUnknownEpochNumber = sdkerrors.Register(ModuleName, 3, "the epoch number is not known in DB")
ErrUnknownQueueLen = sdkerrors.Register(ModuleName, 4, "the msg queue length is not known in DB")
ErrUnknownSlashedVotingPower = sdkerrors.Register(ModuleName, 5, "the slashed voting power is not known in DB. Maybe the epoch has been checkpointed?")
ErrUnknownValidator = sdkerrors.Register(ModuleName, 6, "the slashed validator is not in the validator set.")
ErrUnknownValidator = sdkerrors.Register(ModuleName, 6, "the validator is not known in the validator set.")
ErrUnknownTotalVotingPower = sdkerrors.Register(ModuleName, 7, "the total voting power is not known in DB.")
ErrMarshal = sdkerrors.Register(ModuleName, 8, "marshal error.")
ErrUnmarshal = sdkerrors.Register(ModuleName, 9, "unmarshal error.")
Expand Down