-
Notifications
You must be signed in to change notification settings - Fork 165
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
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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)) | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
} | ||
// 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 | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should have been more specific. |
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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.
I guess the 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 | ||
} | ||
}) | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 given1000000
tokens and thus1
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.