diff --git a/log/logger_test.go b/log/logger_test.go index 9c877a0ca4..36c7bee385 100644 --- a/log/logger_test.go +++ b/log/logger_test.go @@ -6,9 +6,10 @@ import ( "strings" "testing" - "cosmossdk.io/log" "github.com/rs/zerolog" "gotest.tools/v3/assert" + + "cosmossdk.io/log" ) func TestLoggerOptionStackTrace(t *testing.T) { diff --git a/tests/integration/evidence/keeper/infraction_test.go b/tests/integration/evidence/keeper/infraction_test.go index 875e4b8b87..176ae20b38 100644 --- a/tests/integration/evidence/keeper/infraction_test.go +++ b/tests/integration/evidence/keeper/infraction_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "bytes" "context" "encoding/hex" "fmt" @@ -28,6 +29,7 @@ import ( "cosmossdk.io/x/evidence/keeper" evidencetypes "cosmossdk.io/x/evidence/types" minttypes "cosmossdk.io/x/mint/types" + pooltypes "cosmossdk.io/x/protocolpool/types" "cosmossdk.io/x/slashing" slashingkeeper "cosmossdk.io/x/slashing/keeper" "cosmossdk.io/x/slashing/testutil" @@ -94,6 +96,7 @@ func initFixture(tb testing.TB) *fixture { authority := authtypes.NewModuleAddress("gov") maccPerms := map[string][]string{ + pooltypes.ModuleName: {}, minttypes.ModuleName: {authtypes.Minter}, stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, @@ -125,7 +128,9 @@ func initFixture(tb testing.TB) *fixture { slashingKeeper := slashingkeeper.NewKeeper(cdc, codec.NewLegacyAmino(), runtime.NewKVStoreService(keys[slashingtypes.StoreKey]), stakingKeeper, authority.String()) - evidenceKeeper := keeper.NewKeeper(cdc, runtime.NewKVStoreService(keys[evidencetypes.StoreKey]), stakingKeeper, slashingKeeper, addresscodec.NewBech32Codec("cosmos")) + stakingKeeper.SetHooks(stakingtypes.NewMultiStakingHooks(slashingKeeper.Hooks())) + + evidenceKeeper := keeper.NewKeeper(cdc, runtime.NewKVStoreService(keys[evidencetypes.StoreKey]), stakingKeeper, slashingKeeper, addresscodec.NewBech32Codec(sdk.Bech32PrefixAccAddr)) router := evidencetypes.NewRouter() router = router.AddRoute(evidencetypes.RouteEquivocation, testEquivocationHandler(evidenceKeeper)) evidenceKeeper.SetRouter(router) @@ -310,6 +315,123 @@ func TestHandleDoubleSign_TooOld(t *testing.T) { assert.Assert(t, f.slashingKeeper.IsTombstoned(ctx, sdk.ConsAddress(valpubkey.Address())) == false) } +func TestHandleDoubleSignAfterRotation(t *testing.T) { + t.Parallel() + f := initFixture(t) + + ctx := f.sdkCtx.WithIsCheckTx(false).WithBlockHeight(1).WithHeaderInfo(header.Info{Time: time.Now()}) + populateValidators(t, f) + + power := int64(100) + stakingParams, err := f.stakingKeeper.Params.Get(ctx) + assert.NilError(t, err) + + operatorAddr, valpubkey := valAddresses[0], pubkeys[0] + tstaking := stakingtestutil.NewHelper(t, ctx, f.stakingKeeper) + + selfDelegation := tstaking.CreateValidatorWithValPower(operatorAddr, valpubkey, power, true) + + // execute end-blocker and verify validator attributes + _, err = f.stakingKeeper.EndBlocker(ctx) + assert.NilError(t, err) + + assert.DeepEqual(t, + f.bankKeeper.GetAllBalances(ctx, sdk.AccAddress(operatorAddr)).String(), + sdk.NewCoins(sdk.NewCoin(stakingParams.BondDenom, initAmt.Sub(selfDelegation))).String(), + ) + + valInfo, err := f.stakingKeeper.Validator(ctx, operatorAddr) + assert.NilError(t, err) + consAddrBeforeRotn, err := valInfo.GetConsAddr() + + assert.NilError(t, err) + assert.DeepEqual(t, selfDelegation, valInfo.GetBondedTokens()) + + NewConsPubkey := newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB53") + + msgServer := stakingkeeper.NewMsgServerImpl(f.stakingKeeper) + msg, err := stakingtypes.NewMsgRotateConsPubKey(operatorAddr.String(), NewConsPubkey) + assert.NilError(t, err) + _, err = msgServer.RotateConsPubKey(ctx, msg) + assert.NilError(t, err) + + // execute end-blocker and verify validator attributes + _, err = f.stakingKeeper.EndBlocker(ctx) + assert.NilError(t, err) + + valInfo, err = f.stakingKeeper.Validator(ctx, operatorAddr) + assert.NilError(t, err) + consAddrAfterRotn, err := valInfo.GetConsAddr() + assert.NilError(t, err) + assert.Equal(t, bytes.Equal(consAddrBeforeRotn, consAddrAfterRotn), false) + + // handle a signature to set signing info + err = f.slashingKeeper.HandleValidatorSignature(ctx, NewConsPubkey.Address().Bytes(), selfDelegation.Int64(), comet.BlockIDFlagCommit) + assert.NilError(t, err) + + // double sign less than max age + valInfo, err = f.stakingKeeper.Validator(ctx, operatorAddr) + assert.NilError(t, err) + oldTokens := valInfo.GetTokens() + nci := comet.Info{ + Evidence: []comet.Evidence{{ + Validator: comet.Validator{Address: valpubkey.Address(), Power: power}, + Type: comet.MisbehaviorType(abci.MisbehaviorType_DUPLICATE_VOTE), + Time: time.Unix(0, 0), + Height: 0, + }}, + } + + err = f.evidenceKeeper.BeginBlocker(ctx.WithCometInfo(nci)) + assert.NilError(t, err) + + // should be jailed and tombstoned + valInfo, err = f.stakingKeeper.Validator(ctx, operatorAddr) + assert.NilError(t, err) + assert.Assert(t, valInfo.IsJailed()) + assert.Assert(t, f.slashingKeeper.IsTombstoned(ctx, sdk.ConsAddress(NewConsPubkey.Address()))) + + // tokens should be decreased + valInfo, err = f.stakingKeeper.Validator(ctx, operatorAddr) + assert.NilError(t, err) + newTokens := valInfo.GetTokens() + assert.Assert(t, newTokens.LT(oldTokens)) + + // submit duplicate evidence + err = f.evidenceKeeper.BeginBlocker(ctx.WithCometInfo(nci)) + assert.NilError(t, err) + + // tokens should be the same (capped slash) + valInfo, err = f.stakingKeeper.Validator(ctx, operatorAddr) + assert.NilError(t, err) + assert.Assert(t, valInfo.GetTokens().Equal(newTokens)) + + // jump to past the unbonding period + ctx = ctx.WithHeaderInfo(header.Info{Time: time.Unix(1, 0).Add(stakingParams.UnbondingTime)}) + + // require we cannot unjail + assert.Error(t, f.slashingKeeper.Unjail(ctx, operatorAddr), slashingtypes.ErrValidatorJailed.Error()) + + // require we be able to unbond now + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + del, _ := f.stakingKeeper.Delegations.Get(ctx, collections.Join(sdk.AccAddress(operatorAddr), operatorAddr)) + validator, _ := f.stakingKeeper.GetValidator(ctx, operatorAddr) + totalBond := validator.TokensFromShares(del.GetShares()).TruncateInt() + tstaking.Ctx = ctx + tstaking.Denom = stakingParams.BondDenom + tstaking.Undelegate(sdk.AccAddress(operatorAddr), operatorAddr, totalBond, true) + + // query evidence from store + var evidences []exported.Evidence + assert.NilError(t, f.evidenceKeeper.Evidences.Walk(ctx, nil, func(key []byte, value exported.Evidence) (stop bool, err error) { + evidences = append(evidences, value) + return false, nil + })) + // evidences, err := f.evidenceKeeper.GetAllEvidence(ctx) + assert.NilError(t, err) + assert.Assert(t, len(evidences) == 1) +} + func populateValidators(t assert.TestingT, f *fixture) { // add accounts and set total supply totalSupplyAmt := initAmt.MulRaw(int64(len(valAddresses))) diff --git a/tests/integration/staking/keeper/common_test.go b/tests/integration/staking/keeper/common_test.go index f26884a5a5..da294ed4e6 100644 --- a/tests/integration/staking/keeper/common_test.go +++ b/tests/integration/staking/keeper/common_test.go @@ -18,6 +18,7 @@ import ( bankkeeper "cosmossdk.io/x/bank/keeper" banktypes "cosmossdk.io/x/bank/types" minttypes "cosmossdk.io/x/mint/types" + pooltypes "cosmossdk.io/x/protocolpool/types" "cosmossdk.io/x/staking" stakingkeeper "cosmossdk.io/x/staking/keeper" "cosmossdk.io/x/staking/testutil" @@ -107,6 +108,7 @@ func initFixture(tb testing.TB) *fixture { authority := authtypes.NewModuleAddress("gov") maccPerms := map[string][]string{ + pooltypes.ModuleName: {}, minttypes.ModuleName: {authtypes.Minter}, types.ModuleName: {authtypes.Minter}, types.BondedPoolName: {authtypes.Burner, authtypes.Staking}, diff --git a/tests/integration/staking/keeper/msg_server_test.go b/tests/integration/staking/keeper/msg_server_test.go index 5f6c376039..eadd352b8a 100644 --- a/tests/integration/staking/keeper/msg_server_test.go +++ b/tests/integration/staking/keeper/msg_server_test.go @@ -6,12 +6,15 @@ import ( "gotest.tools/v3/assert" + "cosmossdk.io/core/header" "cosmossdk.io/math" "cosmossdk.io/x/bank/testutil" + pooltypes "cosmossdk.io/x/protocolpool/types" "cosmossdk.io/x/staking/keeper" "cosmossdk.io/x/staking/types" "github.com/cosmos/cosmos-sdk/codec/address" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -175,3 +178,217 @@ func TestCancelUnbondingDelegation(t *testing.T) { }) } } + +func TestRotateConsPubKey(t *testing.T) { + t.Parallel() + f := initFixture(t) + + ctx := f.sdkCtx + stakingKeeper := f.stakingKeeper + bankKeeper := f.bankKeeper + accountKeeper := f.accountKeeper + + msgServer := keeper.NewMsgServerImpl(stakingKeeper) + bondDenom, err := stakingKeeper.BondDenom(ctx) + assert.NilError(t, err) + + params, err := stakingKeeper.Params.Get(ctx) + assert.NilError(t, err) + + params.KeyRotationFee = sdk.NewInt64Coin(bondDenom, 10) + err = stakingKeeper.Params.Set(ctx, params) + assert.NilError(t, err) + + addrs := simtestutil.AddTestAddrsIncremental(bankKeeper, stakingKeeper, ctx, 5, stakingKeeper.TokensFromConsensusPower(ctx, 100)) + valAddrs := simtestutil.ConvertAddrsToValAddrs(addrs) + + // create 5 validators + for i := 0; i < 5; i++ { + comm := types.NewCommissionRates(math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0)) + + msg, err := types.NewMsgCreateValidator(valAddrs[i].String(), PKs[i], sdk.NewCoin(sdk.DefaultBondDenom, stakingKeeper.TokensFromConsensusPower(ctx, 30)), + types.Description{Moniker: "NewVal"}, comm, math.OneInt()) + assert.NilError(t, err) + _, err = msgServer.CreateValidator(ctx, msg) + assert.NilError(t, err) + } + + // call endblocker to update the validator state + _, err = stakingKeeper.EndBlocker(ctx.WithBlockHeight(ctx.BlockHeader().Height + 1)) + assert.NilError(t, err) + + params, err = stakingKeeper.Params.Get(ctx) + assert.NilError(t, err) + + validators, err := stakingKeeper.GetAllValidators(ctx) + assert.NilError(t, err) + assert.Equal(t, len(validators) >= 5, true) + + testCases := []struct { + name string + malleate func() sdk.Context + pass bool + validator string + newPubKey cryptotypes.PubKey + expErrMsg string + expHistoryObjs int + fees sdk.Coin + }{ + { + name: "successful consensus pubkey rotation", + malleate: func() sdk.Context { + return ctx + }, + validator: validators[0].GetOperator(), + newPubKey: PKs[499], + pass: true, + expHistoryObjs: 1, + fees: params.KeyRotationFee, + }, + { + name: "non existing validator check", + malleate: func() sdk.Context { + return ctx + }, + validator: sdk.ValAddress("non_existing_val").String(), + newPubKey: PKs[498], + pass: false, + expErrMsg: "validator does not exist", + }, + { + name: "pubkey already associated with another validator", + malleate: func() sdk.Context { + return ctx + }, + validator: validators[0].GetOperator(), + newPubKey: validators[1].ConsensusPubkey.GetCachedValue().(cryptotypes.PubKey), + pass: false, + expErrMsg: "consensus pubkey is already used for a validator", + }, + { + name: "consensus pubkey rotation limit check", + malleate: func() sdk.Context { + params, err := stakingKeeper.Params.Get(ctx) + assert.NilError(t, err) + + params.KeyRotationFee = sdk.NewInt64Coin(bondDenom, 10) + err = stakingKeeper.Params.Set(ctx, params) + assert.NilError(t, err) + + msg, err := types.NewMsgRotateConsPubKey( + validators[1].GetOperator(), + PKs[498], + ) + assert.NilError(t, err) + _, err = msgServer.RotateConsPubKey(ctx, msg) + assert.NilError(t, err) + + return ctx + }, + validator: validators[1].GetOperator(), + newPubKey: PKs[497], + pass: false, + expErrMsg: "exceeding maximum consensus pubkey rotations within unbonding period", + }, + { + name: "limit reached, but should rotate after the unbonding period", + malleate: func() sdk.Context { + params, err := stakingKeeper.Params.Get(ctx) + assert.NilError(t, err) + + params.KeyRotationFee = sdk.NewInt64Coin(bondDenom, 10) + err = stakingKeeper.Params.Set(ctx, params) + assert.NilError(t, err) + + msg, err := types.NewMsgRotateConsPubKey( + validators[3].GetOperator(), + PKs[495], + ) + + assert.NilError(t, err) + _, err = msgServer.RotateConsPubKey(ctx, msg) + assert.NilError(t, err) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + // this shouldn't remove the existing keys from waiting queue since unbonding time isn't reached + _, err = stakingKeeper.EndBlocker(ctx) + assert.NilError(t, err) + // stakingKeeper.UpdateAllMaturedConsKeyRotatedKeys(ctx, ctx.BlockHeader().Time) + + msg, err = types.NewMsgRotateConsPubKey( + validators[3].GetOperator(), + PKs[494], + ) + + assert.NilError(t, err) + _, err = msgServer.RotateConsPubKey(ctx, msg) + assert.Error(t, err, "exceeding maximum consensus pubkey rotations within unbonding period") + + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + newCtx := ctx.WithHeaderInfo(header.Info{Time: ctx.HeaderInfo().Time.Add(params.UnbondingTime)}) + newCtx = newCtx.WithBlockHeight(newCtx.BlockHeight() + 1) + // this should remove keys from waiting queue since unbonding time is reached + _, err = stakingKeeper.EndBlocker(newCtx) + assert.NilError(t, err) + // stakingKeeper.UpdateAllMaturedConsKeyRotatedKeys(newCtx, newCtx.BlockHeader().Time) + + return newCtx + }, + validator: validators[3].GetOperator(), + newPubKey: PKs[494], + pass: true, + expErrMsg: "", + expHistoryObjs: 2, + fees: params.KeyRotationFee, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + newCtx := testCase.malleate() + oldDistrBalance := bankKeeper.GetBalance(newCtx, accountKeeper.GetModuleAddress(pooltypes.ModuleName), bondDenom) + msg, err := types.NewMsgRotateConsPubKey( + testCase.validator, + testCase.newPubKey, + ) + assert.NilError(t, err) + + _, err = msgServer.RotateConsPubKey(newCtx, msg) + + if testCase.pass { + assert.NilError(t, err) + + _, err = stakingKeeper.EndBlocker(newCtx) + assert.NilError(t, err) + + // rotation fee payment from sender to distrtypes + newDistrBalance := bankKeeper.GetBalance(newCtx, accountKeeper.GetModuleAddress(pooltypes.ModuleName), bondDenom) + assert.DeepEqual(t, newDistrBalance, oldDistrBalance.Add(testCase.fees)) + + valBytes, err := stakingKeeper.ValidatorAddressCodec().StringToBytes(testCase.validator) + assert.NilError(t, err) + + // validator consensus pubkey update check + validator, err := stakingKeeper.GetValidator(newCtx, valBytes) + assert.NilError(t, err) + + consAddr, err := validator.GetConsAddr() + assert.NilError(t, err) + assert.DeepEqual(t, consAddr, testCase.newPubKey.Address().Bytes()) + + // consensus rotation history set check + historyObjects, err := stakingKeeper.GetValidatorConsPubKeyRotationHistory(newCtx, valBytes) + assert.NilError(t, err) + assert.Equal(t, len(historyObjects), testCase.expHistoryObjs) + + historyObjects, err = stakingKeeper.GetBlockConsPubKeyRotationHistory(newCtx) + assert.NilError(t, err) + assert.Equal(t, len(historyObjects), 1) + + } else { + assert.ErrorContains(t, err, testCase.expErrMsg) + } + }) + } +} diff --git a/tests/integration/staking/simulation/operations_test.go b/tests/integration/staking/simulation/operations_test.go index c581db3d12..a5340acee2 100644 --- a/tests/integration/staking/simulation/operations_test.go +++ b/tests/integration/staking/simulation/operations_test.go @@ -146,6 +146,7 @@ func (s *SimTestSuite) TestWeightedOperations() { {simulation.DefaultWeightMsgUndelegate, types.ModuleName, sdk.MsgTypeURL(&types.MsgUndelegate{})}, {simulation.DefaultWeightMsgBeginRedelegate, types.ModuleName, sdk.MsgTypeURL(&types.MsgBeginRedelegate{})}, {simulation.DefaultWeightMsgCancelUnbondingDelegation, types.ModuleName, sdk.MsgTypeURL(&types.MsgCancelUnbondingDelegation{})}, + {simulation.DefaultWeightMsgRotateConsPubKey, types.ModuleName, sdk.MsgTypeURL(&types.MsgRotateConsPubKey{})}, } for i, w := range weightedOps { @@ -367,6 +368,32 @@ func (s *SimTestSuite) TestSimulateMsgBeginRedelegate() { require.Len(futureOperations, 0) } +func (s *SimTestSuite) TestSimulateRotateConsPubKey() { + require := s.Require() + blockTime := time.Now().UTC() + ctx := s.ctx.WithHeaderInfo(header.Info{Time: blockTime}) + + _ = s.getTestingValidator2(ctx) + + // begin a new block + _, err := s.app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: s.app.LastBlockHeight() + 1, Hash: s.app.LastCommitID().Hash, Time: blockTime}) + require.NoError(err) + + // execute operation + op := simulation.SimulateMsgRotateConsPubKey(s.txConfig, s.accountKeeper, s.bankKeeper, s.stakingKeeper) + operationMsg, futureOperations, err := op(s.r, s.app.BaseApp, ctx, s.accounts, "") + require.NoError(err) + + var msg types.MsgRotateConsPubKey + err = proto.Unmarshal(operationMsg.Msg, &msg) + require.NoError(err) + + require.True(operationMsg.OK) + require.Equal(sdk.MsgTypeURL(&types.MsgRotateConsPubKey{}), sdk.MsgTypeURL(&msg)) + require.Equal("cosmosvaloper1p8wcgrjr4pjju90xg6u9cgq55dxwq8j7epjs3u", msg.ValidatorAddress) + require.Len(futureOperations, 0) +} + func (s *SimTestSuite) getTestingValidator0(ctx sdk.Context) types.Validator { commission0 := types.NewCommission(math.LegacyZeroDec(), math.LegacyOneDec(), math.LegacyOneDec()) return s.getTestingValidator(ctx, commission0, 1) @@ -393,6 +420,14 @@ func (s *SimTestSuite) getTestingValidator(ctx sdk.Context, commission types.Com return validator } +func (s *SimTestSuite) getTestingValidator2(ctx sdk.Context) types.Validator { + val := s.getTestingValidator0(ctx) + val.Status = types.Bonded + s.Require().NoError(s.stakingKeeper.SetValidator(ctx, val)) + s.Require().NoError(s.stakingKeeper.SetValidatorByConsAddr(ctx, val)) + return val +} + func (s *SimTestSuite) setupValidatorRewards(ctx sdk.Context, valAddress sdk.ValAddress) { decCoins := sdk.DecCoins{sdk.NewDecCoinFromDec(sdk.DefaultBondDenom, math.LegacyOneDec())} historicalRewards := distrtypes.NewValidatorHistoricalRewards(decCoins, 2) diff --git a/x/slashing/keeper/signing_info_test.go b/x/slashing/keeper/signing_info_test.go index 13d88ca4cf..6b779c49a4 100644 --- a/x/slashing/keeper/signing_info_test.go +++ b/x/slashing/keeper/signing_info_test.go @@ -8,6 +8,7 @@ import ( "cosmossdk.io/x/slashing/testutil" slashingtypes "cosmossdk.io/x/slashing/types" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -111,3 +112,49 @@ func (s *KeeperTestSuite) TestValidatorMissedBlockBitmap_SmallWindow() { require.Len(missedBlocks, int(params.SignedBlocksWindow)-1) } } + +func (s *KeeperTestSuite) TestPerformConsensusPubKeyUpdate() { + ctx, slashingKeeper := s.ctx, s.slashingKeeper + + require := s.Require() + + pks := simtestutil.CreateTestPubKeys(500) + + oldConsAddr := sdk.ConsAddress(pks[0].Address()) + newConsAddr := sdk.ConsAddress(pks[1].Address()) + + newInfo := slashingtypes.NewValidatorSigningInfo( + oldConsAddr.String(), + int64(4), + int64(3), + time.Unix(2, 0).UTC(), + false, + int64(10), + ) + + err := slashingKeeper.ValidatorSigningInfo.Set(ctx, oldConsAddr, newInfo) + require.NoError(err) + + s.stakingKeeper.EXPECT().ValidatorIdentifier(gomock.Any(), oldConsAddr).Return(oldConsAddr, nil) + err = slashingKeeper.SetMissedBlockBitmapValue(ctx, oldConsAddr, 10, true) + require.NoError(err) + + err = slashingKeeper.Hooks().AfterConsensusPubKeyUpdate(ctx, pks[0], pks[1], sdk.Coin{}) + require.NoError(err) + + // check pubkey relation is set properly + savedPubKey, err := slashingKeeper.GetPubkey(ctx, newConsAddr.Bytes()) + require.NoError(err) + require.Equal(savedPubKey, pks[1]) + + // check validator SigningInfo is set properly to new consensus pubkey + signingInfo, err := slashingKeeper.ValidatorSigningInfo.Get(ctx, newConsAddr) + require.NoError(err) + require.Equal(signingInfo, newInfo) + + // missed blocks maps to old cons key only since there is a identifier added to get the missed blocks using the new cons key. + missedBlocks, err := slashingKeeper.GetValidatorMissedBlocks(ctx, oldConsAddr) + require.NoError(err) + + require.Len(missedBlocks, 1) +} diff --git a/x/staking/keeper/cons_pubkey.go b/x/staking/keeper/cons_pubkey.go index c6a9763df3..03e41f2261 100644 --- a/x/staking/keeper/cons_pubkey.go +++ b/x/staking/keeper/cons_pubkey.go @@ -131,8 +131,8 @@ func (k Keeper) ValidatorIdentifier(ctx context.Context, newPk sdk.ConsAddress) return pk, nil } -// exceedsMaxRotations returns true if the key rotations exceed the limit, currently we are limiting one rotation for unbonding period. -func (k Keeper) exceedsMaxRotations(ctx context.Context, valAddr sdk.ValAddress) error { +// ExceedsMaxRotations returns true if the key rotations exceed the limit, currently we are limiting one rotation for unbonding period. +func (k Keeper) ExceedsMaxRotations(ctx context.Context, valAddr sdk.ValAddress) error { count := 0 rng := collections.NewPrefixedPairRange[[]byte, time.Time](valAddr) diff --git a/x/staking/keeper/msg_server.go b/x/staking/keeper/msg_server.go index 0814e4b56b..b45b81099a 100644 --- a/x/staking/keeper/msg_server.go +++ b/x/staking/keeper/msg_server.go @@ -655,7 +655,7 @@ func (k msgServer) RotateConsPubKey(ctx context.Context, msg *types.MsgRotateCon // Check if the validator is exceeding parameter MaxConsPubKeyRotations within the // unbonding period by iterating ConsPubKeyRotationHistory. - err = k.exceedsMaxRotations(ctx, valAddr) + err = k.ExceedsMaxRotations(ctx, valAddr) if err != nil { return nil, err } diff --git a/x/staking/keeper/validator.go b/x/staking/keeper/validator.go index ca3f0391fa..0fd4fb1a07 100644 --- a/x/staking/keeper/validator.go +++ b/x/staking/keeper/validator.go @@ -37,18 +37,29 @@ func (k Keeper) GetValidator(ctx context.Context, addr sdk.ValAddress) (validato // GetValidatorByConsAddr gets a single validator by consensus address func (k Keeper) GetValidatorByConsAddr(ctx context.Context, consAddr sdk.ConsAddress) (validator types.Validator, err error) { opAddr, err := k.ValidatorByConsensusAddress.Get(ctx, consAddr) - if err != nil && !errors.Is(err, collections.ErrNotFound) { - // if the validator not found try to find it in the map of `OldToNewConsKeyMap`` because validator may've rotated it's key. + if err != nil { + // if the validator not found try to find it in the map of `OldToNewConsKeyMap` because validator may've rotated it's key. if !errors.Is(err, collections.ErrNotFound) { return types.Validator{}, err } - newConsAddr, err := k.OldToNewConsKeyMap.Get(ctx, consAddr) + newConsAddr, err := k.OldToNewConsKeyMap.Get(ctx, consAddr.Bytes()) if err != nil { + if errors.Is(err, collections.ErrNotFound) { + return types.Validator{}, types.ErrNoValidatorFound + } return types.Validator{}, err } - opAddr = newConsAddr + operatorAddr, err := k.ValidatorByConsensusAddress.Get(ctx, newConsAddr) + if err != nil { + if errors.Is(err, collections.ErrNotFound) { + return types.Validator{}, types.ErrNoValidatorFound + } + return types.Validator{}, err + } + + opAddr = operatorAddr } if opAddr == nil { diff --git a/x/staking/keeper/validator_test.go b/x/staking/keeper/validator_test.go index 948b9d671e..d3bc3316f4 100644 --- a/x/staking/keeper/validator_test.go +++ b/x/staking/keeper/validator_test.go @@ -9,6 +9,7 @@ import ( "cosmossdk.io/collections" "cosmossdk.io/core/header" "cosmossdk.io/math" + authtypes "cosmossdk.io/x/auth/types" stakingkeeper "cosmossdk.io/x/staking/keeper" "cosmossdk.io/x/staking/testutil" stakingtypes "cosmossdk.io/x/staking/types" @@ -442,3 +443,71 @@ func (s *KeeperTestSuite) TestUnbondingValidator() { require.NoError(err) require.Equal(stakingtypes.Unbonded, validator.Status) } + +func (s *KeeperTestSuite) TestValidatorConsPubKeyUpdate() { + ctx, keeper, msgServer, bk, ak := s.ctx, s.stakingKeeper, s.msgServer, s.bankKeeper, s.accountKeeper + require := s.Require() + + powers := []int64{10, 20} + var validators [2]stakingtypes.Validator + + bonedPool := authtypes.NewEmptyModuleAccount(stakingtypes.BondedPoolName) + ak.EXPECT().GetModuleAccount(gomock.Any(), stakingtypes.BondedPoolName).Return(bonedPool).AnyTimes() + bk.EXPECT().GetBalance(gomock.Any(), bonedPool.GetAddress(), sdk.DefaultBondDenom).Return(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000000)).AnyTimes() + + for i, power := range powers { + valAddr := sdk.ValAddress(PKs[i].Address().Bytes()) + validators[i] = testutil.NewValidator(s.T(), valAddr, PKs[i]) + tokens := keeper.TokensFromConsensusPower(ctx, power) + + validators[i], _ = validators[i].AddTokensFromDel(tokens) + require.NoError(keeper.SetValidator(ctx, validators[i])) + require.NoError(keeper.SetValidatorByPowerIndex(ctx, validators[i])) + require.NoError(keeper.SetValidatorByConsAddr(ctx, validators[i])) + + s.bankKeeper.EXPECT().SendCoinsFromModuleToModule(gomock.Any(), stakingtypes.NotBondedPoolName, stakingtypes.BondedPoolName, gomock.Any()) + updates := s.applyValidatorSetUpdates(ctx, keeper, 1) + validator, err := keeper.GetValidator(ctx, valAddr) + require.NoError(err) + require.Equal(validator.ABCIValidatorUpdate(keeper.PowerReduction(ctx)), updates[0]) + } + + params, err := keeper.Params.Get(ctx) + require.NoError(err) + + params.KeyRotationFee = sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000) + err = keeper.Params.Set(ctx, params) + require.NoError(err) + + valAddr1 := sdk.ValAddress(PKs[0].Address().Bytes()) + + valStr, err := keeper.ValidatorAddressCodec().BytesToString(valAddr1) + require.NoError(err) + + msg, err := stakingtypes.NewMsgRotateConsPubKey( + valStr, + PKs[499], // taking the last element from PKs + ) + + require.NoError(err) + + bk.EXPECT().SendCoinsFromAccountToModule(ctx, sdk.AccAddress(valAddr1), gomock.Any(), gomock.Any()).AnyTimes() + _, err = msgServer.RotateConsPubKey(ctx, msg) + require.NoError(err) + + updates := s.applyValidatorSetUpdates(ctx, keeper, 2) + + originalPubKey, err := validators[0].CmtConsPublicKey() + require.NoError(err) + + validator, err := keeper.GetValidator(ctx, valAddr1) + require.NoError(err) + + newPubKey, err := validator.CmtConsPublicKey() + require.NoError(err) + + require.Equal(int64(0), updates[0].Power) + require.Equal(originalPubKey, updates[0].PubKey) + require.Equal(int64(10), updates[1].Power) + require.Equal(newPubKey, updates[1].PubKey) +} diff --git a/x/staking/simulation/operations.go b/x/staking/simulation/operations.go index 7fca2e8d8d..9193fd5522 100644 --- a/x/staking/simulation/operations.go +++ b/x/staking/simulation/operations.go @@ -26,6 +26,7 @@ const ( DefaultWeightMsgUndelegate int = 100 DefaultWeightMsgBeginRedelegate int = 100 DefaultWeightMsgCancelUnbondingDelegation int = 100 + DefaultWeightMsgRotateConsPubKey int = 100 OpWeightMsgCreateValidator = "op_weight_msg_create_validator" OpWeightMsgEditValidator = "op_weight_msg_edit_validator" @@ -33,6 +34,7 @@ const ( OpWeightMsgUndelegate = "op_weight_msg_undelegate" OpWeightMsgBeginRedelegate = "op_weight_msg_begin_redelegate" OpWeightMsgCancelUnbondingDelegation = "op_weight_msg_cancel_unbonding_delegation" + OpWeightMsgRotateConsPubKey = "op_weight_msg_rotate_cons_pubkey" ) // WeightedOperations returns all the operations from the module with their respective weights @@ -51,6 +53,7 @@ func WeightedOperations( weightMsgUndelegate int weightMsgBeginRedelegate int weightMsgCancelUnbondingDelegation int + weightMsgRotateConsPubKey int ) appParams.GetOrGenerate(OpWeightMsgCreateValidator, &weightMsgCreateValidator, nil, func(_ *rand.Rand) { @@ -77,6 +80,10 @@ func WeightedOperations( weightMsgCancelUnbondingDelegation = DefaultWeightMsgCancelUnbondingDelegation }) + appParams.GetOrGenerate(OpWeightMsgRotateConsPubKey, &weightMsgRotateConsPubKey, nil, func(_ *rand.Rand) { + weightMsgRotateConsPubKey = DefaultWeightMsgRotateConsPubKey + }) + return simulation.WeightedOperations{ simulation.NewWeightedOperation( weightMsgCreateValidator, @@ -102,6 +109,10 @@ func WeightedOperations( weightMsgCancelUnbondingDelegation, SimulateMsgCancelUnbondingDelegate(txGen, ak, bk, k), ), + simulation.NewWeightedOperation( + weightMsgRotateConsPubKey, + SimulateMsgRotateConsPubKey(txGen, ak, bk, k), + ), } } @@ -126,6 +137,12 @@ func SimulateMsgCreateValidator( return simtypes.NoOpMsg(types.ModuleName, msgType, "validator already exists"), nil, nil } + consPubKey := sdk.GetConsAddress(simAccount.ConsKey.PubKey()) + _, err = k.GetValidatorByConsAddr(ctx, consPubKey) + if err == nil { + return simtypes.NoOpMsg(types.ModuleName, msgType, "cons key already used"), nil, nil + } + denom, err := k.BondDenom(ctx) if err != nil { return simtypes.NoOpMsg(types.ModuleName, msgType, "bond denom not found"), nil, err @@ -705,3 +722,102 @@ func SimulateMsgBeginRedelegate( return simulation.GenAndDeliverTxWithRandFees(txCtx) } } + +func SimulateMsgRotateConsPubKey(txGen client.TxConfig, ak types.AccountKeeper, bk types.BankKeeper, k *keeper.Keeper) simtypes.Operation { + return func( + r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + msgType := sdk.MsgTypeURL(&types.MsgRotateConsPubKey{}) + + vals, err := k.GetAllValidators(ctx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msgType, "unable to get validators"), nil, err + } + + if len(vals) == 0 { + return simtypes.NoOpMsg(types.ModuleName, msgType, "number of validators equal zero"), nil, nil + } + + val, ok := testutil.RandSliceElem(r, vals) + if !ok { + return simtypes.NoOpMsg(types.ModuleName, msgType, "unable to pick a validator"), nil, nil + } + + if val.Status != types.Bonded || val.ConsensusPower(sdk.DefaultPowerReduction) == 0 { + return simtypes.NoOpMsg(types.ModuleName, msgType, "validator not bonded."), nil, nil + } + + valAddr := val.GetOperator() + valBytes, err := k.ValidatorAddressCodec().StringToBytes(valAddr) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msgType, "error getting validator address bytes"), nil, err + } + + simAccount, found := simtypes.FindAccount(accs, sdk.AccAddress(valBytes)) + if !found { + return simtypes.NoOpMsg(types.ModuleName, msgType, "unable to find account"), nil, fmt.Errorf("validator %s not found", val.GetOperator()) + } + + cons, err := val.GetConsAddr() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msgType, "cannot get conskey"), nil, err + } + + acc, _ := simtypes.RandomAcc(r, accs) + if sdk.ConsAddress(cons).String() == sdk.ConsAddress(acc.ConsKey.PubKey().Address()).String() { + return simtypes.NoOpMsg(types.ModuleName, msgType, "new pubkey and current pubkey should be different"), nil, nil + } + + account := ak.GetAccount(ctx, simAccount.Address) + if account == nil { + return simtypes.NoOpMsg(types.ModuleName, msgType, "unable to find account"), nil, nil + } + + spendable := bk.SpendableCoins(ctx, account.GetAddress()) + params, err := k.Params.Get(ctx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msgType, "cannot get params"), nil, err + } + + if !spendable.IsAllGTE(sdk.NewCoins(params.KeyRotationFee)) { + return simtypes.NoOpMsg(types.ModuleName, msgType, "not enough balance to pay fee"), nil, nil + } + + if err := k.ExceedsMaxRotations(ctx, valBytes); err != nil { + return simtypes.NoOpMsg(types.ModuleName, msgType, "rotations limit reached within unbonding period"), nil, nil + } + + _, err = k.GetValidatorByConsAddr(ctx, cons) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msgType, "cannot get validator"), nil, err + } + + // check whether the new cons key associated with another validator + newConsAddr := sdk.ConsAddress(acc.ConsKey.PubKey().Address()) + _, err = k.GetValidatorByConsAddr(ctx, newConsAddr) + if err == nil { + return simtypes.NoOpMsg(types.ModuleName, msgType, "cons key already used"), nil, nil + } + + msg, err := types.NewMsgRotateConsPubKey(valAddr, acc.ConsKey.PubKey()) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msgType, "unable to build msg"), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: txGen, + Cdc: nil, + Msg: msg, + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +}