diff --git a/CHANGELOG.md b/CHANGELOG.md index 61c13f603072..608a770547e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (abci) [#25969](https://github.com/cosmos/cosmos-sdk/pull/25969) Add support for new ABCI methods, `InsertTx` and `ReapTxs`. * (blockstm) [#25909](https://github.com/cosmos/cosmos-sdk/pull/25909) Cache pre-state to optimize value-based validation. * (deps) [#26388](https://github.com/cosmos/cosmos-sdk/pull/26388) Bump CometBFT version to v0.39.3. +* (staking) [#26440](https://github.com/cosmos/cosmos-sdk/pull/26440) Add basic key rotation for validator consensus keys. * (crypto) [#26436](https://github.com/cosmos/cosmos-sdk/pull/26436) Add ML-DSA-65 (FIPS 204) post-quantum validator consensus key type, with SDK key wrappers, Amino + interface-registry registration, multisig support, and a `hd.MlDsa65Type` constant. ### Improvements diff --git a/enterprise/group/simapp/app.go b/enterprise/group/simapp/app.go index cca9d8a0c045..6a273a8915ee 100644 --- a/enterprise/group/simapp/app.go +++ b/enterprise/group/simapp/app.go @@ -198,11 +198,12 @@ func NewSimApp( authority := authtypes.NewModuleAddress(group.ModuleName).String() maccPerms := map[string][]string{ - authtypes.FeeCollectorName: nil, - distrtypes.ModuleName: nil, - minttypes.ModuleName: {authtypes.Minter}, - stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, - stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + authtypes.FeeCollectorName: nil, + distrtypes.ModuleName: nil, + minttypes.ModuleName: {authtypes.Minter}, + stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.KeyRotationFeePoolName: {authtypes.Burner}, } app.AccountKeeper = authkeeper.NewAccountKeeper( diff --git a/enterprise/poa/examples/migrate-from-pos/app.go b/enterprise/poa/examples/migrate-from-pos/app.go index 7bec9d358636..d7465bdfe217 100644 --- a/enterprise/poa/examples/migrate-from-pos/app.go +++ b/enterprise/poa/examples/migrate-from-pos/app.go @@ -221,12 +221,13 @@ func NewSimApp( runtime.NewKVStoreService(storeKeys[authtypes.StoreKey]), authtypes.ProtoBaseAccount, map[string][]string{ - authtypes.FeeCollectorName: nil, - govtypes.ModuleName: {authtypes.Burner, authtypes.Staking}, - poatypes.ModuleName: nil, - distrtypes.ModuleName: nil, - stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, - stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + authtypes.FeeCollectorName: nil, + govtypes.ModuleName: {authtypes.Burner, authtypes.Staking}, + poatypes.ModuleName: nil, + distrtypes.ModuleName: nil, + stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.KeyRotationFeePoolName: {authtypes.Burner}, }, authcodec.NewBech32Codec(sdk.Bech32MainPrefix), sdk.Bech32MainPrefix, diff --git a/proto/cosmos/staking/v1beta1/tx.proto b/proto/cosmos/staking/v1beta1/tx.proto index d2d29ed4bac8..fb56a6bd3cf3 100644 --- a/proto/cosmos/staking/v1beta1/tx.proto +++ b/proto/cosmos/staking/v1beta1/tx.proto @@ -46,6 +46,10 @@ service Msg { rpc UpdateParams(MsgUpdateParams) returns (MsgUpdateParamsResponse) { option (cosmos_proto.method_added_in) = "cosmos-sdk 0.47"; }; + + // RotateConsPubKey defines an operation for rotating the consensus keys + // of a validator. + rpc RotateConsPubKey(MsgRotateConsPubKey) returns (MsgRotateConsPubKeyResponse); } // MsgCreateValidator defines a SDK message for creating a new validator. @@ -201,4 +205,20 @@ message MsgUpdateParams { // MsgUpdateParams message. message MsgUpdateParamsResponse { option (cosmos_proto.message_added_in) = "cosmos-sdk 0.47"; -}; +} + +// MsgRotateConsPubKey is the Msg/RotateConsPubKey request type. +message MsgRotateConsPubKey { + option (cosmos.msg.v1.signer) = "validator_address"; + option (amino.name) = "cosmos-sdk/MsgRotateConsPubKey"; + + option (gogoproto.goproto_getters) = false; + option (gogoproto.equal) = false; + + string validator_address = 1 [(cosmos_proto.scalar) = "cosmos.ValidatorAddressString"]; + google.protobuf.Any new_pubkey = 2 [(cosmos_proto.accepts_interface) = "cosmos.crypto.PubKey"]; +} + +// MsgRotateConsPubKeyResponse defines the response structure for executing a +// MsgRotateConsPubKey message. +message MsgRotateConsPubKeyResponse {}; diff --git a/simapp/app.go b/simapp/app.go index 8bf7ebff7745..ca6d07f4e5c7 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -100,12 +100,13 @@ var ( // module account permissions maccPerms = map[string][]string{ - authtypes.FeeCollectorName: nil, - distrtypes.ModuleName: nil, - minttypes.ModuleName: {authtypes.Minter}, - stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, - stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, - govtypes.ModuleName: {authtypes.Burner}, + authtypes.FeeCollectorName: nil, + distrtypes.ModuleName: nil, + minttypes.ModuleName: {authtypes.Minter}, + stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.KeyRotationFeePoolName: {authtypes.Burner}, + govtypes.ModuleName: {authtypes.Burner}, } ) diff --git a/tests/e2e/distribution/config.go b/tests/e2e/distribution/config.go index 90ec52665e94..1c06aa2d7ace 100644 --- a/tests/e2e/distribution/config.go +++ b/tests/e2e/distribution/config.go @@ -60,6 +60,7 @@ var ( {Account: minttypes.ModuleName, Permissions: []string{authtypes.Minter}}, {Account: stakingtypes.BondedPoolName, Permissions: []string{authtypes.Burner, stakingtypes.ModuleName}}, {Account: stakingtypes.NotBondedPoolName, Permissions: []string{authtypes.Burner, stakingtypes.ModuleName}}, + {Account: stakingtypes.KeyRotationFeePoolName, Permissions: []string{authtypes.Burner}}, {Account: govtypes.ModuleName, Permissions: []string{authtypes.Burner}}, } diff --git a/tests/integration/distribution/keeper/msg_server_test.go b/tests/integration/distribution/keeper/msg_server_test.go index e689b60b6304..052de8c6866e 100644 --- a/tests/integration/distribution/keeper/msg_server_test.go +++ b/tests/integration/distribution/keeper/msg_server_test.go @@ -76,10 +76,11 @@ func initFixture(tb testing.TB) *fixture { authority := authtypes.NewModuleAddress("gov") maccPerms := map[string][]string{ - distrtypes.ModuleName: {authtypes.Minter}, - minttypes.ModuleName: {authtypes.Minter}, - stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, - stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + distrtypes.ModuleName: {authtypes.Minter}, + minttypes.ModuleName: {authtypes.Minter}, + stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.KeyRotationFeePoolName: {authtypes.Burner}, } accountKeeper := authkeeper.NewAccountKeeper( diff --git a/tests/integration/evidence/keeper/infraction_test.go b/tests/integration/evidence/keeper/infraction_test.go index 5425f968a9e7..1496cc65824e 100644 --- a/tests/integration/evidence/keeper/infraction_test.go +++ b/tests/integration/evidence/keeper/infraction_test.go @@ -93,9 +93,10 @@ func initFixture(tb testing.TB) *fixture { authority := authtypes.NewModuleAddress("gov") maccPerms := map[string][]string{ - minttypes.ModuleName: {authtypes.Minter}, - stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, - stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + minttypes.ModuleName: {authtypes.Minter}, + stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.KeyRotationFeePoolName: {authtypes.Burner}, } accountKeeper := authkeeper.NewAccountKeeper( diff --git a/tests/integration/gov/keeper/keeper_test.go b/tests/integration/gov/keeper/keeper_test.go index 8f2345a2aeb5..8c2ab98d82ca 100644 --- a/tests/integration/gov/keeper/keeper_test.go +++ b/tests/integration/gov/keeper/keeper_test.go @@ -62,11 +62,12 @@ func initFixture(tb testing.TB) *fixture { authority := authtypes.NewModuleAddress(types.ModuleName) maccPerms := map[string][]string{ - distrtypes.ModuleName: nil, - minttypes.ModuleName: {authtypes.Minter}, - stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, - stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, - types.ModuleName: {authtypes.Burner}, + distrtypes.ModuleName: nil, + minttypes.ModuleName: {authtypes.Minter}, + stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.KeyRotationFeePoolName: {authtypes.Burner}, + types.ModuleName: {authtypes.Burner}, } accountKeeper := authkeeper.NewAccountKeeper( diff --git a/tests/integration/slashing/keeper/keeper_test.go b/tests/integration/slashing/keeper/keeper_test.go index 25dbb5a4d359..430b9d554157 100644 --- a/tests/integration/slashing/keeper/keeper_test.go +++ b/tests/integration/slashing/keeper/keeper_test.go @@ -65,9 +65,10 @@ func initFixture(tb testing.TB) *fixture { authority := authtypes.NewModuleAddress("gov") maccPerms := map[string][]string{ - minttypes.ModuleName: {authtypes.Minter}, - stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, - stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + minttypes.ModuleName: {authtypes.Minter}, + stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.KeyRotationFeePoolName: {authtypes.Burner}, } accountKeeper := authkeeper.NewAccountKeeper( diff --git a/tests/integration/staking/keeper/common_test.go b/tests/integration/staking/keeper/common_test.go index 5d8fd50cef68..ed11a4b94880 100644 --- a/tests/integration/staking/keeper/common_test.go +++ b/tests/integration/staking/keeper/common_test.go @@ -111,10 +111,11 @@ func initFixture(tb testing.TB) *fixture { authority := authtypes.NewModuleAddress("gov") maccPerms := map[string][]string{ - minttypes.ModuleName: {authtypes.Minter}, - types.ModuleName: {authtypes.Minter}, - types.BondedPoolName: {authtypes.Burner, authtypes.Staking}, - types.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + minttypes.ModuleName: {authtypes.Minter}, + types.ModuleName: {authtypes.Minter}, + types.BondedPoolName: {authtypes.Burner, authtypes.Staking}, + types.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + types.KeyRotationFeePoolName: {authtypes.Burner}, } accountKeeper := authkeeper.NewAccountKeeper( diff --git a/tests/integration/staking/keeper/cons_key_rotation_test.go b/tests/integration/staking/keeper/cons_key_rotation_test.go new file mode 100644 index 000000000000..65e7a4ecee25 --- /dev/null +++ b/tests/integration/staking/keeper/cons_key_rotation_test.go @@ -0,0 +1,250 @@ +package keeper_test + +import ( + "testing" + "time" + + cmtabcitypes "github.com/cometbft/cometbft/abci/types" + "gotest.tools/v3/assert" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking/keeper" + "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// Covers msg-server queuing plus end-blocker application: the fee transfer, +// all four store indexes, the deferred swap of the validators stored +// ConsensusPubkey, and the two ABCI updates CometBFT needs to retire the old +// key at zero power and instate the new key at the current power. +func TestRotateConsPubKey_MsgServerQueuesAndEndBlockerApplies(t *testing.T) { + t.Parallel() + f := initFixture(t) + msgServer := keeper.NewMsgServerImpl(f.stakingKeeper) + bondDenom, err := f.stakingKeeper.BondDenom(f.sdkCtx) + assert.NilError(t, err) + + oldPk := ed25519.GenPrivKey().PubKey() + newPk := ed25519.GenPrivKey().PubKey() + valAddr, accAddr := bondConsKeyRotationValidator(t, f, oldPk) + oldConsAddr := sdk.ConsAddress(oldPk.Address()) + newConsAddr := sdk.ConsAddress(newPk.Address()) + + accBalBefore := f.bankKeeper.GetBalance(f.sdkCtx, accAddr, bondDenom) + supplyBefore := f.bankKeeper.GetSupply(f.sdkCtx, bondDenom) + + valBefore, err := f.stakingKeeper.GetValidator(f.sdkCtx, valAddr) + assert.NilError(t, err) + powerReduction := f.stakingKeeper.PowerReduction(f.sdkCtx) + powerBefore := valBefore.ConsensusPower(powerReduction) + assert.Assert(t, powerBefore > 0) + + _, err = msgServer.RotateConsPubKey(f.sdkCtx, &types.MsgRotateConsPubKey{ + ValidatorAddress: valAddr.String(), + NewPubkey: newPubKeyAny(t, newPk), + }) + assert.NilError(t, err) + + // fee debited from the operator account and burned (total supply + // decreases by exactly the fee) + fee := types.DefaultKeyRotationFee + assert.DeepEqual(t, accBalBefore.Sub(fee), f.bankKeeper.GetBalance(f.sdkCtx, accAddr, bondDenom)) + assert.DeepEqual(t, supplyBefore.Sub(fee), f.bankKeeper.GetSupply(f.sdkCtx, bondDenom)) + + // per-validator pending index recorded (gates further rotations inside the + // unbonding window) + hasPending, err := f.stakingKeeper.HasConsKeyRotationInUnbondingWindow(f.sdkCtx, valAddr) + assert.NilError(t, err) + assert.Assert(t, hasPending) + + // maturity queue entry recorded at BlockTime + UnbondingTime + unbondingTime, err := f.stakingKeeper.UnbondingTime(f.sdkCtx) + assert.NilError(t, err) + maturity := f.sdkCtx.BlockHeader().Time.Add(unbondingTime) + hasQueue, err := f.stakingKeeper.HasConsKeyRotationQueueEntry(f.sdkCtx, maturity, valAddr) + assert.NilError(t, err) + assert.Assert(t, hasQueue) + + // rotated cons addr index recorded so the old key still resolves to this + // validator for slashing/evidence routing + hasRotated, err := f.stakingKeeper.IsConsAddrLockedByRotation(f.sdkCtx, oldConsAddr) + assert.NilError(t, err) + assert.Assert(t, hasRotated) + + // validators stored ConsensusPubkey is unchanged until the end blocker runs + preEndBlocker, err := f.stakingKeeper.GetValidator(f.sdkCtx, valAddr) + assert.NilError(t, err) + preConsAddr, err := preEndBlocker.GetConsAddr() + assert.NilError(t, err) + assert.DeepEqual(t, oldConsAddr.Bytes(), preConsAddr) + + // advance one block at the current block time so the end blocker applies + // the rotation but does not yet prune (maturity is in the future) + advanceBlock(t, f, f.sdkCtx.BlockHeader().Time) + + // old by-cons-addr index is gone + _, err = f.stakingKeeper.GetValidatorByConsAddr(f.sdkCtx, oldConsAddr) + assert.ErrorContains(t, err, types.ErrNoValidatorFound.Error()) + + // new by-cons-addr index resolves to this validator + byNew, err := f.stakingKeeper.GetValidatorByConsAddr(f.sdkCtx, newConsAddr) + assert.NilError(t, err) + assert.Equal(t, valAddr.String(), byNew.OperatorAddress) + + // validators stored ConsensusPubkey now reflects newPk and power is + // unchanged + stored, err := f.stakingKeeper.GetValidator(f.sdkCtx, valAddr) + assert.NilError(t, err) + storedConsAddr, err := stored.GetConsAddr() + assert.NilError(t, err) + assert.DeepEqual(t, newConsAddr.Bytes(), storedConsAddr) + assert.Equal(t, powerBefore, stored.ConsensusPower(powerReduction)) + + // the per-validator pending index intentionally persists past the end + // blocker so that further rotations are gated until the end blocker + // prunes it after maturity + hasPendingAfter, err := f.stakingKeeper.HasConsKeyRotationInUnbondingWindow(f.sdkCtx, valAddr) + assert.NilError(t, err) + assert.Assert(t, hasPendingAfter) +} + +// Covers PruneMaturedConsKeyRotations (called from the end blocker) clearing +// the maturity queue, the per-validator pending index, and the rotated cons +// addr index once the unbonding window has elapsed. +func TestRotateConsPubKey_PruneClearsRotationStateAfterUnbonding(t *testing.T) { + t.Parallel() + f := initFixture(t) + msgServer := keeper.NewMsgServerImpl(f.stakingKeeper) + + oldPk := ed25519.GenPrivKey().PubKey() + newPk := ed25519.GenPrivKey().PubKey() + valAddr, _ := bondConsKeyRotationValidator(t, f, oldPk) + oldConsAddr := sdk.ConsAddress(oldPk.Address()) + + _, err := msgServer.RotateConsPubKey(f.sdkCtx, &types.MsgRotateConsPubKey{ + ValidatorAddress: valAddr.String(), + NewPubkey: newPubKeyAny(t, newPk), + }) + assert.NilError(t, err) + + unbondingTime, err := f.stakingKeeper.UnbondingTime(f.sdkCtx) + assert.NilError(t, err) + maturity := f.sdkCtx.BlockHeader().Time.Add(unbondingTime) + + // first block at current time: applies the rotation, maturity is in + // the future so no pruning happens + advanceBlock(t, f, f.sdkCtx.BlockHeader().Time) + + has, err := f.stakingKeeper.HasConsKeyRotationQueueEntry(f.sdkCtx, maturity, valAddr) + assert.NilError(t, err) + assert.Assert(t, has) + + // second block past maturity: the end blocker prunes + advanceBlock(t, f, maturity.Add(time.Second)) + + has, err = f.stakingKeeper.HasConsKeyRotationQueueEntry(f.sdkCtx, maturity, valAddr) + assert.NilError(t, err) + assert.Assert(t, !has, "maturity queue entry should be pruned") + + hasPending, err := f.stakingKeeper.HasConsKeyRotationInUnbondingWindow(f.sdkCtx, valAddr) + assert.NilError(t, err) + assert.Assert(t, !hasPending, "per-validator pending index should be pruned") + + hasRotated, err := f.stakingKeeper.IsConsAddrLockedByRotation(f.sdkCtx, oldConsAddr) + assert.NilError(t, err) + assert.Assert(t, !hasRotated, "rotated cons addr index should be pruned") +} + +// Covers the per-window rotation cap lifting after pruning, and that the +// original consensus pubkey can be reused once it leaves the rotation history. +func TestRotateConsPubKey_SecondRotationAfterPruningSucceeds(t *testing.T) { + t.Parallel() + f := initFixture(t) + msgServer := keeper.NewMsgServerImpl(f.stakingKeeper) + + pkA := ed25519.GenPrivKey().PubKey() + pkB := ed25519.GenPrivKey().PubKey() + pkC := ed25519.GenPrivKey().PubKey() + valAddr, _ := bondConsKeyRotationValidator(t, f, pkA) + + // first rotation A -> B + _, err := msgServer.RotateConsPubKey(f.sdkCtx, &types.MsgRotateConsPubKey{ + ValidatorAddress: valAddr.String(), + NewPubkey: newPubKeyAny(t, pkB), + }) + assert.NilError(t, err) + advanceBlock(t, f, f.sdkCtx.BlockHeader().Time) + + // a second rotation inside the unbonding window is rejected + _, err = msgServer.RotateConsPubKey(f.sdkCtx, &types.MsgRotateConsPubKey{ + ValidatorAddress: valAddr.String(), + NewPubkey: newPubKeyAny(t, pkC), + }) + assert.ErrorContains(t, err, types.ErrExceedingMaxConsPubKeyRotations.Error()) + + // advance past maturity and let the end blocker prune + unbondingTime, err := f.stakingKeeper.UnbondingTime(f.sdkCtx) + assert.NilError(t, err) + advanceBlock(t, f, f.sdkCtx.BlockHeader().Time.Add(unbondingTime).Add(time.Second)) + + // second rotation back to pkA (the original key) succeeds: the rotation + // history was cleared by pruning + _, err = msgServer.RotateConsPubKey(f.sdkCtx, &types.MsgRotateConsPubKey{ + ValidatorAddress: valAddr.String(), + NewPubkey: newPubKeyAny(t, pkA), + }) + assert.NilError(t, err) + advanceBlock(t, f, f.sdkCtx.BlockHeader().Time) + + stored, err := f.stakingKeeper.GetValidator(f.sdkCtx, valAddr) + assert.NilError(t, err) + storedConsAddr, err := stored.GetConsAddr() + assert.NilError(t, err) + assert.DeepEqual(t, sdk.ConsAddress(pkA.Address()).Bytes(), storedConsAddr) +} + +// bondConsKeyRotationValidator creates and bonds a single validator under +// consPk, funding the operator account with enough tokens to cover several +// rotation fees plus the self delegation. +func bondConsKeyRotationValidator(t *testing.T, f *fixture, consPk cryptotypes.PubKey) (sdk.ValAddress, sdk.AccAddress) { + t.Helper() + addrs := simtestutil.AddTestAddrsIncremental(f.bankKeeper, f.stakingKeeper, f.sdkCtx, 1, f.stakingKeeper.TokensFromConsensusPower(f.sdkCtx, 300)) + valAddr := sdk.ValAddress(addrs[0]) + + v, err := types.NewValidator(valAddr.String(), consPk, types.NewDescription("v", "", "", "", "")) + assert.NilError(t, err) + assert.NilError(t, f.stakingKeeper.SetValidator(f.sdkCtx, v)) + assert.NilError(t, f.stakingKeeper.SetValidatorByConsAddr(f.sdkCtx, v)) + assert.NilError(t, f.stakingKeeper.SetNewValidatorByPowerIndex(f.sdkCtx, v)) + + _, err = f.stakingKeeper.Delegate(f.sdkCtx, addrs[0], f.stakingKeeper.TokensFromConsensusPower(f.sdkCtx, 100), types.Unbonded, v, true) + assert.NilError(t, err) + + applyValidatorSetUpdates(t, f.sdkCtx, f.stakingKeeper, 1) + + return valAddr, addrs[0] +} + +func newPubKeyAny(t *testing.T, pk cryptotypes.PubKey) *codectypes.Any { + t.Helper() + a, err := codectypes.NewAnyWithValue(pk) + assert.NilError(t, err) + return a +} + +// advanceBlock advances the chain by one block at blockTime, driving the +// staking end blocker through the real ABCI flow so that pending consensus +// key rotations are applied and any matured rotation entries are pruned. +func advanceBlock(t *testing.T, f *fixture, blockTime time.Time) { + t.Helper() + _, err := f.app.FinalizeBlock(&cmtabcitypes.RequestFinalizeBlock{ + Height: f.app.LastBlockHeight() + 1, + Time: blockTime, + }) + assert.NilError(t, err) + _, err = f.app.Commit() + assert.NilError(t, err) +} diff --git a/tests/integration/staking/keeper/determinstic_test.go b/tests/integration/staking/keeper/determinstic_test.go index d99d8db31ce4..8f23746c0eef 100644 --- a/tests/integration/staking/keeper/determinstic_test.go +++ b/tests/integration/staking/keeper/determinstic_test.go @@ -80,10 +80,11 @@ func initDeterministicFixture(t *testing.T) *deterministicFixture { authority := authtypes.NewModuleAddress("gov") maccPerms := map[string][]string{ - minttypes.ModuleName: {authtypes.Minter}, - stakingtypes.ModuleName: {authtypes.Minter}, - stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, - stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + minttypes.ModuleName: {authtypes.Minter}, + stakingtypes.ModuleName: {authtypes.Minter}, + stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.KeyRotationFeePoolName: {authtypes.Burner}, } accountKeeper := authkeeper.NewAccountKeeper( diff --git a/testutil/configurator/configurator.go b/testutil/configurator/configurator.go index e1d77243a567..4fed3023244f 100644 --- a/testutil/configurator/configurator.go +++ b/testutil/configurator/configurator.go @@ -144,6 +144,7 @@ func AuthModule() ModuleOption { {Account: "mint", Permissions: []string{"minter"}}, {Account: "bonded_tokens_pool", Permissions: []string{"burner", "staking"}}, {Account: "not_bonded_tokens_pool", Permissions: []string{"burner", "staking"}}, + {Account: "key_rotation_fee_pool", Permissions: []string{"burner"}}, {Account: "gov", Permissions: []string{"burner"}}, {Account: "nft"}, }, diff --git a/testutil/integration/router.go b/testutil/integration/router.go index bfa770263b50..3002ab7d0305 100644 --- a/testutil/integration/router.go +++ b/testutil/integration/router.go @@ -69,11 +69,16 @@ func NewIntegrationApp( return &cmtabcitypes.ResponseInitChain{}, nil }) - bApp.SetBeginBlocker(func(_ sdk.Context) (sdk.BeginBlock, error) { - return moduleManager.BeginBlock(sdkCtx) + // the integration helper keeps module state on the externally provided + // sdkCtx (cms), but FinalizeBlock passes a ctx whose header reflects the + // current block. forward only the block time so begin and end blockers + // can act on time-dependent state (e.g. matured queues) while still + // operating on the shared store and the captured initial block height. + bApp.SetBeginBlocker(func(ctx sdk.Context) (sdk.BeginBlock, error) { + return moduleManager.BeginBlock(sdkCtx.WithBlockTime(ctx.BlockTime())) }) - bApp.SetEndBlocker(func(_ sdk.Context) (sdk.EndBlock, error) { - return moduleManager.EndBlock(sdkCtx) + bApp.SetEndBlocker(func(ctx sdk.Context) (sdk.EndBlock, error) { + return moduleManager.EndBlock(sdkCtx.WithBlockTime(ctx.BlockTime())) }) router := baseapp.NewMsgServiceRouter() diff --git a/x/staking/keeper/genesis.go b/x/staking/keeper/genesis.go index 3f476986e427..533ad8ec97d7 100644 --- a/x/staking/keeper/genesis.go +++ b/x/staking/keeper/genesis.go @@ -174,6 +174,12 @@ func (k Keeper) InitGenesis(ctx context.Context, data *types.GenesisState) (res panic(fmt.Sprintf("not bonded pool balance is different from not bonded coins: %s <-> %s", notBondedBalance, notBondedCoins)) } + // materialize the key rotation fee pool so it exists from genesis. fees + // only ever transit through it, so it carries no genesis balance. + if keyRotationFeePool := k.GetKeyRotationFeePool(ctx); keyRotationFeePool == nil { + panic(fmt.Sprintf("%s module account has not been set", types.KeyRotationFeePoolName)) + } + // don't need to run CometBFT updates if we exported if data.Exported { for _, lv := range data.LastValidatorPowers { diff --git a/x/staking/keeper/keeper.go b/x/staking/keeper/keeper.go index b2832fba3d56..9fdcba7bacd1 100644 --- a/x/staking/keeper/keeper.go +++ b/x/staking/keeper/keeper.go @@ -53,6 +53,10 @@ func NewKeeper( panic(fmt.Sprintf("%s module account has not been set", types.NotBondedPoolName)) } + if addr := ak.GetModuleAddress(types.KeyRotationFeePoolName); addr == nil { + panic(fmt.Sprintf("%s module account has not been set", types.KeyRotationFeePoolName)) + } + // ensure that authority is a valid AccAddress if _, err := ak.AddressCodec().StringToBytes(authority); err != nil { panic("authority is not a valid acc address") diff --git a/x/staking/keeper/keeper_test.go b/x/staking/keeper/keeper_test.go index f3cb1bb4f340..9311b15f9b60 100644 --- a/x/staking/keeper/keeper_test.go +++ b/x/staking/keeper/keeper_test.go @@ -26,9 +26,10 @@ import ( ) var ( - bondedAcc = authtypes.NewEmptyModuleAccount(stakingtypes.BondedPoolName) - notBondedAcc = authtypes.NewEmptyModuleAccount(stakingtypes.NotBondedPoolName) - PKs = simtestutil.CreateTestPubKeys(500) + bondedAcc = authtypes.NewEmptyModuleAccount(stakingtypes.BondedPoolName) + notBondedAcc = authtypes.NewEmptyModuleAccount(stakingtypes.NotBondedPoolName) + keyRotationFeeAcc = authtypes.NewEmptyModuleAccount(stakingtypes.KeyRotationFeePoolName) + PKs = simtestutil.CreateTestPubKeys(500) ) type KeeperTestSuite struct { @@ -54,6 +55,7 @@ func (s *KeeperTestSuite) SetupTest() { accountKeeper := stakingtestutil.NewMockAccountKeeper(ctrl) accountKeeper.EXPECT().GetModuleAddress(stakingtypes.BondedPoolName).Return(bondedAcc.GetAddress()) accountKeeper.EXPECT().GetModuleAddress(stakingtypes.NotBondedPoolName).Return(notBondedAcc.GetAddress()) + accountKeeper.EXPECT().GetModuleAddress(stakingtypes.KeyRotationFeePoolName).Return(keyRotationFeeAcc.GetAddress()) accountKeeper.EXPECT().AddressCodec().Return(address.NewBech32Codec("cosmos")).AnyTimes() bankKeeper := stakingtestutil.NewMockBankKeeper(ctrl) diff --git a/x/staking/keeper/msg_server.go b/x/staking/keeper/msg_server.go index ee0915990e29..d242c2a54f41 100644 --- a/x/staking/keeper/msg_server.go +++ b/x/staking/keeper/msg_server.go @@ -2,6 +2,7 @@ package keeper import ( "context" + "errors" "slices" "strconv" "time" @@ -613,3 +614,80 @@ func (k msgServer) UpdateParams(ctx context.Context, msg *types.MsgUpdateParams) return &types.MsgUpdateParamsResponse{}, nil } + +// RotateConsPubKey defines a method for changing a validators consensus key to +// a new key. +func (k msgServer) RotateConsPubKey(ctx context.Context, msg *types.MsgRotateConsPubKey) (*types.MsgRotateConsPubKeyResponse, error) { + newPk, ok := msg.NewPubkey.GetCachedValue().(cryptotypes.PubKey) + if !ok { + return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidType, "expecting cryptotypes.PubKey, got %T", msg.NewPubkey.GetCachedValue()) + } + newConsAddr := sdk.ConsAddress(newPk.Address()) + + // reject a key locked by a rotation, either because some validator + // rotated away from it inside the unbonding window or because some + // validator already has a pending rotation targeting it + locked, err := k.IsConsAddrLockedByRotation(ctx, newConsAddr) + if err != nil { + return nil, err + } + if locked { + return nil, types.ErrConsensusPubKeyInRotationHistory + } + + // reject a key currently in use by some validator + existing, err := k.GetValidatorByConsAddr(ctx, newConsAddr) + if err != nil && !errors.Is(err, types.ErrNoValidatorFound) { + return nil, err + } + if err == nil && existing.OperatorAddress != "" { + return nil, types.ErrConsensusPubKeyAlreadyUsedForValidator + } + + valAddr, err := k.validatorAddressCodec.StringToBytes(msg.ValidatorAddress) + if err != nil { + return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid validator address: %s", err) + } + validator, err := k.GetValidator(ctx, valAddr) + if err != nil { + return nil, types.ErrNoValidatorFound + } + + if validator.IsJailed() { + return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "validator is jailed") + } + + // shouldnt ever happen + oldPk, ok := validator.ConsensusPubkey.GetCachedValue().(cryptotypes.PubKey) + if !ok { + return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidType, "expecting cryptotypes.PubKey for validator's current key, got %T", validator.ConsensusPubkey.GetCachedValue()) + } + + // enforce the one rotation per validator limit inside the unbonding window + hasRotated, err := k.HasConsKeyRotationInUnbondingWindow(ctx, valAddr) + if err != nil { + return nil, err + } + if hasRotated { + return nil, types.ErrExceedingMaxConsPubKeyRotations + } + + // route the rotation fee through the dedicated key rotation fee pool + // module account before burning. The pool is a burner module account so + // the fee is fully removed from supply and never mingles with bonded or + // unbonded staking balances. + feeCoins := sdk.NewCoins(types.DefaultKeyRotationFee) + if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sdk.AccAddress(valAddr), types.KeyRotationFeePoolName, feeCoins); err != nil { + return nil, err + } + if err := k.bankKeeper.BurnCoins(ctx, types.KeyRotationFeePoolName, feeCoins); err != nil { + return nil, err + } + + // record the key rotation in the store + if err := k.SetConsKeyRotation(ctx, valAddr, oldPk, newPk); err != nil { + return nil, err + } + + return &types.MsgRotateConsPubKeyResponse{}, nil +} diff --git a/x/staking/keeper/msg_server_test.go b/x/staking/keeper/msg_server_test.go index c181a1d3d668..82432b680962 100644 --- a/x/staking/keeper/msg_server_test.go +++ b/x/staking/keeper/msg_server_test.go @@ -12,6 +12,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec/address" codectypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -1153,6 +1154,168 @@ func (s *KeeperTestSuite) TestMsgUpdateParams() { } } +func (s *KeeperTestSuite) TestMsgRotateConsPubKey() { + require := s.Require() + + newAny := func(pk cryptotypes.PubKey) *codectypes.Any { + a, err := codectypes.NewAnyWithValue(pk) + require.NoError(err) + return a + } + + createValidator := func(status stakingtypes.BondStatus) (sdk.ValAddress, cryptotypes.PubKey) { + pk := ed25519.GenPrivKey().PubKey() + valAddr := sdk.ValAddress(pk.Address()) + v, err := stakingtypes.NewValidator(valAddr.String(), pk, stakingtypes.Description{Moniker: "v"}) + require.NoError(err) + v.Status = status + require.NoError(s.stakingKeeper.SetValidator(s.ctx, v)) + require.NoError(s.stakingKeeper.SetValidatorByConsAddr(s.ctx, v)) + return valAddr, pk + } + + testCases := []struct { + name string + newRotateConsPubKeyMsg func() *stakingtypes.MsgRotateConsPubKey + expErr string + }{ + { + name: "invalid validator address", + newRotateConsPubKeyMsg: func() *stakingtypes.MsgRotateConsPubKey { + return &stakingtypes.MsgRotateConsPubKey{ + ValidatorAddress: "invalid", + NewPubkey: newAny(ed25519.GenPrivKey().PubKey()), + } + }, + expErr: "invalid validator address", + }, + { + name: "validator not found", + newRotateConsPubKeyMsg: func() *stakingtypes.MsgRotateConsPubKey { + missing := sdk.ValAddress(ed25519.GenPrivKey().PubKey().Address()) + return &stakingtypes.MsgRotateConsPubKey{ + ValidatorAddress: missing.String(), + NewPubkey: newAny(ed25519.GenPrivKey().PubKey()), + } + }, + expErr: stakingtypes.ErrNoValidatorFound.Error(), + }, + { + name: "validator jailed", + newRotateConsPubKeyMsg: func() *stakingtypes.MsgRotateConsPubKey { + valAddr, _ := createValidator(stakingtypes.Bonded) + v, err := s.stakingKeeper.GetValidator(s.ctx, valAddr) + require.NoError(err) + v.Jailed = true + require.NoError(s.stakingKeeper.SetValidator(s.ctx, v)) + return &stakingtypes.MsgRotateConsPubKey{ + ValidatorAddress: valAddr.String(), + NewPubkey: newAny(ed25519.GenPrivKey().PubKey()), + } + }, + expErr: "validator is jailed", + }, + { + name: "new pubkey already used by another validator", + newRotateConsPubKeyMsg: func() *stakingtypes.MsgRotateConsPubKey { + valAddr, _ := createValidator(stakingtypes.Bonded) + _, occupiedPk := createValidator(stakingtypes.Bonded) + return &stakingtypes.MsgRotateConsPubKey{ + ValidatorAddress: valAddr.String(), + NewPubkey: newAny(occupiedPk), + } + }, + expErr: stakingtypes.ErrConsensusPubKeyAlreadyUsedForValidator.Error(), + }, + { + name: "new pubkey in rotation history", + newRotateConsPubKeyMsg: func() *stakingtypes.MsgRotateConsPubKey { + valAddr, _ := createValidator(stakingtypes.Bonded) + oldPubKey := ed25519.GenPrivKey().PubKey() + dummy := sdk.ValAddress(ed25519.GenPrivKey().PubKey().Address()) + require.NoError(s.stakingKeeper.SetConsKeyRotation(s.ctx, dummy, oldPubKey, ed25519.GenPrivKey().PubKey())) + return &stakingtypes.MsgRotateConsPubKey{ + ValidatorAddress: valAddr.String(), + NewPubkey: newAny(oldPubKey), + } + }, + expErr: stakingtypes.ErrConsensusPubKeyInRotationHistory.Error(), + }, + { + name: "new pubkey is the target of another pending rotation", + newRotateConsPubKeyMsg: func() *stakingtypes.MsgRotateConsPubKey { + valAddr, _ := createValidator(stakingtypes.Bonded) + targetPubKey := ed25519.GenPrivKey().PubKey() + dummy := sdk.ValAddress(ed25519.GenPrivKey().PubKey().Address()) + require.NoError(s.stakingKeeper.SetConsKeyRotation(s.ctx, dummy, ed25519.GenPrivKey().PubKey(), targetPubKey)) + return &stakingtypes.MsgRotateConsPubKey{ + ValidatorAddress: valAddr.String(), + NewPubkey: newAny(targetPubKey), + } + }, + expErr: stakingtypes.ErrConsensusPubKeyInRotationHistory.Error(), + }, + { + name: "valid msg", + newRotateConsPubKeyMsg: func() *stakingtypes.MsgRotateConsPubKey { + valAddr, _ := createValidator(stakingtypes.Bonded) + s.bankKeeper.EXPECT(). + SendCoinsFromAccountToModule(gomock.Any(), sdk.AccAddress(valAddr), stakingtypes.KeyRotationFeePoolName, gomock.Any()). + Return(nil) + s.bankKeeper.EXPECT(). + BurnCoins(gomock.Any(), stakingtypes.KeyRotationFeePoolName, gomock.Any()). + Return(nil) + return &stakingtypes.MsgRotateConsPubKey{ + ValidatorAddress: valAddr.String(), + NewPubkey: newAny(ed25519.GenPrivKey().PubKey()), + } + }, + expErr: "", + }, + { + name: "exceeds max rotations (1)", + newRotateConsPubKeyMsg: func() *stakingtypes.MsgRotateConsPubKey { + // submit a valid rotation for valAddr + valAddr, _ := createValidator(stakingtypes.Bonded) + s.bankKeeper.EXPECT(). + SendCoinsFromAccountToModule(gomock.Any(), sdk.AccAddress(valAddr), stakingtypes.KeyRotationFeePoolName, gomock.Any()). + Return(nil) + s.bankKeeper.EXPECT(). + BurnCoins(gomock.Any(), stakingtypes.KeyRotationFeePoolName, gomock.Any()). + Return(nil) + valid := &stakingtypes.MsgRotateConsPubKey{ + ValidatorAddress: valAddr.String(), + NewPubkey: newAny(ed25519.GenPrivKey().PubKey()), + } + _, err := s.msgServer.RotateConsPubKey(s.ctx, valid) + require.NoError(err) + + // try and rotate again + return &stakingtypes.MsgRotateConsPubKey{ + ValidatorAddress: valAddr.String(), + NewPubkey: newAny(ed25519.GenPrivKey().PubKey()), + } + }, + expErr: stakingtypes.ErrExceedingMaxConsPubKeyRotations.Error(), + }, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + s.SetupTest() + + msg := tc.newRotateConsPubKeyMsg() + _, err := s.msgServer.RotateConsPubKey(s.ctx, msg) + if tc.expErr != "" { + require.Error(err) + require.Contains(err.Error(), tc.expErr) + return + } + require.NoError(err) + }) + } +} + func (s *KeeperTestSuite) TestUpdateParamsAuthority() { ctx, keeper, msgServer := s.ctx, s.stakingKeeper, s.msgServer require := s.Require() diff --git a/x/staking/keeper/pool.go b/x/staking/keeper/pool.go index 5e76397a660a..50d2457ffb29 100644 --- a/x/staking/keeper/pool.go +++ b/x/staking/keeper/pool.go @@ -19,6 +19,13 @@ func (k Keeper) GetNotBondedPool(ctx context.Context) (notBondedPool sdk.ModuleA return k.authKeeper.GetModuleAccount(ctx, types.NotBondedPoolName) } +// GetKeyRotationFeePool returns the key rotation fee pool's module account. +// Consensus key rotation fees are routed through this account before being +// burned. +func (k Keeper) GetKeyRotationFeePool(ctx context.Context) (keyRotationFeePool sdk.ModuleAccountI) { + return k.authKeeper.GetModuleAccount(ctx, types.KeyRotationFeePoolName) +} + // bondedTokensToNotBonded transfers coins from the bonded to the not bonded pool within staking func (k Keeper) bondedTokensToNotBonded(ctx context.Context, tokens math.Int) error { bondDenom, err := k.BondDenom(ctx) diff --git a/x/staking/keeper/rotation.go b/x/staking/keeper/rotation.go new file mode 100644 index 000000000000..d845ee989813 --- /dev/null +++ b/x/staking/keeper/rotation.go @@ -0,0 +1,279 @@ +package keeper + +import ( + "bytes" + "context" + "errors" + "time" + + abci "github.com/cometbft/cometbft/abci/types" + + "cosmossdk.io/math" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + storetypes "github.com/cosmos/cosmos-sdk/store/v2/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// HasConsKeyRotationInUnbondingWindow returns whether the validator has +// performed a consensus key rotation inside current the unbonding window. +func (k Keeper) HasConsKeyRotationInUnbondingWindow(ctx context.Context, valAddr sdk.ValAddress) (bool, error) { + return k.storeService.OpenKVStore(ctx).Has(types.GetValidatorConsKeyRotationKey(valAddr)) +} + +// IsConsAddrLockedByRotation returns whether the given consensus address is +// locked by a key rotation, either because some validator previously rotated +// away from it (and is still inside the unbonding window) or because some +// validator has enqueued a pending rotation targeting it. +func (k Keeper) IsConsAddrLockedByRotation(ctx context.Context, consAddr sdk.ConsAddress) (bool, error) { + return k.storeService.OpenKVStore(ctx).Has(types.GetRotationLockedConsAddrIndexKey(consAddr)) +} + +// HasConsKeyRotationQueueEntry returns whether the maturity queue holds an +// entry at the given maturity for the given validator. +func (k Keeper) HasConsKeyRotationQueueEntry(ctx context.Context, maturity time.Time, valAddr sdk.ValAddress) (bool, error) { + return k.storeService.OpenKVStore(ctx).Has(types.GetConsKeyRotationQueueKey(maturity, valAddr)) +} + +// SetConsKeyRotation writes to indexes that track a pending consensus key +// rotation. The new pubkey is written to the unapplied queue so the end +// blocker can perform the rotation in this block. +func (k Keeper) SetConsKeyRotation(ctx context.Context, valAddr sdk.ValAddress, oldPubKey, newPubKey cryptotypes.PubKey) error { + sdkCtx := sdk.UnwrapSDKContext(ctx) + + unbondingTime, err := k.UnbondingTime(ctx) + if err != nil { + return err + } + maturity := sdkCtx.BlockHeader().Time.Add(unbondingTime) + + oldConsAddr := sdk.ConsAddress(oldPubKey.Address()) + newConsAddr := sdk.ConsAddress(newPubKey.Address()) + + store := k.storeService.OpenKVStore(ctx) + + // add to queue keyed by time so that we can iterate rotations happening by + // time and quickly remove ones that have matured (fallen out of the + // current unbonding period). + if err := store.Set(types.GetConsKeyRotationQueueKey(maturity, valAddr), oldConsAddr); err != nil { + return err + } + + if err := store.Set(types.GetValidatorConsKeyRotationKey(valAddr), []byte{}); err != nil { + return err + } + + // lock both the old and new cons addrs so that no validator can rotate + // to either while the rotation is pending or within the unbonding + // window. The new addr entry is cleared when the rotation is applied + // in the end blocker (after which it is the validator's live cons + // addr). The old addr entry is cleared when the rotation matures. + if err := store.Set(types.GetRotationLockedConsAddrIndexKey(oldConsAddr), valAddr); err != nil { + return err + } + if err := store.Set(types.GetRotationLockedConsAddrIndexKey(newConsAddr), valAddr); err != nil { + return err + } + + newPubKeyBz, err := k.cdc.MarshalInterface(newPubKey) + if err != nil { + return err + } + return store.Set(types.GetUnappliedConsKeyRotationKey(valAddr), newPubKeyBz) +} + +// ApplyPendingConsKeyRotations applies every rotation queued by the msg server +// and returns the validator updates needed to retire each old key at zero +// power and instate each new key at the validator's current power. +func (k Keeper) ApplyPendingConsKeyRotations(ctx context.Context, powerReduction math.Int) ([]abci.ValidatorUpdate, error) { + var totalUpdates abci.ValidatorUpdates + + store := k.storeService.OpenKVStore(ctx) + err := k.IterateUnappliedConsKeyRotations(ctx, func(valAddr sdk.ValAddress, newPubKey cryptotypes.PubKey) error { + validator, err := k.GetValidator(ctx, valAddr) + if err != nil { + return err + } + + // handles updating state with the validators new consensus key and + // creating abci updates to pass to comet + updates, err := k.ApplyConsKeyRotation(ctx, validator, newPubKey, powerReduction) + if err != nil { + return err + } + totalUpdates = append(totalUpdates, updates...) + + // the new cons addr is now the validator's live cons addr; further + // rotations targeting it are blocked by the by cons addr lookup, + // so release its rotation lock entry. The old cons addr entry + // stays until the rotation matures. + if err := store.Delete(types.GetRotationLockedConsAddrIndexKey(sdk.ConsAddress(newPubKey.Address()))); err != nil { + return err + } + + return store.Delete(types.GetUnappliedConsKeyRotationKey(valAddr)) + }) + if err != nil { + return nil, err + } + + return totalUpdates, nil +} + +// ApplyConsKeyRotation switches the validator's consensus pubkey to newPubKey +// in x/staking state. The validator record is updated, the old by cons address +// index entry is deleted, and the new by cons address index entry is written. +func (k Keeper) ApplyConsKeyRotation(ctx context.Context, validator types.Validator, newPubKey cryptotypes.PubKey, powerReduction math.Int) (abci.ValidatorUpdates, error) { + // we will have two validator updates for every consensus key rotation + // since a key rotation to comet looks like a validator becoming 0 power + // (the old cons addr) and a new validator coming online with the new cons + // addr that has the same power as the old validator. + updates := make([]abci.ValidatorUpdate, 2) + + // create a validator update that will mark its current cons addr as 0 + // power + updates[0] = validator.ABCIValidatorUpdateZero() + + // capture the old cons addr before the in-memory swap below, so that we + // can delete the old by cons addr index entry further down + oldConsAddr, err := validator.GetConsAddr() + if err != nil { + return nil, err + } + + // update the validator in memory to use the new cons addr + newAny, err := codectypes.NewAnyWithValue(newPubKey) + if err != nil { + return nil, err + } + validator.ConsensusPubkey = newAny + + // create a validator update that will mark its new cons addr with the same + // power as its previous cons addr + updates[1] = validator.ABCIValidatorUpdate(powerReduction) + + // set the validator in the store (keyed by operator address, which didnt + // change) to the updated validator with the new cons addr + if err := k.SetValidator(ctx, validator); err != nil { + return nil, err + } + + // remove the store entry for the previous cons addr pointing to the + // validator + store := k.storeService.OpenKVStore(ctx) + if err := store.Delete(types.GetValidatorByConsAddrKey(oldConsAddr)); err != nil { + return nil, err + } + + // create a new store entry for the new cons addr pointing to the validator + if err := k.SetValidatorByConsAddr(ctx, validator); err != nil { + return nil, err + } + + return updates, nil +} + +// PruneMaturedConsKeyRotations removes every rotation whose unbonding window +// has elapsed at the current block time. It deletes the entries from the +// maturity queue, the per validator pending index, and the rotated consensus +// address index. +func (k Keeper) PruneMaturedConsKeyRotations(ctx context.Context) error { + sdkCtx := sdk.UnwrapSDKContext(ctx) + blockTime := sdkCtx.BlockHeader().Time + + keys, err := k.maturedConsKeyRotationKeys(ctx, blockTime) + if err != nil { + return err + } + + store := k.storeService.OpenKVStore(ctx) + for _, key := range keys { + if err := store.Delete(key); err != nil { + return err + } + } + return nil +} + +// maturedConsKeyRotationKeys walks the maturity queue up to blockTime and +// returns the full set of keys to delete to retire each matured rotation. +func (k Keeper) maturedConsKeyRotationKeys(ctx context.Context, blockTime time.Time) (keys [][]byte, err error) { + store := k.storeService.OpenKVStore(ctx) + iterator, err := store.Iterator( + types.ConsKeyRotationQueueKey, + storetypes.PrefixEndBytes(types.GetConsKeyRotationQueueTimePrefix(blockTime)), + ) + if err != nil { + return nil, err + } + defer func() { + err = errors.Join(err, iterator.Close()) + }() + + // TODO: migrate ValidatorSigningInfo from oldConsAddr to newConsAddr + for ; iterator.Valid(); iterator.Next() { + maturity, valAddr, perr := types.ParseConsKeyRotationQueueKey(iterator.Key()) + if perr != nil { + return nil, perr + } + oldConsAddr := sdk.ConsAddress(iterator.Value()) + + keys = append(keys, + types.GetConsKeyRotationQueueKey(maturity, valAddr), + types.GetValidatorConsKeyRotationKey(valAddr), + types.GetRotationLockedConsAddrIndexKey(oldConsAddr), + ) + } + return keys, nil +} + +// IterateUnappliedConsKeyRotations walks every rotation queued by the msg +// server that the end blocker has not yet applied, in valAddr sorted order. It +// is safe to delete store keys within the supplied callback. +func (k Keeper) IterateUnappliedConsKeyRotations( + ctx context.Context, + cb func(valAddr sdk.ValAddress, newPubKey cryptotypes.PubKey) error, +) error { + rotations, err := k.unappliedConsKeyRotations(ctx) + if err != nil { + return err + } + for _, r := range rotations { + if err := cb(r.valAddr, r.newPubKey); err != nil { + return err + } + } + return nil +} + +type unappliedConsKeyRotation struct { + valAddr sdk.ValAddress + newPubKey cryptotypes.PubKey +} + +// unappliedConsKeyRotations returns every rotation queued by the msg server +// that the end blocker has not yet applied. +func (k Keeper) unappliedConsKeyRotations(ctx context.Context) (rotations []unappliedConsKeyRotation, err error) { + store := k.storeService.OpenKVStore(ctx) + iterator, err := store.Iterator(types.UnappliedConsKeyRotationKey, storetypes.PrefixEndBytes(types.UnappliedConsKeyRotationKey)) + if err != nil { + return nil, err + } + defer func() { + err = errors.Join(err, iterator.Close()) + }() + + for ; iterator.Valid(); iterator.Next() { + key := iterator.Key() + valAddr := sdk.ValAddress(bytes.Clone(key[len(types.UnappliedConsKeyRotationKey)+1:])) + + var newPubKey cryptotypes.PubKey + if err := k.cdc.UnmarshalInterface(iterator.Value(), &newPubKey); err != nil { + return nil, err + } + rotations = append(rotations, unappliedConsKeyRotation{valAddr: valAddr, newPubKey: newPubKey}) + } + return rotations, nil +} diff --git a/x/staking/keeper/rotation_test.go b/x/staking/keeper/rotation_test.go new file mode 100644 index 000000000000..0a42f8deaf5c --- /dev/null +++ b/x/staking/keeper/rotation_test.go @@ -0,0 +1,232 @@ +package keeper_test + +import ( + "errors" + "testing" + "time" + + "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +func (s *KeeperTestSuite) TestApplyConsKeyRotation() { + require := s.Require() + + createValidator := func() (stakingtypes.Validator, sdk.ValAddress, cryptotypes.PubKey) { + pk := ed25519.GenPrivKey().PubKey() + valAddr := sdk.ValAddress(pk.Address()) + v, err := stakingtypes.NewValidator(valAddr.String(), pk, stakingtypes.Description{Moniker: "v"}) + require.NoError(err) + v.Status = stakingtypes.Bonded + v.Tokens = sdk.TokensFromConsensusPower(10, sdk.DefaultPowerReduction) + v.DelegatorShares = math.LegacyNewDecFromInt(v.Tokens) + require.NoError(s.stakingKeeper.SetValidator(s.ctx, v)) + require.NoError(s.stakingKeeper.SetValidatorByConsAddr(s.ctx, v)) + return v, valAddr, pk + } + + testCases := []struct { + name string + setup func() (validator stakingtypes.Validator, valAddr sdk.ValAddress, oldPk, newPk cryptotypes.PubKey) + }{ + { + name: "successful rotation", + setup: func() (stakingtypes.Validator, sdk.ValAddress, cryptotypes.PubKey, cryptotypes.PubKey) { + v, valAddr, oldPk := createValidator() + return v, valAddr, oldPk, ed25519.GenPrivKey().PubKey() + }, + }, + { + name: "rotate to same key is a no-op", + setup: func() (stakingtypes.Validator, sdk.ValAddress, cryptotypes.PubKey, cryptotypes.PubKey) { + v, valAddr, oldPk := createValidator() + return v, valAddr, oldPk, oldPk + }, + }, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + s.SetupTest() + + validator, valAddr, oldPk, newPk := tc.setup() + + updates, err := s.stakingKeeper.ApplyConsKeyRotation(s.ctx, validator, newPk, sdk.DefaultPowerReduction) + require.NoError(err) + require.Len(updates, 2) + require.Equal(int64(0), updates[0].Power) + require.Equal(int64(10), updates[1].Power) + + // validator's stored ConsensusPubkey must now resolve to newPk + stored, err := s.stakingKeeper.GetValidator(s.ctx, valAddr) + require.NoError(err) + gotConsAddr, err := stored.GetConsAddr() + require.NoError(err) + require.Equal(sdk.ConsAddress(newPk.Address()).Bytes(), gotConsAddr) + + // new by cons address lookup resolves to this validator + byNew, err := s.stakingKeeper.GetValidatorByConsAddr(s.ctx, sdk.ConsAddress(newPk.Address())) + require.NoError(err) + require.Equal(valAddr.String(), byNew.OperatorAddress) + + // old by cons address lookup is gone unless the rotation was a no-op + if !oldPk.Equals(newPk) { + _, err = s.stakingKeeper.GetValidatorByConsAddr(s.ctx, sdk.ConsAddress(oldPk.Address())) + require.Error(err) + } + }) + } +} + +func (s *KeeperTestSuite) TestIterateUnappliedConsKeyRotations() { + require := s.Require() + + type entry struct { + valAddr sdk.ValAddress + newPk cryptotypes.PubKey + } + + errStop := errors.New("stop") + + testCases := []struct { + name string + seedCount int + stopAfter int // 0 = no stop + expectLen int + expectErr error + }{ + {name: "empty store", seedCount: 0, expectLen: 0}, + {name: "single entry", seedCount: 1, expectLen: 1}, + {name: "three entries", seedCount: 3, expectLen: 3}, + {name: "stop with error after first of three", seedCount: 3, stopAfter: 1, expectLen: 1, expectErr: errStop}, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + s.SetupTest() + + seeded := make([]entry, tc.seedCount) + for i := range seeded { + seeded[i] = entry{ + valAddr: sdk.ValAddress(ed25519.GenPrivKey().PubKey().Address()), + newPk: ed25519.GenPrivKey().PubKey(), + } + oldPk := ed25519.GenPrivKey().PubKey() + require.NoError(s.stakingKeeper.SetConsKeyRotation(s.ctx, seeded[i].valAddr, oldPk, seeded[i].newPk)) + } + + var observed []entry + err := s.stakingKeeper.IterateUnappliedConsKeyRotations(s.ctx, func(valAddr sdk.ValAddress, newPk cryptotypes.PubKey) error { + observed = append(observed, entry{valAddr, newPk}) + if tc.stopAfter > 0 && len(observed) >= tc.stopAfter { + return errStop + } + return nil + }) + if tc.expectErr != nil { + require.ErrorIs(err, tc.expectErr) + } else { + require.NoError(err) + } + require.Len(observed, tc.expectLen) + + // each observed entry must round trip to one of the seeded entries + for _, got := range observed { + found := false + for _, want := range seeded { + if got.valAddr.Equals(want.valAddr) && got.newPk.Equals(want.newPk) { + found = true + break + } + } + require.True(found, "observed entry not in seeded set: %s", got.valAddr) + } + }) + } +} + +func (s *KeeperTestSuite) TestPruneMaturedConsKeyRotations() { + require := s.Require() + + type rec struct { + valAddr sdk.ValAddress + consAddr sdk.ConsAddress + maturity time.Time + } + + queueRotation := func() rec { + oldPk := ed25519.GenPrivKey().PubKey() + valAddr := sdk.ValAddress(oldPk.Address()) + newPk := ed25519.GenPrivKey().PubKey() + maturity := s.ctx.BlockTime().Add(stakingtypes.DefaultUnbondingTime) + require.NoError(s.stakingKeeper.SetConsKeyRotation(s.ctx, valAddr, oldPk, newPk)) + return rec{valAddr, sdk.ConsAddress(oldPk.Address()), maturity} + } + + testCases := []struct { + name string + matured int + notMatured int + }{ + {name: "empty queue", matured: 0, notMatured: 0}, + {name: "single matured entry", matured: 1, notMatured: 0}, + {name: "single not yet matured entry", matured: 0, notMatured: 1}, + {name: "mixed matured and future", matured: 2, notMatured: 2}, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + s.SetupTest() + + baseTime := s.ctx.BlockTime() + var maturedEntries, futureEntries []rec + + // queue matured entries by rewinding the context so their maturity + // (queueTime + unbondingTime) falls strictly before baseTime + s.ctx = s.ctx.WithBlockTime(baseTime.Add(-stakingtypes.DefaultUnbondingTime - time.Hour)) + for i := 0; i < tc.matured; i++ { + maturedEntries = append(maturedEntries, queueRotation()) + } + + // queue future entries at baseTime so their maturity is in the future + s.ctx = s.ctx.WithBlockTime(baseTime) + for i := 0; i < tc.notMatured; i++ { + futureEntries = append(futureEntries, queueRotation()) + } + + require.NoError(s.stakingKeeper.PruneMaturedConsKeyRotations(s.ctx)) + + for _, e := range maturedEntries { + hasQueue, err := s.stakingKeeper.HasConsKeyRotationQueueEntry(s.ctx, e.maturity, e.valAddr) + require.NoError(err) + require.False(hasQueue, "matured queue entry should be pruned") + + hasPending, err := s.stakingKeeper.HasConsKeyRotationInUnbondingWindow(s.ctx, e.valAddr) + require.NoError(err) + require.False(hasPending, "matured per-validator entry should be pruned") + + hasCons, err := s.stakingKeeper.IsConsAddrLockedByRotation(s.ctx, e.consAddr) + require.NoError(err) + require.False(hasCons, "matured rotated cons addr entry should be pruned") + } + + for _, e := range futureEntries { + hasQueue, err := s.stakingKeeper.HasConsKeyRotationQueueEntry(s.ctx, e.maturity, e.valAddr) + require.NoError(err) + require.True(hasQueue, "future queue entry should remain") + + hasPending, err := s.stakingKeeper.HasConsKeyRotationInUnbondingWindow(s.ctx, e.valAddr) + require.NoError(err) + require.True(hasPending, "future per-validator entry should remain") + + hasCons, err := s.stakingKeeper.IsConsAddrLockedByRotation(s.ctx, e.consAddr) + require.NoError(err) + require.True(hasCons, "future rotated cons addr entry should remain") + } + }) + } +} diff --git a/x/staking/keeper/val_state_change.go b/x/staking/keeper/val_state_change.go index 7761d80a2eea..243206c663c0 100644 --- a/x/staking/keeper/val_state_change.go +++ b/x/staking/keeper/val_state_change.go @@ -113,6 +113,12 @@ func (k Keeper) BlockValidatorUpdates(ctx context.Context) ([]abci.ValidatorUpda ) } + // prune consensus key rotations that have fallen out of the current + // unbonding period. + if err := k.PruneMaturedConsKeyRotations(ctx); err != nil { + return nil, err + } + return validatorUpdates, nil } @@ -238,6 +244,17 @@ func (k Keeper) ApplyAndReturnValidatorSetUpdates(ctx context.Context) (updates updates = append(updates, validator.ABCIValidatorUpdateZero()) } + // apply pending consensus key rotations. each rotation emits a zero + // power update for the old key followed by a current power update for + // the new key. an old key that already received a power change earlier + // in this updates list is correctly retired because cometbft applies + // updates in order. + rotationUpdates, err := k.ApplyPendingConsKeyRotations(ctx, powerReduction) + if err != nil { + return nil, err + } + updates = append(updates, rotationUpdates...) + // Update the pools based on the recent updates in the validator set: // - The tokens from the non-bonded candidates that enter the new validator set need to be transferred // to the Bonded pool. diff --git a/x/staking/keeper/val_state_change_test.go b/x/staking/keeper/val_state_change_test.go new file mode 100644 index 000000000000..72e9ac0e8574 --- /dev/null +++ b/x/staking/keeper/val_state_change_test.go @@ -0,0 +1,80 @@ +package keeper_test + +import ( + "cosmossdk.io/math" + + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// TestApplyAndReturnValidatorSetUpdatesWithKeyRotation exercises the EndBlocker +// path where the validator set update loop has already appended an update for +// the validator's old consensus key (because its power changed) and then a +// queued key rotation runs. The new key must end up at the validator's current +// power, not at a delta against the prior update. +func (s *KeeperTestSuite) TestApplyAndReturnValidatorSetUpdatesWithKeyRotation() { + require := s.Require() + + powerReduction := s.stakingKeeper.PowerReduction(s.ctx) + + // set up a bonded validator with 10 consensus power. LastValidatorPower + // is intentionally not seeded, so the main loop will append an update + // for the old key with the validator's current power. + oldPk := ed25519.GenPrivKey().PubKey() + valAddr := sdk.ValAddress(oldPk.Address()) + v, err := stakingtypes.NewValidator(valAddr.String(), oldPk, stakingtypes.Description{Moniker: "v"}) + require.NoError(err) + v.Status = stakingtypes.Bonded + v.Tokens = sdk.TokensFromConsensusPower(10, powerReduction) + v.DelegatorShares = math.LegacyNewDecFromInt(v.Tokens) + require.NoError(s.stakingKeeper.SetValidator(s.ctx, v)) + require.NoError(s.stakingKeeper.SetValidatorByConsAddr(s.ctx, v)) + require.NoError(s.stakingKeeper.SetNewValidatorByPowerIndex(s.ctx, v)) + + // queue a key rotation for the same validator + newPk := ed25519.GenPrivKey().PubKey() + require.NoError(s.stakingKeeper.SetConsKeyRotation(s.ctx, valAddr, oldPk, newPk)) + + updates, err := s.stakingKeeper.ApplyAndReturnValidatorSetUpdates(s.ctx) + require.NoError(err) + + // expected list, in order: + // [0] old @ 10 (from the main loop discovering a power change) + // [1] old @ 0 (from the rotation retiring the old key) + // [2] new @ 10 (from the rotation instating the new key) + require.Len(updates, 3) + + oldCmtPk, err := cryptocodec.ToCmtProtoPublicKey(oldPk) + require.NoError(err) + newCmtPk, err := cryptocodec.ToCmtProtoPublicKey(newPk) + require.NoError(err) + + require.Equal(oldCmtPk, updates[0].PubKey) + require.Equal(int64(10), updates[0].Power) + + require.Equal(oldCmtPk, updates[1].PubKey) + require.Equal(int64(0), updates[1].Power) + + require.Equal(newCmtPk, updates[2].PubKey) + require.Equal(int64(10), updates[2].Power) + + // simulate cometbft applying the updates in order, last write wins per + // key. the final state must have the old key removed and the new key at + // the validator's current power. + finalPower := map[string]int64{} + for _, u := range updates { + bz, err := u.PubKey.Marshal() + require.NoError(err) + finalPower[string(bz)] = u.Power + } + + oldBz, err := oldCmtPk.Marshal() + require.NoError(err) + newBz, err := newCmtPk.Marshal() + require.NoError(err) + + require.Equal(int64(0), finalPower[string(oldBz)]) + require.Equal(int64(10), finalPower[string(newBz)]) +} diff --git a/x/staking/keeper_bench_test.go b/x/staking/keeper_bench_test.go index 6d70064595cf..eb188f854895 100644 --- a/x/staking/keeper_bench_test.go +++ b/x/staking/keeper_bench_test.go @@ -79,6 +79,8 @@ func newTestEnvironment(tb testing.TB) *KeeperTestEnvironment { Return(authtypes.NewEmptyModuleAccount(types.BondedPoolName).GetAddress()) accountKeeper.EXPECT().GetModuleAddress(types.NotBondedPoolName). Return(authtypes.NewEmptyModuleAccount(types.NotBondedPoolName).GetAddress()) + accountKeeper.EXPECT().GetModuleAddress(types.KeyRotationFeePoolName). + Return(authtypes.NewEmptyModuleAccount(types.KeyRotationFeePoolName).GetAddress()) accountKeeper.EXPECT().AddressCodec().Return(address.NewBech32Codec("cosmos")).AnyTimes() bankKeeper := stakingtestutil.NewMockBankKeeper(ctrl) diff --git a/x/staking/module_test.go b/x/staking/module_test.go index 67d560c1d976..32f471f91bab 100644 --- a/x/staking/module_test.go +++ b/x/staking/module_test.go @@ -30,4 +30,7 @@ func TestItCreatesModuleAccountOnInitBlock(t *testing.T) { acc = accountKeeper.GetAccount(ctx, authtypes.NewModuleAddress(types.NotBondedPoolName)) require.NotNil(t, acc) + + acc = accountKeeper.GetAccount(ctx, authtypes.NewModuleAddress(types.KeyRotationFeePoolName)) + require.NotNil(t, acc) } diff --git a/x/staking/testutil/expected_keepers_mocks.go b/x/staking/testutil/expected_keepers_mocks.go index 0d93358816eb..6c1aaf1deccc 100644 --- a/x/staking/testutil/expected_keepers_mocks.go +++ b/x/staking/testutil/expected_keepers_mocks.go @@ -233,6 +233,20 @@ func (mr *MockBankKeeperMockRecorder) LockedCoins(ctx, addr any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LockedCoins", reflect.TypeOf((*MockBankKeeper)(nil).LockedCoins), ctx, addr) } +// SendCoinsFromAccountToModule mocks base method. +func (m *MockBankKeeper) SendCoinsFromAccountToModule(ctx context.Context, senderAddr types.AccAddress, recipientModule string, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoinsFromAccountToModule", ctx, senderAddr, recipientModule, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoinsFromAccountToModule indicates an expected call of SendCoinsFromAccountToModule. +func (mr *MockBankKeeperMockRecorder) SendCoinsFromAccountToModule(ctx, senderAddr, recipientModule, amt any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromAccountToModule", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromAccountToModule), ctx, senderAddr, recipientModule, amt) +} + // SendCoinsFromModuleToModule mocks base method. func (m *MockBankKeeper) SendCoinsFromModuleToModule(ctx context.Context, senderPool, recipientPool string, amt types.Coins) error { m.ctrl.T.Helper() diff --git a/x/staking/types/errors.go b/x/staking/types/errors.go index 585a9e8e83ca..cfe070c62646 100644 --- a/x/staking/types/errors.go +++ b/x/staking/types/errors.go @@ -48,4 +48,8 @@ var ( ErrInvalidSigner = errors.Register(ModuleName, 43, "expected authority account as only signer for proposal message") ErrBadRedelegationSrc = errors.Register(ModuleName, 44, "redelegation source validator not found") ErrNoUnbondingType = errors.Register(ModuleName, 45, "unbonding type not found") + + ErrConsensusPubKeyAlreadyUsedForValidator = errors.Register(ModuleName, 46, "consensus pubkey is already in use by another validator") + ErrConsensusPubKeyInRotationHistory = errors.Register(ModuleName, 47, "consensus pubkey was previously rotated away from and is still within the unbonding window") + ErrExceedingMaxConsPubKeyRotations = errors.Register(ModuleName, 48, "validator has reached the max consensus pubkey rotations within the unbonding period") ) diff --git a/x/staking/types/expected_keepers.go b/x/staking/types/expected_keepers.go index f9da52098456..ea47bdef0292 100644 --- a/x/staking/types/expected_keepers.go +++ b/x/staking/types/expected_keepers.go @@ -35,6 +35,7 @@ type BankKeeper interface { GetSupply(ctx context.Context, denom string) sdk.Coin SendCoinsFromModuleToModule(ctx context.Context, senderPool, recipientPool string, amt sdk.Coins) error + SendCoinsFromAccountToModule(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error UndelegateCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error DelegateCoinsFromAccountToModule(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error diff --git a/x/staking/types/keys.go b/x/staking/types/keys.go index 8f51f6f8800d..9be1e41e877d 100644 --- a/x/staking/types/keys.go +++ b/x/staking/types/keys.go @@ -60,6 +60,40 @@ var ( // NOTE: keys in range 0x81–0x87 were previously used in liquid staking forks of the staking module. // Module developers MUST NOT use these keys and MUST consider them "reserved". + + // ConsKeyRotationQueueKey allows us to iterate over key rotations + // happening by time, so that the end blocker can quickly determine which + // key rotations we can stop keeping track of since they have fallen out of + // the current unbonding period. + ConsKeyRotationQueueKey = []byte{0x91} // prefix for the consensus key rotation maturity queue, keyed by (time, valAddr) + + // ValidatorConsKeyRotationKey allows us lookup key rotations happening by + // validator address, so we can quickly determine in the msg server how + // many key rotations this validator has done in the unbonding period to + // enforce the max key rotation limit. This is pruned when the key rotation + // falls out of the current unbonding period in the end blocker (determined + // by the ConsKeyRotationQueueKey). + ValidatorConsKeyRotationKey = []byte{0x92} // prefix for a validator's pending consensus key rotation, keyed by valAddr + + // RotationLockedConsAddrIndexKey marks a consensus address as locked by + // a key rotation, either because some validator previously rotated away + // from it (and is still inside its unbonding window) or because some + // validator has enqueued a pending rotation targeting it. In both cases + // the address must not be the target of a new rotation. The old key + // lookup also lets slashing/evidence handling associate an infraction + // on an old consensus key with the new consensus key. The old key entry + // is pruned when its rotation falls out of the unbonding window + // (determined by the ConsKeyRotationQueueKey); the new key entry is + // removed when the rotation is applied in the end blocker. + RotationLockedConsAddrIndexKey = []byte{0x93} // prefix for the rotation-locked consensus address lookup + + // UnappliedConsKeyRotationKey is the drain queue of rotations that the + // msg server has accepted but the end blocker has not yet performed. + // Each entry holds the new pubkey to apply. The end blocker iterates + // this prefix once per block, applies each rotation, and deletes the + // entry. The other three rotation stores remain so the rotation can be + // tracked through the rest of the unbonding period. + UnappliedConsKeyRotationKey = []byte{0x94} // prefix for unapplied consensus key rotations, keyed by valAddr ) // UnbondingType defines the type of unbonding operation @@ -426,3 +460,61 @@ func GetHistoricalInfoKey(height int64) []byte { binary.BigEndian.PutUint64(heightBytes, uint64(height)) return append(HistoricalInfoKey, heightBytes...) } + +// GetConsKeyRotationQueueKey returns the queue key for a pending rotation +// maturing at the given time. +func GetConsKeyRotationQueueKey(maturity time.Time, valAddr sdk.ValAddress) []byte { + timeBz := sdk.FormatTimeBytes(maturity) + valBz := address.MustLengthPrefix(valAddr) + + key := make([]byte, len(ConsKeyRotationQueueKey)+len(timeBz)+len(valBz)) + copy(key, ConsKeyRotationQueueKey) + copy(key[len(ConsKeyRotationQueueKey):], timeBz) + copy(key[len(ConsKeyRotationQueueKey)+len(timeBz):], valBz) + return key +} + +// GetConsKeyRotationQueueTimePrefix returns the queue iteration prefix up to +// the given time. +func GetConsKeyRotationQueueTimePrefix(maturity time.Time) []byte { + return append(ConsKeyRotationQueueKey, sdk.FormatTimeBytes(maturity)...) +} + +// ParseConsKeyRotationQueueKey extracts the maturity time and validator +// address from a queue key. +func ParseConsKeyRotationQueueKey(bz []byte) (time.Time, sdk.ValAddress, error) { + prefixLen := len(ConsKeyRotationQueueKey) + if prefix := bz[:prefixLen]; !bytes.Equal(prefix, ConsKeyRotationQueueKey) { + return time.Time{}, nil, fmt.Errorf("invalid prefix; expected: %X, got: %X", ConsKeyRotationQueueKey, prefix) + } + + timeLen := len(sdk.SortableTimeFormat) + kv.AssertKeyAtLeastLength(bz, prefixLen+timeLen+1) + + ts, err := sdk.ParseTimeBytes(bz[prefixLen : prefixLen+timeLen]) + if err != nil { + return time.Time{}, nil, err + } + + valAddrLen := int(bz[prefixLen+timeLen]) + kv.AssertKeyAtLeastLength(bz, prefixLen+timeLen+1+valAddrLen) + + return ts, sdk.ValAddress(bz[prefixLen+timeLen+1 : prefixLen+timeLen+1+valAddrLen]), nil +} + +// GetValidatorConsKeyRotationKey returns the key for a validator's pending rotation record. +func GetValidatorConsKeyRotationKey(valAddr sdk.ValAddress) []byte { + return append(ValidatorConsKeyRotationKey, address.MustLengthPrefix(valAddr)...) +} + +// GetRotationLockedConsAddrIndexKey returns the lookup key for a consensus +// address that is locked by a pending or recently completed rotation. +func GetRotationLockedConsAddrIndexKey(consAddr sdk.ConsAddress) []byte { + return append(RotationLockedConsAddrIndexKey, address.MustLengthPrefix(consAddr)...) +} + +// GetUnappliedConsKeyRotationKey returns the key for a rotation that the +// msg server has queued and the end blocker has not yet applied. +func GetUnappliedConsKeyRotationKey(valAddr sdk.ValAddress) []byte { + return append(UnappliedConsKeyRotationKey, address.MustLengthPrefix(valAddr)...) +} diff --git a/x/staking/types/keys_test.go b/x/staking/types/keys_test.go index f9fa94979623..48a52d504eb8 100644 --- a/x/staking/types/keys_test.go +++ b/x/staking/types/keys_test.go @@ -134,6 +134,149 @@ func TestTestGetValidatorQueueKeyOrder(t *testing.T) { require.Equal(t, 1, bytes.Compare(keyC, endKey)) // keyB >= endKey } +func TestGetConsKeyRotationQueueKey(t *testing.T) { + tests := []struct { + name string + ts time.Time + valAddr sdk.ValAddress + }{ + {"keysAddr1 now", time.Now(), sdk.ValAddress(keysAddr1)}, + {"keysAddr2 epoch", time.Unix(0, 0), sdk.ValAddress(keysAddr2)}, + {"keysAddr3 future", time.Now().Add(24 * time.Hour), sdk.ValAddress(keysAddr3)}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bz := types.GetConsKeyRotationQueueKey(tt.ts, tt.valAddr) + gotTs, gotValAddr, err := types.ParseConsKeyRotationQueueKey(bz) + require.NoError(t, err) + require.Equal(t, tt.ts.UTC(), gotTs.UTC()) + require.Equal(t, tt.valAddr, gotValAddr) + }) + } +} + +func TestGetConsKeyRotationQueueKeyOrder(t *testing.T) { + ts := time.Now().UTC() + valAddr := sdk.ValAddress(keysAddr1) + endKey := types.GetConsKeyRotationQueueKey(ts, valAddr) + + keyA := types.GetConsKeyRotationQueueKey(ts.Add(-10*time.Minute), valAddr) + keyB := types.GetConsKeyRotationQueueKey(ts.Add(-5*time.Minute), valAddr) + keyC := types.GetConsKeyRotationQueueKey(ts.Add(10*time.Minute), valAddr) + + require.Equal(t, -1, bytes.Compare(keyA, endKey)) + require.Equal(t, -1, bytes.Compare(keyB, endKey)) + require.Equal(t, 1, bytes.Compare(keyC, endKey)) +} + +func TestParseConsKeyRotationQueueKey(t *testing.T) { + ts := time.Date(2025, 1, 15, 12, 0, 0, 0, time.UTC) + valAddr := sdk.ValAddress(keysAddr1) + + tests := []struct { + name string + buildKey func() []byte + expErrContains string + expTs time.Time + expValAddr sdk.ValAddress + }{ + { + name: "valid key", + buildKey: func() []byte { return types.GetConsKeyRotationQueueKey(ts, valAddr) }, + expTs: ts, + expValAddr: valAddr, + }, + { + name: "wrong prefix", + buildKey: func() []byte { + bz := types.GetConsKeyRotationQueueKey(ts, valAddr) + bz[0] = 0xff + return bz + }, + expErrContains: "invalid prefix", + }, + { + name: "unparseable time bytes", + buildKey: func() []byte { + bz := types.GetConsKeyRotationQueueKey(ts, valAddr) + prefixLen := len(types.ConsKeyRotationQueueKey) + for i := prefixLen; i < prefixLen+5; i++ { + bz[i] = 0xff + } + return bz + }, + expErrContains: "cannot parse", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotTs, gotValAddr, err := types.ParseConsKeyRotationQueueKey(tt.buildKey()) + if tt.expErrContains != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expErrContains) + return + } + require.NoError(t, err) + require.Equal(t, tt.expTs.UTC(), gotTs.UTC()) + require.Equal(t, tt.expValAddr, gotValAddr) + }) + } +} + +func TestGetConsKeyRotationQueueTimePrefix(t *testing.T) { + ts := time.Now() + prefix := types.GetConsKeyRotationQueueTimePrefix(ts) + full := types.GetConsKeyRotationQueueKey(ts, sdk.ValAddress(keysAddr1)) + + require.True(t, bytes.HasPrefix(full, prefix)) + require.Equal(t, len(types.ConsKeyRotationQueueKey)+len(sdk.FormatTimeBytes(ts)), len(prefix)) +} + +func TestGetValidatorConsKeyRotationKey(t *testing.T) { + tests := []struct { + valAddr sdk.ValAddress + wantHex string + }{ + {sdk.ValAddress(keysAddr1), "921463d771218209d8bd03c482f69dfba57310f08609"}, + {sdk.ValAddress(keysAddr2), "92145ef3b5f25c54946d4a89fc0d09d2f126614540f2"}, + {sdk.ValAddress(keysAddr3), "92143ab62f0d93849be495e21e3e9013a517038f45bd"}, + } + for i, tt := range tests { + got := hex.EncodeToString(types.GetValidatorConsKeyRotationKey(tt.valAddr)) + require.Equal(t, tt.wantHex, got, "Keys did not match on test case %d", i) + } +} + +func TestGetRotationLockedConsAddrIndexKey(t *testing.T) { + tests := []struct { + consAddr sdk.ConsAddress + wantHex string + }{ + {sdk.ConsAddress(keysAddr1), "931463d771218209d8bd03c482f69dfba57310f08609"}, + {sdk.ConsAddress(keysAddr2), "93145ef3b5f25c54946d4a89fc0d09d2f126614540f2"}, + {sdk.ConsAddress(keysAddr3), "93143ab62f0d93849be495e21e3e9013a517038f45bd"}, + } + for i, tt := range tests { + got := hex.EncodeToString(types.GetRotationLockedConsAddrIndexKey(tt.consAddr)) + require.Equal(t, tt.wantHex, got, "Keys did not match on test case %d", i) + } +} + +func TestGetUnappliedConsKeyRotationKey(t *testing.T) { + tests := []struct { + valAddr sdk.ValAddress + wantHex string + }{ + {sdk.ValAddress(keysAddr1), "941463d771218209d8bd03c482f69dfba57310f08609"}, + {sdk.ValAddress(keysAddr2), "94145ef3b5f25c54946d4a89fc0d09d2f126614540f2"}, + {sdk.ValAddress(keysAddr3), "94143ab62f0d93849be495e21e3e9013a517038f45bd"}, + } + for i, tt := range tests { + got := hex.EncodeToString(types.GetUnappliedConsKeyRotationKey(tt.valAddr)) + require.Equal(t, tt.wantHex, got, "Keys did not match on test case %d", i) + } +} + func TestGetHistoricalInfoKey(t *testing.T) { tests := []struct { height int64 diff --git a/x/staking/types/params.go b/x/staking/types/params.go index fa5dcffbe3ae..f8f186fe3e86 100644 --- a/x/staking/types/params.go +++ b/x/staking/types/params.go @@ -31,8 +31,15 @@ const ( DefaultHistoricalEntries uint32 = 10000 ) -// DefaultMinCommissionRate is set to 0% -var DefaultMinCommissionRate = math.LegacyZeroDec() +var ( + // DefaultMinCommissionRate is set to 0% + DefaultMinCommissionRate = math.LegacyZeroDec() + + // DefaultKeyRotationFee is the fee charged to rotate a validators ConsPubkey + // + // TODO: move this into the actual params struct + DefaultKeyRotationFee = sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000000) +) // NewParams creates a new Params instance func NewParams(unbondingTime time.Duration, maxValidators, maxEntries, historicalEntries uint32, bondDenom string, minCommissionRate math.LegacyDec) Params { diff --git a/x/staking/types/pool.go b/x/staking/types/pool.go index 79f24d33705c..0ca29252fe7d 100644 --- a/x/staking/types/pool.go +++ b/x/staking/types/pool.go @@ -9,9 +9,12 @@ import ( // - NotBondedPool -> "not_bonded_tokens_pool" // // - BondedPool -> "bonded_tokens_pool" +// +// - KeyRotationFeePool -> "key_rotation_fee_pool" const ( - NotBondedPoolName = "not_bonded_tokens_pool" - BondedPoolName = "bonded_tokens_pool" + NotBondedPoolName = "not_bonded_tokens_pool" + BondedPoolName = "bonded_tokens_pool" + KeyRotationFeePoolName = "key_rotation_fee_pool" ) // NewPool creates a new Pool instance used for queries diff --git a/x/staking/types/tx.pb.go b/x/staking/types/tx.pb.go index 92d9a3884976..bdfbf1fad650 100644 --- a/x/staking/types/tx.pb.go +++ b/x/staking/types/tx.pb.go @@ -639,6 +639,83 @@ func (m *MsgUpdateParamsResponse) XXX_DiscardUnknown() { var xxx_messageInfo_MsgUpdateParamsResponse proto.InternalMessageInfo +// MsgRotateConsPubKey is the Msg/RotateConsPubKey request type. +type MsgRotateConsPubKey struct { + ValidatorAddress string `protobuf:"bytes,1,opt,name=validator_address,json=validatorAddress,proto3" json:"validator_address,omitempty"` + NewPubkey *any.Any `protobuf:"bytes,2,opt,name=new_pubkey,json=newPubkey,proto3" json:"new_pubkey,omitempty"` +} + +func (m *MsgRotateConsPubKey) Reset() { *m = MsgRotateConsPubKey{} } +func (m *MsgRotateConsPubKey) String() string { return proto.CompactTextString(m) } +func (*MsgRotateConsPubKey) ProtoMessage() {} +func (*MsgRotateConsPubKey) Descriptor() ([]byte, []int) { + return fileDescriptor_0926ef28816b35ab, []int{14} +} +func (m *MsgRotateConsPubKey) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgRotateConsPubKey) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgRotateConsPubKey.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgRotateConsPubKey) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgRotateConsPubKey.Merge(m, src) +} +func (m *MsgRotateConsPubKey) XXX_Size() int { + return m.Size() +} +func (m *MsgRotateConsPubKey) XXX_DiscardUnknown() { + xxx_messageInfo_MsgRotateConsPubKey.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgRotateConsPubKey proto.InternalMessageInfo + +// MsgRotateConsPubKeyResponse defines the response structure for executing a +// MsgRotateConsPubKey message. +type MsgRotateConsPubKeyResponse struct { +} + +func (m *MsgRotateConsPubKeyResponse) Reset() { *m = MsgRotateConsPubKeyResponse{} } +func (m *MsgRotateConsPubKeyResponse) String() string { return proto.CompactTextString(m) } +func (*MsgRotateConsPubKeyResponse) ProtoMessage() {} +func (*MsgRotateConsPubKeyResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_0926ef28816b35ab, []int{15} +} +func (m *MsgRotateConsPubKeyResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgRotateConsPubKeyResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgRotateConsPubKeyResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgRotateConsPubKeyResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgRotateConsPubKeyResponse.Merge(m, src) +} +func (m *MsgRotateConsPubKeyResponse) XXX_Size() int { + return m.Size() +} +func (m *MsgRotateConsPubKeyResponse) XXX_DiscardUnknown() { + xxx_messageInfo_MsgRotateConsPubKeyResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgRotateConsPubKeyResponse proto.InternalMessageInfo + func init() { proto.RegisterType((*MsgCreateValidator)(nil), "cosmos.staking.v1beta1.MsgCreateValidator") proto.RegisterType((*MsgCreateValidatorResponse)(nil), "cosmos.staking.v1beta1.MsgCreateValidatorResponse") @@ -654,87 +731,93 @@ func init() { proto.RegisterType((*MsgCancelUnbondingDelegationResponse)(nil), "cosmos.staking.v1beta1.MsgCancelUnbondingDelegationResponse") proto.RegisterType((*MsgUpdateParams)(nil), "cosmos.staking.v1beta1.MsgUpdateParams") proto.RegisterType((*MsgUpdateParamsResponse)(nil), "cosmos.staking.v1beta1.MsgUpdateParamsResponse") + proto.RegisterType((*MsgRotateConsPubKey)(nil), "cosmos.staking.v1beta1.MsgRotateConsPubKey") + proto.RegisterType((*MsgRotateConsPubKeyResponse)(nil), "cosmos.staking.v1beta1.MsgRotateConsPubKeyResponse") } func init() { proto.RegisterFile("cosmos/staking/v1beta1/tx.proto", fileDescriptor_0926ef28816b35ab) } var fileDescriptor_0926ef28816b35ab = []byte{ - // 1187 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x58, 0xcf, 0x6f, 0xdc, 0x44, - 0x14, 0x5e, 0xef, 0x26, 0x0b, 0x99, 0x90, 0x6c, 0xe2, 0x24, 0xed, 0xc6, 0x0d, 0xbb, 0xc1, 0x0d, - 0x4a, 0x14, 0x58, 0x3b, 0x0d, 0xa5, 0x11, 0xdb, 0x0a, 0x35, 0xdb, 0xb4, 0x50, 0x20, 0x10, 0x39, - 0xa4, 0x48, 0x08, 0xb4, 0xcc, 0xda, 0x13, 0xc7, 0xca, 0xda, 0xe3, 0x7a, 0x66, 0xa3, 0xee, 0x01, - 0x09, 0x71, 0x02, 0x4e, 0xfd, 0x07, 0x90, 0x8a, 0x04, 0x12, 0xc7, 0x1c, 0x72, 0xe4, 0x4e, 0xd5, - 0x53, 0x95, 0x53, 0xd5, 0x43, 0x40, 0xc9, 0x21, 0xfc, 0x0f, 0xbd, 0x20, 0xdb, 0x63, 0xef, 0xda, - 0xfb, 0xb3, 0x81, 0x5e, 0x7a, 0x49, 0x36, 0x33, 0xdf, 0x7c, 0x6f, 0xde, 0xf7, 0xbd, 0x37, 0x33, - 0x1b, 0x90, 0x57, 0x31, 0x31, 0x31, 0x91, 0x09, 0x85, 0xbb, 0x86, 0xa5, 0xcb, 0x7b, 0x97, 0x2a, - 0x88, 0xc2, 0x4b, 0x32, 0xbd, 0x27, 0xd9, 0x0e, 0xa6, 0x98, 0x3f, 0xe7, 0x03, 0x24, 0x06, 0x90, - 0x18, 0x40, 0x98, 0xd6, 0x31, 0xd6, 0xab, 0x48, 0xf6, 0x50, 0x95, 0xda, 0xb6, 0x0c, 0xad, 0xba, - 0xbf, 0x44, 0xc8, 0xc7, 0xa7, 0xa8, 0x61, 0x22, 0x42, 0xa1, 0x69, 0x33, 0xc0, 0xa4, 0x8e, 0x75, - 0xec, 0x7d, 0x94, 0xdd, 0x4f, 0x6c, 0x74, 0xda, 0x8f, 0x54, 0xf6, 0x27, 0x58, 0x58, 0x7f, 0x2a, - 0xc7, 0x76, 0x59, 0x81, 0x04, 0x85, 0x5b, 0x54, 0xb1, 0x61, 0xb1, 0xf9, 0xb9, 0x0e, 0x59, 0x04, - 0x9b, 0xf6, 0x51, 0xe7, 0x19, 0xca, 0x24, 0x2e, 0xc2, 0xfd, 0xc5, 0x26, 0xc6, 0xa1, 0x69, 0x58, - 0x58, 0xf6, 0x7e, 0xfa, 0x43, 0xe2, 0xb3, 0x01, 0xc0, 0xaf, 0x13, 0xfd, 0x86, 0x83, 0x20, 0x45, - 0x77, 0x60, 0xd5, 0xd0, 0x20, 0xc5, 0x0e, 0xbf, 0x01, 0x86, 0x35, 0x44, 0x54, 0xc7, 0xb0, 0xa9, - 0x81, 0xad, 0x2c, 0x37, 0xcb, 0x2d, 0x0c, 0x2f, 0x5f, 0x94, 0xda, 0x6b, 0x24, 0xad, 0x35, 0xa0, - 0xa5, 0xa1, 0x87, 0x47, 0xf9, 0xc4, 0xef, 0xa7, 0xfb, 0x8b, 0x9c, 0xd2, 0x4c, 0xc1, 0x2b, 0x00, - 0xa8, 0xd8, 0x34, 0x0d, 0x42, 0x5c, 0xc2, 0xa4, 0x47, 0x38, 0xdf, 0x89, 0xf0, 0x46, 0x88, 0x54, - 0x20, 0x45, 0xa4, 0x99, 0xb4, 0x89, 0x85, 0xff, 0x06, 0x4c, 0x98, 0x86, 0x55, 0x26, 0xa8, 0xba, - 0x5d, 0xd6, 0x50, 0x15, 0xe9, 0xd0, 0xdb, 0x6d, 0x6a, 0x96, 0x5b, 0x18, 0x2a, 0x2d, 0xb9, 0x6b, - 0x9e, 0x1e, 0xe5, 0xa7, 0xfc, 0x18, 0x44, 0xdb, 0x95, 0x0c, 0x2c, 0x9b, 0x90, 0xee, 0x48, 0xb7, - 0x2d, 0x7a, 0x78, 0x50, 0x00, 0x2c, 0xf8, 0x6d, 0x8b, 0xfa, 0xd4, 0xe3, 0xa6, 0x61, 0x6d, 0xa2, - 0xea, 0xf6, 0x5a, 0x48, 0xc5, 0x7f, 0x00, 0xc6, 0x19, 0x31, 0x76, 0xca, 0x50, 0xd3, 0x1c, 0x44, - 0x48, 0x76, 0xc0, 0xe3, 0x17, 0x0e, 0x0f, 0x0a, 0x93, 0x8c, 0x62, 0xd5, 0x9f, 0xd9, 0xa4, 0x8e, - 0x61, 0xe9, 0x59, 0x4e, 0x19, 0x0b, 0x17, 0xb1, 0x19, 0xfe, 0x53, 0x30, 0xbe, 0x17, 0xa8, 0x1b, - 0x12, 0x0d, 0x7a, 0x44, 0x6f, 0x1c, 0x1e, 0x14, 0x5e, 0x67, 0x44, 0xa1, 0x03, 0x11, 0x46, 0x65, - 0x6c, 0x2f, 0x36, 0xce, 0xdf, 0x02, 0x69, 0xbb, 0x56, 0xd9, 0x45, 0xf5, 0x6c, 0xda, 0x93, 0x72, - 0x52, 0xf2, 0x8b, 0x51, 0x0a, 0x8a, 0x51, 0x5a, 0xb5, 0xea, 0xa5, 0xec, 0xa3, 0xc6, 0x1e, 0x55, - 0xa7, 0x6e, 0x53, 0x2c, 0x6d, 0xd4, 0x2a, 0x1f, 0xa3, 0xba, 0xc2, 0x56, 0xf3, 0x45, 0x30, 0xb8, - 0x07, 0xab, 0x35, 0x94, 0x7d, 0xc5, 0xa3, 0x99, 0x0e, 0x1c, 0x71, 0x2b, 0xb0, 0xc9, 0x0e, 0x23, - 0x62, 0xac, 0xbf, 0xa4, 0x78, 0xfd, 0x87, 0x07, 0xf9, 0xc4, 0x3f, 0x0f, 0xf2, 0x89, 0xef, 0x4f, - 0xf7, 0x17, 0x5b, 0xd3, 0xfb, 0xe9, 0x74, 0x7f, 0x91, 0xe5, 0x55, 0x20, 0xda, 0xae, 0xdc, 0x5a, - 0x66, 0xe2, 0x0c, 0x10, 0x5a, 0x47, 0x15, 0x44, 0x6c, 0x6c, 0x11, 0x24, 0xfe, 0x96, 0x02, 0x63, - 0xeb, 0x44, 0xbf, 0xa9, 0x19, 0xf4, 0x45, 0x56, 0x66, 0x5b, 0x6b, 0x92, 0x67, 0xb7, 0xe6, 0x0e, - 0xc8, 0x34, 0x6a, 0xb4, 0xec, 0x40, 0x8a, 0x58, 0x45, 0x16, 0x9e, 0x1e, 0xe5, 0x2f, 0xb4, 0x56, - 0xe3, 0x27, 0x48, 0x87, 0x6a, 0x7d, 0x0d, 0xa9, 0x4d, 0x35, 0xb9, 0x86, 0x54, 0x65, 0x54, 0x8d, - 0x74, 0x01, 0xff, 0x45, 0xfb, 0x6a, 0xf7, 0xab, 0x71, 0xbe, 0xcf, 0x4a, 0x6f, 0x53, 0xe4, 0xc5, - 0xf7, 0x7b, 0xfb, 0x78, 0x21, 0xea, 0x63, 0xc4, 0x12, 0x51, 0x00, 0xd9, 0xf8, 0x58, 0xe8, 0xe1, - 0xcf, 0x49, 0x30, 0xbc, 0x4e, 0x74, 0x16, 0x0d, 0xf1, 0x37, 0xdb, 0x35, 0x14, 0xe7, 0xa5, 0x90, - 0xed, 0xd4, 0x50, 0xfd, 0xb6, 0xd3, 0x7f, 0xf0, 0xec, 0x1a, 0x48, 0x43, 0x13, 0xd7, 0x2c, 0xea, - 0x59, 0xd5, 0x6f, 0x1f, 0xb0, 0x35, 0xc5, 0xf7, 0x22, 0x02, 0xb6, 0xe4, 0xe7, 0x0a, 0x78, 0x2e, - 0x2a, 0x60, 0xa0, 0x87, 0x38, 0x05, 0x26, 0x9a, 0xfe, 0x0c, 0x65, 0xfb, 0x31, 0xe5, 0x1d, 0xcb, - 0x25, 0xa4, 0x1b, 0x96, 0x82, 0xb4, 0xff, 0x59, 0xbd, 0x2d, 0x30, 0xd5, 0x50, 0x8f, 0x38, 0xea, - 0xf3, 0x2b, 0x38, 0x11, 0xae, 0xdf, 0x74, 0xd4, 0xb6, 0xb4, 0x1a, 0xa1, 0x21, 0x6d, 0xea, 0xf9, - 0x69, 0xd7, 0x08, 0x6d, 0xf5, 0x66, 0xe0, 0x0c, 0xde, 0x5c, 0xef, 0xed, 0x4d, 0xec, 0x90, 0x8a, - 0x89, 0x2e, 0xda, 0xde, 0x21, 0x15, 0x1b, 0x0d, 0x9c, 0xe2, 0x15, 0xaf, 0xdb, 0xed, 0x2a, 0x72, - 0x5b, 0xa9, 0xec, 0xbe, 0x00, 0xd8, 0x99, 0x24, 0xb4, 0x9c, 0xc8, 0x9f, 0x07, 0xcf, 0x83, 0xd2, - 0x88, 0xbb, 0xcf, 0xfb, 0x7f, 0xe5, 0x39, 0x7f, 0xaf, 0xa3, 0x0d, 0x06, 0x17, 0x23, 0xfe, 0x92, - 0x04, 0x23, 0xeb, 0x44, 0xdf, 0xb2, 0xb4, 0x97, 0xba, 0x6d, 0xae, 0xf6, 0xb6, 0x26, 0x1b, 0xb5, - 0xa6, 0xa1, 0x88, 0xf8, 0x07, 0x07, 0xa6, 0x22, 0x23, 0x2f, 0xd2, 0x11, 0xfe, 0xb3, 0x30, 0xd1, - 0x64, 0xaf, 0x44, 0x67, 0xbc, 0x77, 0xc7, 0x41, 0x21, 0xd3, 0xd8, 0xfa, 0xec, 0x92, 0xf4, 0xee, - 0x52, 0x24, 0x77, 0xf1, 0x59, 0x12, 0xcc, 0xb8, 0x57, 0x1f, 0xb4, 0x54, 0x54, 0xdd, 0xb2, 0x2a, - 0xd8, 0xd2, 0x0c, 0x4b, 0x6f, 0x7a, 0x79, 0xbc, 0x8c, 0x8e, 0xf3, 0xf3, 0x20, 0xa3, 0xba, 0x97, - 0xbd, 0x6b, 0xcc, 0x0e, 0x32, 0xf4, 0x1d, 0xbf, 0xa7, 0x53, 0xca, 0x68, 0x30, 0xfc, 0xa1, 0x37, - 0x5a, 0xfc, 0x3a, 0x28, 0x8d, 0xc3, 0xb8, 0x90, 0x97, 0xaf, 0x74, 0xae, 0x96, 0xf9, 0xd8, 0x6b, - 0xa3, 0x93, 0xb8, 0xe2, 0x55, 0x30, 0xd7, 0x6d, 0x3e, 0x28, 0xa5, 0xe2, 0x44, 0x9b, 0xf0, 0xe2, - 0x13, 0x0e, 0x64, 0xdc, 0xca, 0xb3, 0x35, 0x48, 0xd1, 0x06, 0x74, 0xa0, 0x49, 0xf8, 0x2b, 0x60, - 0x08, 0xd6, 0xe8, 0x0e, 0x76, 0x0c, 0x5a, 0xef, 0xe9, 0x52, 0x03, 0xca, 0xaf, 0x82, 0xb4, 0xed, - 0x31, 0xb0, 0xba, 0xca, 0x75, 0x7a, 0xc8, 0xf8, 0x71, 0x22, 0x9a, 0xfa, 0x0b, 0x8b, 0x1f, 0xb5, - 0xee, 0x71, 0xc5, 0x95, 0xa8, 0x11, 0xc5, 0x95, 0x66, 0xae, 0x49, 0x9a, 0x7b, 0xe1, 0xf7, 0x87, - 0x58, 0x1a, 0xa2, 0x04, 0xce, 0xc7, 0x86, 0xba, 0x49, 0xb1, 0xb2, 0xfc, 0x67, 0x1a, 0xa4, 0xd6, - 0x89, 0xce, 0xdf, 0x05, 0x99, 0xf8, 0x37, 0x88, 0xc5, 0x4e, 0x99, 0xb4, 0x3e, 0xf8, 0x84, 0xe5, - 0xfe, 0xb1, 0x61, 0x97, 0xef, 0x82, 0x91, 0xe8, 0xc3, 0x70, 0xa1, 0x0b, 0x49, 0x04, 0x29, 0x2c, - 0xf5, 0x8b, 0x0c, 0x83, 0x7d, 0x05, 0x5e, 0x0d, 0x5f, 0x30, 0x17, 0xbb, 0xac, 0x0e, 0x40, 0xc2, - 0x5b, 0x7d, 0x80, 0x42, 0xf6, 0xbb, 0x20, 0x13, 0xbf, 0xe8, 0xbb, 0xa9, 0x17, 0xc3, 0x76, 0x55, - 0xaf, 0xd3, 0xad, 0x55, 0x01, 0xa0, 0xe9, 0x76, 0x79, 0xb3, 0x0b, 0x43, 0x03, 0x26, 0x14, 0xfa, - 0x82, 0x85, 0x31, 0x7e, 0xe5, 0xc0, 0x74, 0xe7, 0xf3, 0xed, 0x72, 0x37, 0xcf, 0x3b, 0xad, 0x12, - 0xae, 0x9d, 0x65, 0x55, 0xf8, 0xaa, 0x9a, 0x78, 0xd4, 0xda, 0xce, 0xfc, 0xb7, 0xe0, 0xb5, 0x48, - 0x2b, 0xcf, 0x77, 0xcb, 0xb2, 0x09, 0x28, 0xc8, 0x7d, 0x02, 0xbb, 0x85, 0x5f, 0x11, 0x06, 0xbf, - 0x73, 0xbb, 0xb9, 0x74, 0xeb, 0xe1, 0x71, 0x8e, 0x7b, 0x7c, 0x9c, 0xe3, 0xfe, 0x3e, 0xce, 0x71, - 0xf7, 0x4f, 0x72, 0x89, 0xc7, 0x27, 0xb9, 0xc4, 0x93, 0x93, 0x5c, 0xe2, 0xcb, 0xb7, 0x75, 0x83, - 0xee, 0xd4, 0x2a, 0x92, 0x8a, 0x4d, 0xf6, 0xcf, 0x02, 0xb9, 0x6d, 0x2f, 0xd3, 0xba, 0x8d, 0x48, - 0x25, 0xed, 0xdd, 0x6d, 0xef, 0xfc, 0x1b, 0x00, 0x00, 0xff, 0xff, 0xf8, 0x25, 0x35, 0x94, 0xf0, - 0x10, 0x00, 0x00, + // 1264 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x58, 0x41, 0x4f, 0x1b, 0x47, + 0x14, 0xf6, 0xda, 0x09, 0x29, 0x43, 0xc1, 0xb0, 0x40, 0x62, 0x16, 0x62, 0xd3, 0x0d, 0x15, 0x88, + 0xd6, 0x6b, 0x42, 0xd2, 0xa0, 0x3a, 0x51, 0x15, 0x0c, 0x49, 0x9b, 0xb6, 0x6e, 0xd1, 0x52, 0x52, + 0xa9, 0x6a, 0xe5, 0x8e, 0x77, 0x87, 0x65, 0x85, 0x77, 0xc7, 0xd9, 0x19, 0x93, 0xf8, 0x50, 0xa9, + 0xea, 0xa9, 0xed, 0x29, 0x7f, 0xa0, 0x52, 0x2a, 0xb5, 0x52, 0x8f, 0x1c, 0x38, 0xb6, 0xf7, 0x28, + 0xa7, 0x88, 0x53, 0x94, 0x03, 0xad, 0xe0, 0x40, 0xff, 0x41, 0x0f, 0xb9, 0x54, 0xbb, 0x3b, 0xbb, + 0xf6, 0xae, 0xed, 0xb5, 0xa1, 0xcd, 0x25, 0x17, 0x30, 0x33, 0xdf, 0x7c, 0x33, 0xef, 0xfb, 0xde, + 0xbc, 0x79, 0x06, 0x64, 0x14, 0x4c, 0x0c, 0x4c, 0x72, 0x84, 0xc2, 0x6d, 0xdd, 0xd4, 0x72, 0x3b, + 0x97, 0xcb, 0x88, 0xc2, 0xcb, 0x39, 0xfa, 0x40, 0xaa, 0x5a, 0x98, 0x62, 0xfe, 0xbc, 0x0b, 0x90, + 0x18, 0x40, 0x62, 0x00, 0x61, 0x42, 0xc3, 0x58, 0xab, 0xa0, 0x9c, 0x83, 0x2a, 0xd7, 0x36, 0x73, + 0xd0, 0xac, 0xbb, 0x4b, 0x84, 0x4c, 0x78, 0x8a, 0xea, 0x06, 0x22, 0x14, 0x1a, 0x55, 0x06, 0x18, + 0xd3, 0xb0, 0x86, 0x9d, 0x8f, 0x39, 0xfb, 0x13, 0x1b, 0x9d, 0x70, 0x77, 0x2a, 0xb9, 0x13, 0x6c, + 0x5b, 0x77, 0x2a, 0xcd, 0x4e, 0x59, 0x86, 0x04, 0xf9, 0x47, 0x54, 0xb0, 0x6e, 0xb2, 0xf9, 0x99, + 0x0e, 0x51, 0x78, 0x87, 0x76, 0x51, 0x17, 0x18, 0xca, 0x20, 0x36, 0xc2, 0xfe, 0xc5, 0x26, 0x46, + 0xa0, 0xa1, 0x9b, 0x38, 0xe7, 0xfc, 0x74, 0x87, 0xc4, 0x17, 0x67, 0x00, 0x5f, 0x24, 0xda, 0x8a, + 0x85, 0x20, 0x45, 0x77, 0x61, 0x45, 0x57, 0x21, 0xc5, 0x16, 0xbf, 0x06, 0x06, 0x54, 0x44, 0x14, + 0x4b, 0xaf, 0x52, 0x1d, 0x9b, 0x29, 0x6e, 0x9a, 0x9b, 0x1b, 0x58, 0xbc, 0x24, 0xb5, 0xd7, 0x48, + 0x5a, 0x6d, 0x40, 0x0b, 0xfd, 0x8f, 0x0f, 0x32, 0xb1, 0xdf, 0x8e, 0x77, 0xe7, 0x39, 0xb9, 0x99, + 0x82, 0x97, 0x01, 0x50, 0xb0, 0x61, 0xe8, 0x84, 0xd8, 0x84, 0x71, 0x87, 0x70, 0xb6, 0x13, 0xe1, + 0x8a, 0x8f, 0x94, 0x21, 0x45, 0xa4, 0x99, 0xb4, 0x89, 0x85, 0xff, 0x1a, 0x8c, 0x1a, 0xba, 0x59, + 0x22, 0xa8, 0xb2, 0x59, 0x52, 0x51, 0x05, 0x69, 0xd0, 0x39, 0x6d, 0x62, 0x9a, 0x9b, 0xeb, 0x2f, + 0x2c, 0xd8, 0x6b, 0x9e, 0x1f, 0x64, 0xc6, 0xdd, 0x3d, 0x88, 0xba, 0x2d, 0xe9, 0x38, 0x67, 0x40, + 0xba, 0x25, 0xdd, 0x31, 0xe9, 0xfe, 0x5e, 0x16, 0xb0, 0xcd, 0xef, 0x98, 0xd4, 0xa5, 0x1e, 0x31, + 0x74, 0x73, 0x1d, 0x55, 0x36, 0x57, 0x7d, 0x2a, 0xfe, 0x7d, 0x30, 0xc2, 0x88, 0xb1, 0x55, 0x82, + 0xaa, 0x6a, 0x21, 0x42, 0x52, 0x67, 0x1c, 0x7e, 0x61, 0x7f, 0x2f, 0x3b, 0xc6, 0x28, 0x96, 0xdd, + 0x99, 0x75, 0x6a, 0xe9, 0xa6, 0x96, 0xe2, 0xe4, 0x61, 0x7f, 0x11, 0x9b, 0xe1, 0x3f, 0x01, 0x23, + 0x3b, 0x9e, 0xba, 0x3e, 0xd1, 0x59, 0x87, 0xe8, 0x8d, 0xfd, 0xbd, 0xec, 0x45, 0x46, 0xe4, 0x3b, + 0x10, 0x60, 0x94, 0x87, 0x77, 0x42, 0xe3, 0xfc, 0x6d, 0xd0, 0x57, 0xad, 0x95, 0xb7, 0x51, 0x3d, + 0xd5, 0xe7, 0x48, 0x39, 0x26, 0xb9, 0xc9, 0x28, 0x79, 0xc9, 0x28, 0x2d, 0x9b, 0xf5, 0x42, 0xea, + 0x49, 0xe3, 0x8c, 0x8a, 0x55, 0xaf, 0x52, 0x2c, 0xad, 0xd5, 0xca, 0x1f, 0xa1, 0xba, 0xcc, 0x56, + 0xf3, 0x79, 0x70, 0x76, 0x07, 0x56, 0x6a, 0x28, 0x75, 0xce, 0xa1, 0x99, 0xf0, 0x1c, 0xb1, 0x33, + 0xb0, 0xc9, 0x0e, 0x3d, 0x60, 0xac, 0xbb, 0x24, 0x7f, 0xf3, 0xfb, 0x47, 0x99, 0xd8, 0xdf, 0x8f, + 0x32, 0xb1, 0xef, 0x8e, 0x77, 0xe7, 0x5b, 0xc3, 0xfb, 0xf1, 0x78, 0x77, 0x9e, 0xc5, 0x95, 0x25, + 0xea, 0x76, 0xae, 0x35, 0xcd, 0xc4, 0x29, 0x20, 0xb4, 0x8e, 0xca, 0x88, 0x54, 0xb1, 0x49, 0x90, + 0xf8, 0x6b, 0x02, 0x0c, 0x17, 0x89, 0x76, 0x4b, 0xd5, 0xe9, 0xcb, 0xcc, 0xcc, 0xb6, 0xd6, 0xc4, + 0x4f, 0x6f, 0xcd, 0x5d, 0x90, 0x6c, 0xe4, 0x68, 0xc9, 0x82, 0x14, 0xb1, 0x8c, 0xcc, 0x3e, 0x3f, + 0xc8, 0x4c, 0xb6, 0x66, 0xe3, 0xc7, 0x48, 0x83, 0x4a, 0x7d, 0x15, 0x29, 0x4d, 0x39, 0xb9, 0x8a, + 0x14, 0x79, 0x48, 0x09, 0xdc, 0x02, 0xfe, 0xf3, 0xf6, 0xd9, 0xee, 0x66, 0xe3, 0x6c, 0x8f, 0x99, + 0xde, 0x26, 0xc9, 0xf3, 0xef, 0x75, 0xf7, 0x71, 0x32, 0xe8, 0x63, 0xc0, 0x12, 0x51, 0x00, 0xa9, + 0xf0, 0x98, 0xef, 0xe1, 0x4f, 0x71, 0x30, 0x50, 0x24, 0x1a, 0xdb, 0x0d, 0xf1, 0xb7, 0xda, 0x5d, + 0x28, 0xce, 0x09, 0x21, 0xd5, 0xe9, 0x42, 0xf5, 0x7a, 0x9d, 0xfe, 0x83, 0x67, 0x37, 0x40, 0x1f, + 0x34, 0x70, 0xcd, 0xa4, 0x8e, 0x55, 0xbd, 0xde, 0x03, 0xb6, 0x26, 0xff, 0x6e, 0x40, 0xc0, 0x96, + 0xf8, 0x6c, 0x01, 0xcf, 0x07, 0x05, 0xf4, 0xf4, 0x10, 0xc7, 0xc1, 0x68, 0xd3, 0x9f, 0xbe, 0x6c, + 0x3f, 0x24, 0x9c, 0xb2, 0x5c, 0x40, 0x9a, 0x6e, 0xca, 0x48, 0xfd, 0x9f, 0xd5, 0xdb, 0x00, 0xe3, + 0x0d, 0xf5, 0x88, 0xa5, 0x9c, 0x5c, 0xc1, 0x51, 0x7f, 0xfd, 0xba, 0xa5, 0xb4, 0xa5, 0x55, 0x09, + 0xf5, 0x69, 0x13, 0x27, 0xa7, 0x5d, 0x25, 0xb4, 0xd5, 0x9b, 0x33, 0xa7, 0xf0, 0xe6, 0x66, 0x77, + 0x6f, 0x42, 0x45, 0x2a, 0x24, 0xba, 0x58, 0x75, 0x8a, 0x54, 0x68, 0xd4, 0x73, 0x8a, 0x97, 0x9d, + 0xdb, 0x5e, 0xad, 0x20, 0xfb, 0x2a, 0x95, 0xec, 0x0e, 0x80, 0xd5, 0x24, 0xa1, 0xa5, 0x22, 0x7f, + 0xe6, 0xb5, 0x07, 0x85, 0x41, 0xfb, 0x9c, 0x0f, 0xff, 0xcc, 0x70, 0xee, 0x59, 0x87, 0x1a, 0x0c, + 0x36, 0x46, 0xfc, 0x39, 0x0e, 0x06, 0x8b, 0x44, 0xdb, 0x30, 0xd5, 0x57, 0xfa, 0xda, 0x5c, 0xef, + 0x6e, 0x4d, 0x2a, 0x68, 0x4d, 0x43, 0x11, 0xf1, 0x77, 0x0e, 0x8c, 0x07, 0x46, 0x5e, 0xa6, 0x23, + 0xfc, 0xa7, 0x7e, 0xa0, 0xf1, 0x6e, 0x81, 0x4e, 0x39, 0x7d, 0xc7, 0x5e, 0x36, 0xd9, 0x38, 0xfa, + 0xf4, 0x82, 0xf4, 0xce, 0x42, 0x20, 0x76, 0xf1, 0x45, 0x1c, 0x4c, 0xd9, 0x4f, 0x1f, 0x34, 0x15, + 0x54, 0xd9, 0x30, 0xcb, 0xd8, 0x54, 0x75, 0x53, 0x6b, 0xea, 0x3c, 0x5e, 0x45, 0xc7, 0xf9, 0x59, + 0x90, 0x54, 0xec, 0xc7, 0xde, 0x36, 0x66, 0x0b, 0xe9, 0xda, 0x96, 0x7b, 0xa7, 0x13, 0xf2, 0x90, + 0x37, 0xfc, 0x81, 0x33, 0x9a, 0xff, 0xca, 0x4b, 0x8d, 0xfd, 0xb0, 0x90, 0x57, 0xaf, 0x75, 0xce, + 0x96, 0xd9, 0x50, 0xb7, 0xd1, 0x49, 0x5c, 0xf1, 0x3a, 0x98, 0x89, 0x9a, 0xf7, 0x52, 0x29, 0x3f, + 0xda, 0x66, 0x7b, 0xf1, 0x19, 0x07, 0x92, 0x76, 0xe6, 0x55, 0x55, 0x48, 0xd1, 0x1a, 0xb4, 0xa0, + 0x41, 0xf8, 0x6b, 0xa0, 0x1f, 0xd6, 0xe8, 0x16, 0xb6, 0x74, 0x5a, 0xef, 0xea, 0x52, 0x03, 0xca, + 0x2f, 0x83, 0xbe, 0xaa, 0xc3, 0xc0, 0xf2, 0x2a, 0xdd, 0xa9, 0x91, 0x71, 0xf7, 0x09, 0x68, 0xea, + 0x2e, 0xcc, 0x7f, 0xd8, 0x7a, 0xc6, 0x25, 0x5b, 0xa2, 0xc6, 0x2e, 0xb6, 0x34, 0x33, 0x4d, 0xd2, + 0x3c, 0xf0, 0xbf, 0x3f, 0x84, 0xc2, 0x10, 0x25, 0x70, 0x21, 0x34, 0x14, 0x25, 0xc5, 0x92, 0xf8, + 0x0f, 0xe7, 0x3c, 0x5f, 0x32, 0xa6, 0x90, 0xa2, 0x15, 0x6c, 0x12, 0xb7, 0xbb, 0x6c, 0x9f, 0x75, + 0xdc, 0xe9, 0xb3, 0xae, 0x08, 0x80, 0x89, 0xee, 0x97, 0x58, 0xc7, 0x1b, 0x3f, 0x55, 0xc7, 0xdb, + 0x6f, 0xa2, 0xfb, 0x6b, 0x0e, 0x41, 0x7e, 0xb9, 0x7b, 0xc3, 0x93, 0x0e, 0xa6, 0x52, 0x38, 0x42, + 0xf1, 0x22, 0x98, 0x6c, 0x33, 0xec, 0xa9, 0xb5, 0xf8, 0xc7, 0x39, 0x90, 0x28, 0x12, 0x8d, 0xbf, + 0x07, 0x92, 0xe1, 0xaf, 0x56, 0xf3, 0x9d, 0x2c, 0x6e, 0xed, 0x84, 0x85, 0xc5, 0xde, 0xb1, 0x7e, + 0xf9, 0xdb, 0x06, 0x83, 0xc1, 0x8e, 0x79, 0x2e, 0x82, 0x24, 0x80, 0x14, 0x16, 0x7a, 0x45, 0xfa, + 0x9b, 0x7d, 0x09, 0x5e, 0xf3, 0x5b, 0xbb, 0x4b, 0x11, 0xab, 0x3d, 0x90, 0xf0, 0x56, 0x0f, 0x20, + 0x9f, 0xfd, 0x1e, 0x48, 0x86, 0x3b, 0xa0, 0x28, 0xf5, 0x42, 0xd8, 0x48, 0xf5, 0x3a, 0x3d, 0xe7, + 0x65, 0x00, 0x9a, 0x9e, 0xdd, 0x37, 0x23, 0x18, 0x1a, 0x30, 0x21, 0xdb, 0x13, 0xcc, 0xdf, 0xe3, + 0x17, 0x0e, 0x4c, 0x74, 0x2e, 0xfc, 0x57, 0xa3, 0x3c, 0xef, 0xb4, 0x4a, 0xb8, 0x71, 0x9a, 0x55, + 0x7e, 0xbb, 0x39, 0xfa, 0xa4, 0xb5, 0xce, 0xf1, 0xdf, 0x80, 0xd7, 0x03, 0x35, 0x6e, 0x36, 0x2a, + 0xca, 0x26, 0xa0, 0x90, 0xeb, 0x11, 0x18, 0xb5, 0xfd, 0x12, 0x4f, 0xc1, 0x70, 0x4b, 0x5d, 0x89, + 0xca, 0x9e, 0x30, 0x58, 0xb8, 0x72, 0x02, 0xb0, 0x77, 0x14, 0xe1, 0xec, 0xb7, 0x76, 0x71, 0x2d, + 0xdc, 0x7e, 0x7c, 0x98, 0xe6, 0x9e, 0x1e, 0xa6, 0xb9, 0xbf, 0x0e, 0xd3, 0xdc, 0xc3, 0xa3, 0x74, + 0xec, 0xe9, 0x51, 0x3a, 0xf6, 0xec, 0x28, 0x1d, 0xfb, 0xe2, 0x6d, 0x4d, 0xa7, 0x5b, 0xb5, 0xb2, + 0xa4, 0x60, 0x83, 0xfd, 0xef, 0x26, 0xd7, 0xb6, 0xb4, 0xd2, 0x7a, 0x15, 0x91, 0x72, 0x9f, 0x53, + 0x9c, 0xae, 0xfc, 0x1b, 0x00, 0x00, 0xff, 0xff, 0x9a, 0xd3, 0x7d, 0x0f, 0x7f, 0x12, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -768,6 +851,9 @@ type MsgClient interface { // UpdateParams defines an operation for updating the x/staking module // parameters. UpdateParams(ctx context.Context, in *MsgUpdateParams, opts ...grpc.CallOption) (*MsgUpdateParamsResponse, error) + // RotateConsPubKey defines an operation for rotating the consensus keys + // of a validator. + RotateConsPubKey(ctx context.Context, in *MsgRotateConsPubKey, opts ...grpc.CallOption) (*MsgRotateConsPubKeyResponse, error) } type msgClient struct { @@ -841,6 +927,15 @@ func (c *msgClient) UpdateParams(ctx context.Context, in *MsgUpdateParams, opts return out, nil } +func (c *msgClient) RotateConsPubKey(ctx context.Context, in *MsgRotateConsPubKey, opts ...grpc.CallOption) (*MsgRotateConsPubKeyResponse, error) { + out := new(MsgRotateConsPubKeyResponse) + err := c.cc.Invoke(ctx, "/cosmos.staking.v1beta1.Msg/RotateConsPubKey", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // MsgServer is the server API for Msg service. type MsgServer interface { // CreateValidator defines a method for creating a new validator. @@ -862,6 +957,9 @@ type MsgServer interface { // UpdateParams defines an operation for updating the x/staking module // parameters. UpdateParams(context.Context, *MsgUpdateParams) (*MsgUpdateParamsResponse, error) + // RotateConsPubKey defines an operation for rotating the consensus keys + // of a validator. + RotateConsPubKey(context.Context, *MsgRotateConsPubKey) (*MsgRotateConsPubKeyResponse, error) } // UnimplementedMsgServer can be embedded to have forward compatible implementations. @@ -889,6 +987,9 @@ func (*UnimplementedMsgServer) CancelUnbondingDelegation(ctx context.Context, re func (*UnimplementedMsgServer) UpdateParams(ctx context.Context, req *MsgUpdateParams) (*MsgUpdateParamsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method UpdateParams not implemented") } +func (*UnimplementedMsgServer) RotateConsPubKey(ctx context.Context, req *MsgRotateConsPubKey) (*MsgRotateConsPubKeyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RotateConsPubKey not implemented") +} func RegisterMsgServer(s grpc1.Server, srv MsgServer) { s.RegisterService(&_Msg_serviceDesc, srv) @@ -1020,6 +1121,24 @@ func _Msg_UpdateParams_Handler(srv interface{}, ctx context.Context, dec func(in return interceptor(ctx, in, info, handler) } +func _Msg_RotateConsPubKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgRotateConsPubKey) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MsgServer).RotateConsPubKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/cosmos.staking.v1beta1.Msg/RotateConsPubKey", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MsgServer).RotateConsPubKey(ctx, req.(*MsgRotateConsPubKey)) + } + return interceptor(ctx, in, info, handler) +} + var Msg_serviceDesc = _Msg_serviceDesc var _Msg_serviceDesc = grpc.ServiceDesc{ ServiceName: "cosmos.staking.v1beta1.Msg", @@ -1053,6 +1172,10 @@ var _Msg_serviceDesc = grpc.ServiceDesc{ MethodName: "UpdateParams", Handler: _Msg_UpdateParams_Handler, }, + { + MethodName: "RotateConsPubKey", + Handler: _Msg_RotateConsPubKey_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "cosmos/staking/v1beta1/tx.proto", @@ -1638,6 +1761,71 @@ func (m *MsgUpdateParamsResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) return len(dAtA) - i, nil } +func (m *MsgRotateConsPubKey) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgRotateConsPubKey) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgRotateConsPubKey) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.NewPubkey != nil { + { + size, err := m.NewPubkey.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTx(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + if len(m.ValidatorAddress) > 0 { + i -= len(m.ValidatorAddress) + copy(dAtA[i:], m.ValidatorAddress) + i = encodeVarintTx(dAtA, i, uint64(len(m.ValidatorAddress))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *MsgRotateConsPubKeyResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgRotateConsPubKeyResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgRotateConsPubKeyResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + func encodeVarintTx(dAtA []byte, offset int, v uint64) int { offset -= sovTx(v) base := offset @@ -1868,6 +2056,32 @@ func (m *MsgUpdateParamsResponse) Size() (n int) { return n } +func (m *MsgRotateConsPubKey) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.ValidatorAddress) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + if m.NewPubkey != nil { + l = m.NewPubkey.Size() + n += 1 + l + sovTx(uint64(l)) + } + return n +} + +func (m *MsgRotateConsPubKeyResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + func sovTx(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -3547,6 +3761,174 @@ func (m *MsgUpdateParamsResponse) Unmarshal(dAtA []byte) error { } return nil } +func (m *MsgRotateConsPubKey) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgRotateConsPubKey: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgRotateConsPubKey: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ValidatorAddress", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ValidatorAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field NewPubkey", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.NewPubkey == nil { + m.NewPubkey = &any.Any{} + } + if err := m.NewPubkey.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MsgRotateConsPubKeyResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgRotateConsPubKeyResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgRotateConsPubKeyResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipTx(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0