From 9a48941288b976a7c39673dfe8ca430094d6f91e Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Wed, 20 May 2026 15:09:24 -0400 Subject: [PATCH 01/19] add protos & generate --- proto/cosmos/staking/v1beta1/tx.proto | 22 +- x/staking/types/tx.pb.go | 534 ++++++++++++++++++++++---- 2 files changed, 479 insertions(+), 77 deletions(-) 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/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 From cb87501b1d37162cdb94205b91bcb9f05c6f7d2b Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Wed, 20 May 2026 18:37:22 -0400 Subject: [PATCH 02/19] implement x/staking msg server handler for MsgRotateConsPubKey --- x/staking/keeper/msg_server.go | 64 +++++++++ x/staking/keeper/msg_server_test.go | 139 +++++++++++++++++++ x/staking/keeper/rotation.go | 50 +++++++ x/staking/testutil/expected_keepers_mocks.go | 14 ++ x/staking/types/errors.go | 4 + x/staking/types/expected_keepers.go | 1 + x/staking/types/keys.go | 59 ++++++++ x/staking/types/keys_test.go | 128 +++++++++++++++++ x/staking/types/params.go | 11 +- 9 files changed, 468 insertions(+), 2 deletions(-) create mode 100644 x/staking/keeper/rotation.go diff --git a/x/staking/keeper/msg_server.go b/x/staking/keeper/msg_server.go index ee0915990e29..deee6ce54baf 100644 --- a/x/staking/keeper/msg_server.go +++ b/x/staking/keeper/msg_server.go @@ -613,3 +613,67 @@ func (k msgServer) UpdateParams(ctx context.Context, msg *types.MsgUpdateParams) return &types.MsgUpdateParamsResponse{}, nil } + +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 reuse of a key that some validator rotated away from inside the + // unbonding window + rotated, err := k.HasRotatedConsAddr(ctx, newConsAddr) + if err != nil { + return nil, err + } + if rotated { + return nil, types.ErrConsensusPubKeyInRotationHistory + } + + // reject a key currently in use by some validator + if existing, err := k.GetValidatorByConsAddr(ctx, newConsAddr); 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 status := validator.GetStatus(); status != types.Bonded { + return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "validator status is not bonded, got %s", status) + } + + // 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 per validator rotation limit inside the unbonding window + pending, err := k.HasPendingConsKeyRotation(ctx, valAddr) + if err != nil { + return nil, err + } + if pending { + return nil, types.ErrExceedingMaxConsPubKeyRotations + } + + // transfer the rotation fee from the validators account to the + // distribution module, which forwards it to the community pool + fee := types.DefaultKeyRotationFee + if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sdk.AccAddress(valAddr), types.DistributionModuleName, sdk.NewCoins(fee)); err != nil { + return nil, err + } + + // record the key rotation in the store + if err := k.SetConsKeyRotation(ctx, valAddr, oldPk, fee); 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..4fd22f80549b 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,144 @@ 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 not bonded", + newRotateConsPubKeyMsg: func() *stakingtypes.MsgRotateConsPubKey { + valAddr, _ := createValidator(stakingtypes.Unbonded) + return &stakingtypes.MsgRotateConsPubKey{ + ValidatorAddress: valAddr.String(), + NewPubkey: newAny(ed25519.GenPrivKey().PubKey()), + } + }, + expErr: "validator status is not bonded", + }, + { + 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, stakingtypes.DefaultKeyRotationFee)) + return &stakingtypes.MsgRotateConsPubKey{ + ValidatorAddress: valAddr.String(), + NewPubkey: newAny(oldPubKey), + } + }, + 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.DistributionModuleName, 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.DistributionModuleName, 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/rotation.go b/x/staking/keeper/rotation.go new file mode 100644 index 000000000000..60c67fc85d0b --- /dev/null +++ b/x/staking/keeper/rotation.go @@ -0,0 +1,50 @@ +package keeper + +import ( + "context" + + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// MaxConsKeyRotations is the maximum number of pending consensus key rotations +// a validator may have inside the unbonding window. +const MaxConsKeyRotations = 1 + +// HasPendingConsKeyRotation returns whether the validator has a pending +// consensus key rotation inside the unbonding window. +func (k Keeper) HasPendingConsKeyRotation(ctx context.Context, valAddr sdk.ValAddress) (bool, error) { + return k.storeService.OpenKVStore(ctx).Has(types.GetValidatorConsKeyRotationKey(valAddr)) +} + +// HasRotatedConsAddr returns whether the given consensus address was previously +// rotated away from and is still inside its unbonding window. +func (k Keeper) HasRotatedConsAddr(ctx context.Context, consAddr sdk.ConsAddress) (bool, error) { + return k.storeService.OpenKVStore(ctx).Has(types.GetRotatedConsAddrIndexKey(consAddr)) +} + +// SetConsKeyRotation writes to indexes that track a pending consensus key +// rotation. +func (k Keeper) SetConsKeyRotation(ctx context.Context, valAddr sdk.ValAddress, oldPubKey cryptotypes.PubKey, fee sdk.Coin) 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()) + + store := k.storeService.OpenKVStore(ctx) + 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 + } + + return store.Set(types.GetRotatedConsAddrIndexKey(oldConsAddr), valAddr) +} 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..5bef4a8f0a4e 100644 --- a/x/staking/types/keys.go +++ b/x/staking/types/keys.go @@ -23,6 +23,10 @@ const ( // RouterKey is the msg router key for the staking module RouterKey = ModuleName + + // DistributionModuleName is the name of the distribution module account, which + // receives the consensus key rotation fee. + DistributionModuleName = "distribution" ) // Keys for store prefixes @@ -60,6 +64,10 @@ 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 = []byte{0x91} // prefix for the consensus key rotation maturity queue, keyed by (time, valAddr) + ValidatorConsKeyRotationKey = []byte{0x92} // prefix for a validator's pending consensus key rotation, keyed by valAddr + RotatedConsAddrIndexKey = []byte{0x93} // prefix for the previously rotated consensus address lookup ) // UnbondingType defines the type of unbonding operation @@ -426,3 +434,54 @@ 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)...) +} + +// GetRotatedConsAddrIndexKey returns the lookup key for a previously rotated consensus address. +func GetRotatedConsAddrIndexKey(oldConsAddr sdk.ConsAddress) []byte { + return append(RotatedConsAddrIndexKey, address.MustLengthPrefix(oldConsAddr)...) +} diff --git a/x/staking/types/keys_test.go b/x/staking/types/keys_test.go index f9fa94979623..f431791f2293 100644 --- a/x/staking/types/keys_test.go +++ b/x/staking/types/keys_test.go @@ -134,6 +134,134 @@ 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 TestGetRotatedConsAddrIndexKey(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.GetRotatedConsAddrIndexKey(tt.consAddr)) + 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 { From 4908310088d6f20f93950d86d7a053ee83108063 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Thu, 21 May 2026 11:00:37 -0400 Subject: [PATCH 03/19] add comments to store keys --- x/staking/types/keys.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/x/staking/types/keys.go b/x/staking/types/keys.go index 5bef4a8f0a4e..2dcd6b49aa69 100644 --- a/x/staking/types/keys.go +++ b/x/staking/types/keys.go @@ -65,9 +65,30 @@ 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 = []byte{0x91} // prefix for the consensus key rotation maturity queue, keyed by (time, valAddr) + // 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 - RotatedConsAddrIndexKey = []byte{0x93} // prefix for the previously rotated consensus address lookup + + // RotatedConsAddrIndexKey allows us to lookup what an old consensus key + // has changed to. This allows us to ensure that a validator does not + // rotate to a consensus key that was previously used within the current + // unbondong period (e.g. val 1 changes from key A->B, val 2 cannot change + // from C->A). This lookup also allows slashing/evidence handling to + // associate an infraction on an old consensus key to the new consensus + // key. This is pruned when the key rotation falls out of the current + // unbonding period in the end blocker (determined by the + // ConsKeyRotationQueueKey). + RotatedConsAddrIndexKey = []byte{0x93} // prefix for the previously rotated consensus address lookup ) // UnbondingType defines the type of unbonding operation From a4e1182b20d9e9d4dd0ba32b5621396a252e7c59 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Thu, 21 May 2026 14:59:49 -0400 Subject: [PATCH 04/19] implement staking endblocker to perform key rotations --- x/staking/keeper/msg_server.go | 2 +- x/staking/keeper/msg_server_test.go | 2 +- x/staking/keeper/rotation.go | 154 +++++++++++++++++++++- x/staking/keeper/rotation_test.go | 152 +++++++++++++++++++++ x/staking/keeper/val_state_change.go | 11 ++ x/staking/keeper/val_state_change_test.go | 80 +++++++++++ x/staking/types/keys.go | 14 ++ x/staking/types/keys_test.go | 15 +++ 8 files changed, 425 insertions(+), 5 deletions(-) create mode 100644 x/staking/keeper/rotation_test.go create mode 100644 x/staking/keeper/val_state_change_test.go diff --git a/x/staking/keeper/msg_server.go b/x/staking/keeper/msg_server.go index deee6ce54baf..21f3b9685edd 100644 --- a/x/staking/keeper/msg_server.go +++ b/x/staking/keeper/msg_server.go @@ -671,7 +671,7 @@ func (k msgServer) RotateConsPubKey(ctx context.Context, msg *types.MsgRotateCon } // record the key rotation in the store - if err := k.SetConsKeyRotation(ctx, valAddr, oldPk, fee); err != nil { + if err := k.SetConsKeyRotation(ctx, valAddr, oldPk, newPk, fee); err != nil { return nil, err } diff --git a/x/staking/keeper/msg_server_test.go b/x/staking/keeper/msg_server_test.go index 4fd22f80549b..3ed8de9f013d 100644 --- a/x/staking/keeper/msg_server_test.go +++ b/x/staking/keeper/msg_server_test.go @@ -1229,7 +1229,7 @@ func (s *KeeperTestSuite) TestMsgRotateConsPubKey() { valAddr, _ := createValidator(stakingtypes.Bonded) oldPubKey := ed25519.GenPrivKey().PubKey() dummy := sdk.ValAddress(ed25519.GenPrivKey().PubKey().Address()) - require.NoError(s.stakingKeeper.SetConsKeyRotation(s.ctx, dummy, oldPubKey, stakingtypes.DefaultKeyRotationFee)) + require.NoError(s.stakingKeeper.SetConsKeyRotation(s.ctx, dummy, oldPubKey, ed25519.GenPrivKey().PubKey(), stakingtypes.DefaultKeyRotationFee)) return &stakingtypes.MsgRotateConsPubKey{ ValidatorAddress: valAddr.String(), NewPubkey: newAny(oldPubKey), diff --git a/x/staking/keeper/rotation.go b/x/staking/keeper/rotation.go index 60c67fc85d0b..4a03b0299815 100644 --- a/x/staking/keeper/rotation.go +++ b/x/staking/keeper/rotation.go @@ -2,8 +2,15 @@ package keeper import ( "context" + "errors" + 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" ) @@ -25,8 +32,9 @@ func (k Keeper) HasRotatedConsAddr(ctx context.Context, consAddr sdk.ConsAddress } // SetConsKeyRotation writes to indexes that track a pending consensus key -// rotation. -func (k Keeper) SetConsKeyRotation(ctx context.Context, valAddr sdk.ValAddress, oldPubKey cryptotypes.PubKey, fee sdk.Coin) error { +// 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, fee sdk.Coin) error { sdkCtx := sdk.UnwrapSDKContext(ctx) unbondingTime, err := k.UnbondingTime(ctx) @@ -38,6 +46,10 @@ func (k Keeper) SetConsKeyRotation(ctx context.Context, valAddr sdk.ValAddress, oldConsAddr := sdk.ConsAddress(oldPubKey.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 } @@ -46,5 +58,141 @@ func (k Keeper) SetConsKeyRotation(ctx context.Context, valAddr sdk.ValAddress, return err } - return store.Set(types.GetRotatedConsAddrIndexKey(oldConsAddr), valAddr) + if err := store.Set(types.GetRotatedConsAddrIndexKey(oldConsAddr), 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 ( + // used to defer the removal of UnappliedConsensusKeyRotationKeys until + // after iteration + rotatedValidators []sdk.ValAddress + + 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...) + + // defer removal until after iteration + rotatedValidators = append(rotatedValidators, valAddr) + + return nil + }) + if err != nil { + return nil, err + } + + // perform removal of pending rotation for each validator that rotated + for _, rotatedValidator := range rotatedValidators { + if err := store.Delete(types.GetUnappliedConsKeyRotationKey(rotatedValidator)); 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() + + // 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 + } + + store := k.storeService.OpenKVStore(ctx) + oldConsAddr, err := validator.GetConsAddr() + if err != nil { + return nil, err + } + + // remove the store entry for the previous cons addr pointing to the + // validator + 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 +} + +// IterateUnappliedConsKeyRotations walks every rotation queued by the msg +// server that the end blocker has not yet applied, in valAddr sorted order. +func (k Keeper) IterateUnappliedConsKeyRotations( + ctx context.Context, + cb func(valAddr sdk.ValAddress, newPubKey cryptotypes.PubKey) error, +) (err error) { + store := k.storeService.OpenKVStore(ctx) + iterator, err := store.Iterator(types.UnappliedConsKeyRotationKey, storetypes.PrefixEndBytes(types.UnappliedConsKeyRotationKey)) + if err != nil { + return err + } + defer func() { + err = errors.Join(err, iterator.Close()) + }() + + for ; iterator.Valid(); iterator.Next() { + key := iterator.Key() + valAddr := sdk.ValAddress(key[len(types.UnappliedConsKeyRotationKey)+1:]) + + var newPubKey cryptotypes.PubKey + if err := k.cdc.UnmarshalInterface(iterator.Value(), &newPubKey); err != nil { + return err + } + + if err := cb(valAddr, newPubKey); err != nil { + return err + } + } + return nil } diff --git a/x/staking/keeper/rotation_test.go b/x/staking/keeper/rotation_test.go new file mode 100644 index 000000000000..d7a1d44c7f13 --- /dev/null +++ b/x/staking/keeper/rotation_test.go @@ -0,0 +1,152 @@ +package keeper_test + +import ( + "testing" + + "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() (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 + require.NoError(s.stakingKeeper.SetValidator(s.ctx, v)) + require.NoError(s.stakingKeeper.SetValidatorByConsAddr(s.ctx, v)) + return valAddr, pk + } + + testCases := []struct { + name string + setup func() (valAddr sdk.ValAddress, oldPk, newPk cryptotypes.PubKey) + expErr bool + expErrMsg string + }{ + { + name: "successful rotation", + setup: func() (sdk.ValAddress, cryptotypes.PubKey, cryptotypes.PubKey) { + valAddr, oldPk := createValidator() + return valAddr, oldPk, ed25519.GenPrivKey().PubKey() + }, + }, + { + name: "validator not found", + setup: func() (sdk.ValAddress, cryptotypes.PubKey, cryptotypes.PubKey) { + missing := sdk.ValAddress(ed25519.GenPrivKey().PubKey().Address()) + return missing, nil, ed25519.GenPrivKey().PubKey() + }, + expErr: true, + expErrMsg: stakingtypes.ErrNoValidatorFound.Error(), + }, + { + name: "rotate to same key is a no-op", + setup: func() (sdk.ValAddress, cryptotypes.PubKey, cryptotypes.PubKey) { + valAddr, oldPk := createValidator() + return valAddr, oldPk, oldPk + }, + }, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + s.SetupTest() + + valAddr, oldPk, newPk := tc.setup() + + err := s.stakingKeeper.ApplyConsKeyRotation(s.ctx, valAddr, newPk) + if tc.expErr { + require.Error(err) + require.Contains(err.Error(), tc.expErrMsg) + return + } + require.NoError(err) + + // validator's stored ConsensusPubkey must now resolve to newPk + v, err := s.stakingKeeper.GetValidator(s.ctx, valAddr) + require.NoError(err) + gotConsAddr, err := v.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 + } + + testCases := []struct { + name string + seedCount int + stopAfter int + expectLen int + }{ + {"empty store", 0, 0, 0}, + {"single entry", 1, 0, 1}, + {"three entries", 3, 0, 3}, + {"stop after first of three", 3, 1, 1}, + } + + 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, stakingtypes.DefaultKeyRotationFee)) + } + + var observed []entry + err := s.stakingKeeper.IterateUnappliedConsKeyRotations(s.ctx, func(valAddr sdk.ValAddress, newPk cryptotypes.PubKey) bool { + observed = append(observed, entry{valAddr, newPk}) + return tc.stopAfter > 0 && len(observed) >= tc.stopAfter + }) + 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) + } + + // non-stop cases must observe every seeded entry + if tc.stopAfter == 0 { + require.Len(observed, len(seeded)) + } + }) + } +} diff --git a/x/staking/keeper/val_state_change.go b/x/staking/keeper/val_state_change.go index 7761d80a2eea..742f15497448 100644 --- a/x/staking/keeper/val_state_change.go +++ b/x/staking/keeper/val_state_change.go @@ -238,6 +238,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..99139c8cec37 --- /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, stakingtypes.DefaultKeyRotationFee)) + + 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/types/keys.go b/x/staking/types/keys.go index 2dcd6b49aa69..13e30fd27916 100644 --- a/x/staking/types/keys.go +++ b/x/staking/types/keys.go @@ -89,6 +89,14 @@ var ( // unbonding period in the end blocker (determined by the // ConsKeyRotationQueueKey). RotatedConsAddrIndexKey = []byte{0x93} // prefix for the previously rotated 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 @@ -506,3 +514,9 @@ func GetValidatorConsKeyRotationKey(valAddr sdk.ValAddress) []byte { func GetRotatedConsAddrIndexKey(oldConsAddr sdk.ConsAddress) []byte { return append(RotatedConsAddrIndexKey, address.MustLengthPrefix(oldConsAddr)...) } + +// 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 f431791f2293..e1dfd8ccc9cb 100644 --- a/x/staking/types/keys_test.go +++ b/x/staking/types/keys_test.go @@ -262,6 +262,21 @@ func TestGetRotatedConsAddrIndexKey(t *testing.T) { } } +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 From 90a2a03cf2ea07282badc98f433b800ed0db14a1 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Thu, 21 May 2026 15:27:13 -0400 Subject: [PATCH 05/19] prune key rotations from the store that have fallen out of their unbonding period --- x/staking/keeper/rotation.go | 82 ++++++++++---- x/staking/keeper/rotation_test.go | 162 ++++++++++++++++++++------- x/staking/keeper/val_state_change.go | 6 + 3 files changed, 185 insertions(+), 65 deletions(-) diff --git a/x/staking/keeper/rotation.go b/x/staking/keeper/rotation.go index 4a03b0299815..1084d4577852 100644 --- a/x/staking/keeper/rotation.go +++ b/x/staking/keeper/rotation.go @@ -3,6 +3,7 @@ package keeper import ( "context" "errors" + "time" abci "github.com/cometbft/cometbft/abci/types" @@ -31,6 +32,12 @@ func (k Keeper) HasRotatedConsAddr(ctx context.Context, consAddr sdk.ConsAddress return k.storeService.OpenKVStore(ctx).Has(types.GetRotatedConsAddrIndexKey(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. @@ -73,13 +80,7 @@ func (k Keeper) SetConsKeyRotation(ctx context.Context, valAddr sdk.ValAddress, // 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 ( - // used to defer the removal of UnappliedConsensusKeyRotationKeys until - // after iteration - rotatedValidators []sdk.ValAddress - - totalUpdates abci.ValidatorUpdates - ) + var totalUpdates abci.ValidatorUpdates store := k.storeService.OpenKVStore(ctx) err := k.IterateUnappliedConsKeyRotations(ctx, func(valAddr sdk.ValAddress, newPubKey cryptotypes.PubKey) error { @@ -96,22 +97,12 @@ func (k Keeper) ApplyPendingConsKeyRotations(ctx context.Context, powerReduction } totalUpdates = append(totalUpdates, updates...) - // defer removal until after iteration - rotatedValidators = append(rotatedValidators, valAddr) - - return nil + return store.Delete(types.GetUnappliedConsKeyRotationKey(valAddr)) }) if err != nil { return nil, err } - // perform removal of pending rotation for each validator that rotated - for _, rotatedValidator := range rotatedValidators { - if err := store.Delete(types.GetUnappliedConsKeyRotationKey(rotatedValidator)); err != nil { - return nil, err - } - } - return totalUpdates, nil } @@ -129,6 +120,13 @@ func (k Keeper) ApplyConsKeyRotation(ctx context.Context, validator types.Valida // 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 { @@ -146,14 +144,9 @@ func (k Keeper) ApplyConsKeyRotation(ctx context.Context, validator types.Valida return nil, err } - store := k.storeService.OpenKVStore(ctx) - oldConsAddr, err := validator.GetConsAddr() - if 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 } @@ -166,6 +159,47 @@ func (k Keeper) ApplyConsKeyRotation(ctx context.Context, validator types.Valida 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) (err error) { + sdkCtx := sdk.UnwrapSDKContext(ctx) + blockTime := sdkCtx.BlockHeader().Time + + store := k.storeService.OpenKVStore(ctx) + iterator, err := store.Iterator( + types.ConsKeyRotationQueueKey, + storetypes.PrefixEndBytes(types.GetConsKeyRotationQueueTimePrefix(blockTime)), + ) + if err != nil { + return err + } + defer func() { + err = errors.Join(err, iterator.Close()) + }() + + for ; iterator.Valid(); iterator.Next() { + _, valAddr, err := types.ParseConsKeyRotationQueueKey(iterator.Key()) + if err != nil { + return err + } + oldConsAddr := sdk.ConsAddress(iterator.Value()) + + if err := store.Delete(iterator.Key()); err != nil { + return err + } + if err := store.Delete(types.GetValidatorConsKeyRotationKey(valAddr)); err != nil { + return err + } + if err := store.Delete(types.GetRotatedConsAddrIndexKey(oldConsAddr)); err != nil { + return err + } + } + + return nil +} + // IterateUnappliedConsKeyRotations walks every rotation queued by the msg // server that the end blocker has not yet applied, in valAddr sorted order. func (k Keeper) IterateUnappliedConsKeyRotations( diff --git a/x/staking/keeper/rotation_test.go b/x/staking/keeper/rotation_test.go index d7a1d44c7f13..5436550883fd 100644 --- a/x/staking/keeper/rotation_test.go +++ b/x/staking/keeper/rotation_test.go @@ -1,7 +1,11 @@ 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" @@ -12,44 +16,35 @@ import ( func (s *KeeperTestSuite) TestApplyConsKeyRotation() { require := s.Require() - createValidator := func() (sdk.ValAddress, cryptotypes.PubKey) { + 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 valAddr, pk + return v, valAddr, pk } testCases := []struct { - name string - setup func() (valAddr sdk.ValAddress, oldPk, newPk cryptotypes.PubKey) - expErr bool - expErrMsg string + name string + setup func() (validator stakingtypes.Validator, valAddr sdk.ValAddress, oldPk, newPk cryptotypes.PubKey) }{ { name: "successful rotation", - setup: func() (sdk.ValAddress, cryptotypes.PubKey, cryptotypes.PubKey) { - valAddr, oldPk := createValidator() - return valAddr, oldPk, ed25519.GenPrivKey().PubKey() - }, - }, - { - name: "validator not found", - setup: func() (sdk.ValAddress, cryptotypes.PubKey, cryptotypes.PubKey) { - missing := sdk.ValAddress(ed25519.GenPrivKey().PubKey().Address()) - return missing, nil, ed25519.GenPrivKey().PubKey() + setup: func() (stakingtypes.Validator, sdk.ValAddress, cryptotypes.PubKey, cryptotypes.PubKey) { + v, valAddr, oldPk := createValidator() + return v, valAddr, oldPk, ed25519.GenPrivKey().PubKey() }, - expErr: true, - expErrMsg: stakingtypes.ErrNoValidatorFound.Error(), }, { name: "rotate to same key is a no-op", - setup: func() (sdk.ValAddress, cryptotypes.PubKey, cryptotypes.PubKey) { - valAddr, oldPk := createValidator() - return valAddr, oldPk, oldPk + setup: func() (stakingtypes.Validator, sdk.ValAddress, cryptotypes.PubKey, cryptotypes.PubKey) { + v, valAddr, oldPk := createValidator() + return v, valAddr, oldPk, oldPk }, }, } @@ -58,20 +53,18 @@ func (s *KeeperTestSuite) TestApplyConsKeyRotation() { s.T().Run(tc.name, func(t *testing.T) { s.SetupTest() - valAddr, oldPk, newPk := tc.setup() + validator, valAddr, oldPk, newPk := tc.setup() - err := s.stakingKeeper.ApplyConsKeyRotation(s.ctx, valAddr, newPk) - if tc.expErr { - require.Error(err) - require.Contains(err.Error(), tc.expErrMsg) - return - } + 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 - v, err := s.stakingKeeper.GetValidator(s.ctx, valAddr) + stored, err := s.stakingKeeper.GetValidator(s.ctx, valAddr) require.NoError(err) - gotConsAddr, err := v.GetConsAddr() + gotConsAddr, err := stored.GetConsAddr() require.NoError(err) require.Equal(sdk.ConsAddress(newPk.Address()).Bytes(), gotConsAddr) @@ -97,16 +90,19 @@ func (s *KeeperTestSuite) TestIterateUnappliedConsKeyRotations() { newPk cryptotypes.PubKey } + errStop := errors.New("stop") + testCases := []struct { name string seedCount int - stopAfter int + stopAfter int // 0 = no stop expectLen int + expectErr error }{ - {"empty store", 0, 0, 0}, - {"single entry", 1, 0, 1}, - {"three entries", 3, 0, 3}, - {"stop after first of three", 3, 1, 1}, + {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 { @@ -124,11 +120,18 @@ func (s *KeeperTestSuite) TestIterateUnappliedConsKeyRotations() { } var observed []entry - err := s.stakingKeeper.IterateUnappliedConsKeyRotations(s.ctx, func(valAddr sdk.ValAddress, newPk cryptotypes.PubKey) bool { + err := s.stakingKeeper.IterateUnappliedConsKeyRotations(s.ctx, func(valAddr sdk.ValAddress, newPk cryptotypes.PubKey) error { observed = append(observed, entry{valAddr, newPk}) - return tc.stopAfter > 0 && len(observed) >= tc.stopAfter + if tc.stopAfter > 0 && len(observed) >= tc.stopAfter { + return errStop + } + return nil }) - require.NoError(err) + 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 @@ -142,10 +145,87 @@ func (s *KeeperTestSuite) TestIterateUnappliedConsKeyRotations() { } 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, stakingtypes.DefaultKeyRotationFee)) + 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.HasPendingConsKeyRotation(s.ctx, e.valAddr) + require.NoError(err) + require.False(hasPending, "matured per-validator entry should be pruned") + + hasCons, err := s.stakingKeeper.HasRotatedConsAddr(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.HasPendingConsKeyRotation(s.ctx, e.valAddr) + require.NoError(err) + require.True(hasPending, "future per-validator entry should remain") - // non-stop cases must observe every seeded entry - if tc.stopAfter == 0 { - require.Len(observed, len(seeded)) + hasCons, err := s.stakingKeeper.HasRotatedConsAddr(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 742f15497448..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 } From 614892719d8228728f94167cc26db9e7cc72b116 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Thu, 21 May 2026 16:04:05 -0400 Subject: [PATCH 06/19] burn key rotation fee instad of going to community pool --- x/staking/keeper/msg_server.go | 16 +++++++++++++--- x/staking/keeper/msg_server_test.go | 10 ++++++++-- x/staking/types/keys.go | 4 ---- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/x/staking/keeper/msg_server.go b/x/staking/keeper/msg_server.go index 21f3b9685edd..d49402811ecc 100644 --- a/x/staking/keeper/msg_server.go +++ b/x/staking/keeper/msg_server.go @@ -644,6 +644,9 @@ func (k msgServer) RotateConsPubKey(ctx context.Context, msg *types.MsgRotateCon if err != nil { return nil, types.ErrNoValidatorFound } + + // TODO: this is likely too strict, we probably only need to restrict to + // not allowing tombstoned validators to rotate if status := validator.GetStatus(); status != types.Bonded { return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "validator status is not bonded, got %s", status) } @@ -663,10 +666,17 @@ func (k msgServer) RotateConsPubKey(ctx context.Context, msg *types.MsgRotateCon return nil, types.ErrExceedingMaxConsPubKeyRotations } - // transfer the rotation fee from the validators account to the - // distribution module, which forwards it to the community pool + // burn the rotation fee. NotBondedPool is used as the transit account + // because BurnCoins requires a module account with Burner permission. + + // TODO: is there an easier way to burn without having to go to the not + // bonded pool/module account first? seems like no fee := types.DefaultKeyRotationFee - if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sdk.AccAddress(valAddr), types.DistributionModuleName, sdk.NewCoins(fee)); err != nil { + feeCoins := sdk.NewCoins(fee) + if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sdk.AccAddress(valAddr), types.NotBondedPoolName, feeCoins); err != nil { + return nil, err + } + if err := k.bankKeeper.BurnCoins(ctx, types.NotBondedPoolName, feeCoins); err != nil { return nil, err } diff --git a/x/staking/keeper/msg_server_test.go b/x/staking/keeper/msg_server_test.go index 3ed8de9f013d..e245ba061a09 100644 --- a/x/staking/keeper/msg_server_test.go +++ b/x/staking/keeper/msg_server_test.go @@ -1242,7 +1242,10 @@ func (s *KeeperTestSuite) TestMsgRotateConsPubKey() { newRotateConsPubKeyMsg: func() *stakingtypes.MsgRotateConsPubKey { valAddr, _ := createValidator(stakingtypes.Bonded) s.bankKeeper.EXPECT(). - SendCoinsFromAccountToModule(gomock.Any(), sdk.AccAddress(valAddr), stakingtypes.DistributionModuleName, gomock.Any()). + SendCoinsFromAccountToModule(gomock.Any(), sdk.AccAddress(valAddr), stakingtypes.NotBondedPoolName, gomock.Any()). + Return(nil) + s.bankKeeper.EXPECT(). + BurnCoins(gomock.Any(), stakingtypes.NotBondedPoolName, gomock.Any()). Return(nil) return &stakingtypes.MsgRotateConsPubKey{ ValidatorAddress: valAddr.String(), @@ -1257,7 +1260,10 @@ func (s *KeeperTestSuite) TestMsgRotateConsPubKey() { // submit a valid rotation for valAddr valAddr, _ := createValidator(stakingtypes.Bonded) s.bankKeeper.EXPECT(). - SendCoinsFromAccountToModule(gomock.Any(), sdk.AccAddress(valAddr), stakingtypes.DistributionModuleName, gomock.Any()). + SendCoinsFromAccountToModule(gomock.Any(), sdk.AccAddress(valAddr), stakingtypes.NotBondedPoolName, gomock.Any()). + Return(nil) + s.bankKeeper.EXPECT(). + BurnCoins(gomock.Any(), stakingtypes.NotBondedPoolName, gomock.Any()). Return(nil) valid := &stakingtypes.MsgRotateConsPubKey{ ValidatorAddress: valAddr.String(), diff --git a/x/staking/types/keys.go b/x/staking/types/keys.go index 13e30fd27916..90466a70fc34 100644 --- a/x/staking/types/keys.go +++ b/x/staking/types/keys.go @@ -23,10 +23,6 @@ const ( // RouterKey is the msg router key for the staking module RouterKey = ModuleName - - // DistributionModuleName is the name of the distribution module account, which - // receives the consensus key rotation fee. - DistributionModuleName = "distribution" ) // Keys for store prefixes From 44361e1684e61923980836aae8b094dd33809d89 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Thu, 21 May 2026 16:20:54 -0400 Subject: [PATCH 07/19] add consensus key rotation integration tests --- .../staking/keeper/cons_key_rotation_test.go | 250 ++++++++++++++++++ testutil/integration/router.go | 13 +- 2 files changed, 259 insertions(+), 4 deletions(-) create mode 100644 tests/integration/staking/keeper/cons_key_rotation_test.go 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..94949035d38b --- /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.HasPendingConsKeyRotation(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.HasRotatedConsAddr(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.HasPendingConsKeyRotation(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.HasPendingConsKeyRotation(f.sdkCtx, valAddr) + assert.NilError(t, err) + assert.Assert(t, !hasPending, "per-validator pending index should be pruned") + + hasRotated, err := f.stakingKeeper.HasRotatedConsAddr(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/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() From af08ef6c6a2168504e26448d90c6a1defb2a2e96 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Thu, 21 May 2026 16:32:54 -0400 Subject: [PATCH 08/19] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6362ad7ea702..5e84a589bd00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (abci) [#25620](https://github.com/cosmos/cosmos-sdk/pull/25620) Add support for new application side mempool ABCI methods. * (abci) [#25969](https://github.com/cosmos/cosmos-sdk/pull/25969) Add support for new ABCI methods, `InsertTx` and `ReapTxs`. * (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. ### Improvements From 733e1820944cd4f1226343076aa593d64ba06cb3 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Thu, 21 May 2026 16:36:21 -0400 Subject: [PATCH 09/19] RotateConsPubKey godoc --- x/staking/keeper/msg_server.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x/staking/keeper/msg_server.go b/x/staking/keeper/msg_server.go index d49402811ecc..b9c03807d7bd 100644 --- a/x/staking/keeper/msg_server.go +++ b/x/staking/keeper/msg_server.go @@ -614,6 +614,8 @@ 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 { From 99d977b9abf08a10d8f41c4b3e019d5719595e58 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Thu, 21 May 2026 17:06:04 -0400 Subject: [PATCH 10/19] allow any bond status to rotate cons keys, dont allow jailed to rotate keys --- x/staking/keeper/msg_server.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/x/staking/keeper/msg_server.go b/x/staking/keeper/msg_server.go index b9c03807d7bd..599c3990173a 100644 --- a/x/staking/keeper/msg_server.go +++ b/x/staking/keeper/msg_server.go @@ -647,10 +647,8 @@ func (k msgServer) RotateConsPubKey(ctx context.Context, msg *types.MsgRotateCon return nil, types.ErrNoValidatorFound } - // TODO: this is likely too strict, we probably only need to restrict to - // not allowing tombstoned validators to rotate - if status := validator.GetStatus(); status != types.Bonded { - return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "validator status is not bonded, got %s", status) + if validator.IsJailed() { + return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "validator is jailed") } // shouldnt ever happen From 2d6870fa8d02baf03fe5f914ae7d4dd54ea53cbb Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Thu, 21 May 2026 17:07:59 -0400 Subject: [PATCH 11/19] change to function name HasPendingConsKeyRotation to HasConsKeyRotationInUnbondingWindow --- x/staking/keeper/rotation.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x/staking/keeper/rotation.go b/x/staking/keeper/rotation.go index 1084d4577852..64b78e39b3c0 100644 --- a/x/staking/keeper/rotation.go +++ b/x/staking/keeper/rotation.go @@ -20,9 +20,9 @@ import ( // a validator may have inside the unbonding window. const MaxConsKeyRotations = 1 -// HasPendingConsKeyRotation returns whether the validator has a pending -// consensus key rotation inside the unbonding window. -func (k Keeper) HasPendingConsKeyRotation(ctx context.Context, valAddr sdk.ValAddress) (bool, error) { +// 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)) } From cb7697a37a212102130c363b8e17addb32000389 Mon Sep 17 00:00:00 2001 From: mattac21 Date: Thu, 21 May 2026 17:03:39 -0400 Subject: [PATCH 12/19] dont discard GetValidator errors when checking if new consensus key is in use Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- x/staking/keeper/msg_server.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/x/staking/keeper/msg_server.go b/x/staking/keeper/msg_server.go index 599c3990173a..9ba347e5215c 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" @@ -634,7 +635,11 @@ func (k msgServer) RotateConsPubKey(ctx context.Context, msg *types.MsgRotateCon } // reject a key currently in use by some validator - if existing, err := k.GetValidatorByConsAddr(ctx, newConsAddr); err == nil && existing.OperatorAddress != "" { + 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 } @@ -658,7 +663,7 @@ func (k msgServer) RotateConsPubKey(ctx context.Context, msg *types.MsgRotateCon } // enforce the per validator rotation limit inside the unbonding window - pending, err := k.HasPendingConsKeyRotation(ctx, valAddr) + pending, err := k.HasConsKeyRotationInUnbondingWindow(ctx, valAddr) if err != nil { return nil, err } From 5681d5c8d0280cfcae2cb60e78d20c5e76900107 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Thu, 21 May 2026 17:45:24 -0400 Subject: [PATCH 13/19] send key rotation fee to new staking module account key_rotation_fee_pool before burning --- enterprise/group/simapp/app.go | 11 ++++++----- enterprise/poa/examples/migrate-from-pos/app.go | 13 +++++++------ simapp/app.go | 13 +++++++------ tests/e2e/distribution/config.go | 1 + .../distribution/keeper/msg_server_test.go | 9 +++++---- .../integration/evidence/keeper/infraction_test.go | 7 ++++--- tests/integration/gov/keeper/keeper_test.go | 11 ++++++----- tests/integration/slashing/keeper/keeper_test.go | 7 ++++--- tests/integration/staking/keeper/common_test.go | 9 +++++---- .../staking/keeper/cons_key_rotation_test.go | 6 +++--- .../integration/staking/keeper/determinstic_test.go | 9 +++++---- testutil/configurator/configurator.go | 1 + x/staking/keeper/keeper.go | 4 ++++ x/staking/keeper/keeper_test.go | 8 +++++--- x/staking/keeper/msg_server.go | 13 ++++++------- x/staking/keeper/msg_server_test.go | 8 ++++---- x/staking/keeper/rotation_test.go | 4 ++-- x/staking/keeper_bench_test.go | 2 ++ x/staking/module_test.go | 3 +++ x/staking/types/pool.go | 7 +++++-- 20 files changed, 85 insertions(+), 61 deletions(-) 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/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 index 94949035d38b..471377c2ad3f 100644 --- a/tests/integration/staking/keeper/cons_key_rotation_test.go +++ b/tests/integration/staking/keeper/cons_key_rotation_test.go @@ -56,7 +56,7 @@ func TestRotateConsPubKey_MsgServerQueuesAndEndBlockerApplies(t *testing.T) { // per-validator pending index recorded (gates further rotations inside the // unbonding window) - hasPending, err := f.stakingKeeper.HasPendingConsKeyRotation(f.sdkCtx, valAddr) + hasPending, err := f.stakingKeeper.HasConsKeyRotationInUnbondingWindow(f.sdkCtx, valAddr) assert.NilError(t, err) assert.Assert(t, hasPending) @@ -106,7 +106,7 @@ func TestRotateConsPubKey_MsgServerQueuesAndEndBlockerApplies(t *testing.T) { // 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.HasPendingConsKeyRotation(f.sdkCtx, valAddr) + hasPendingAfter, err := f.stakingKeeper.HasConsKeyRotationInUnbondingWindow(f.sdkCtx, valAddr) assert.NilError(t, err) assert.Assert(t, hasPendingAfter) } @@ -149,7 +149,7 @@ func TestRotateConsPubKey_PruneClearsRotationStateAfterUnbonding(t *testing.T) { assert.NilError(t, err) assert.Assert(t, !has, "maturity queue entry should be pruned") - hasPending, err := f.stakingKeeper.HasPendingConsKeyRotation(f.sdkCtx, valAddr) + hasPending, err := f.stakingKeeper.HasConsKeyRotationInUnbondingWindow(f.sdkCtx, valAddr) assert.NilError(t, err) assert.Assert(t, !hasPending, "per-validator pending index should be pruned") 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/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 9ba347e5215c..a1fca8b28ccc 100644 --- a/x/staking/keeper/msg_server.go +++ b/x/staking/keeper/msg_server.go @@ -671,17 +671,16 @@ func (k msgServer) RotateConsPubKey(ctx context.Context, msg *types.MsgRotateCon return nil, types.ErrExceedingMaxConsPubKeyRotations } - // burn the rotation fee. NotBondedPool is used as the transit account - // because BurnCoins requires a module account with Burner permission. - - // TODO: is there an easier way to burn without having to go to the not - // bonded pool/module account first? seems like no + // 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. fee := types.DefaultKeyRotationFee feeCoins := sdk.NewCoins(fee) - if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sdk.AccAddress(valAddr), types.NotBondedPoolName, feeCoins); err != nil { + if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sdk.AccAddress(valAddr), types.KeyRotationFeePoolName, feeCoins); err != nil { return nil, err } - if err := k.bankKeeper.BurnCoins(ctx, types.NotBondedPoolName, feeCoins); err != nil { + if err := k.bankKeeper.BurnCoins(ctx, types.KeyRotationFeePoolName, feeCoins); err != nil { return nil, err } diff --git a/x/staking/keeper/msg_server_test.go b/x/staking/keeper/msg_server_test.go index e245ba061a09..50631a3320ee 100644 --- a/x/staking/keeper/msg_server_test.go +++ b/x/staking/keeper/msg_server_test.go @@ -1242,10 +1242,10 @@ func (s *KeeperTestSuite) TestMsgRotateConsPubKey() { newRotateConsPubKeyMsg: func() *stakingtypes.MsgRotateConsPubKey { valAddr, _ := createValidator(stakingtypes.Bonded) s.bankKeeper.EXPECT(). - SendCoinsFromAccountToModule(gomock.Any(), sdk.AccAddress(valAddr), stakingtypes.NotBondedPoolName, gomock.Any()). + SendCoinsFromAccountToModule(gomock.Any(), sdk.AccAddress(valAddr), stakingtypes.KeyRotationFeePoolName, gomock.Any()). Return(nil) s.bankKeeper.EXPECT(). - BurnCoins(gomock.Any(), stakingtypes.NotBondedPoolName, gomock.Any()). + BurnCoins(gomock.Any(), stakingtypes.KeyRotationFeePoolName, gomock.Any()). Return(nil) return &stakingtypes.MsgRotateConsPubKey{ ValidatorAddress: valAddr.String(), @@ -1260,10 +1260,10 @@ func (s *KeeperTestSuite) TestMsgRotateConsPubKey() { // submit a valid rotation for valAddr valAddr, _ := createValidator(stakingtypes.Bonded) s.bankKeeper.EXPECT(). - SendCoinsFromAccountToModule(gomock.Any(), sdk.AccAddress(valAddr), stakingtypes.NotBondedPoolName, gomock.Any()). + SendCoinsFromAccountToModule(gomock.Any(), sdk.AccAddress(valAddr), stakingtypes.KeyRotationFeePoolName, gomock.Any()). Return(nil) s.bankKeeper.EXPECT(). - BurnCoins(gomock.Any(), stakingtypes.NotBondedPoolName, gomock.Any()). + BurnCoins(gomock.Any(), stakingtypes.KeyRotationFeePoolName, gomock.Any()). Return(nil) valid := &stakingtypes.MsgRotateConsPubKey{ ValidatorAddress: valAddr.String(), diff --git a/x/staking/keeper/rotation_test.go b/x/staking/keeper/rotation_test.go index 5436550883fd..5b60ed5fa533 100644 --- a/x/staking/keeper/rotation_test.go +++ b/x/staking/keeper/rotation_test.go @@ -205,7 +205,7 @@ func (s *KeeperTestSuite) TestPruneMaturedConsKeyRotations() { require.NoError(err) require.False(hasQueue, "matured queue entry should be pruned") - hasPending, err := s.stakingKeeper.HasPendingConsKeyRotation(s.ctx, e.valAddr) + hasPending, err := s.stakingKeeper.HasConsKeyRotationInUnbondingWindow(s.ctx, e.valAddr) require.NoError(err) require.False(hasPending, "matured per-validator entry should be pruned") @@ -219,7 +219,7 @@ func (s *KeeperTestSuite) TestPruneMaturedConsKeyRotations() { require.NoError(err) require.True(hasQueue, "future queue entry should remain") - hasPending, err := s.stakingKeeper.HasPendingConsKeyRotation(s.ctx, e.valAddr) + hasPending, err := s.stakingKeeper.HasConsKeyRotationInUnbondingWindow(s.ctx, e.valAddr) require.NoError(err) require.True(hasPending, "future per-validator entry should remain") 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/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 From c12ff76bd805d7692892ce2c056f3a809953fa45 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Thu, 21 May 2026 17:48:15 -0400 Subject: [PATCH 14/19] update validator not bonded test to validator not jailed --- x/staking/keeper/msg_server_test.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/x/staking/keeper/msg_server_test.go b/x/staking/keeper/msg_server_test.go index 50631a3320ee..0a0999747cc3 100644 --- a/x/staking/keeper/msg_server_test.go +++ b/x/staking/keeper/msg_server_test.go @@ -1201,15 +1201,19 @@ func (s *KeeperTestSuite) TestMsgRotateConsPubKey() { expErr: stakingtypes.ErrNoValidatorFound.Error(), }, { - name: "validator not bonded", + name: "validator jailed", newRotateConsPubKeyMsg: func() *stakingtypes.MsgRotateConsPubKey { - valAddr, _ := createValidator(stakingtypes.Unbonded) + 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 status is not bonded", + expErr: "validator is jailed", }, { name: "new pubkey already used by another validator", From 60e55eedf6b9c08103cbee8a1c18920a9546d44e Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Thu, 21 May 2026 18:01:34 -0400 Subject: [PATCH 15/19] fetch the key rotation fee pool module account in staking genesis to make sure it is initialized --- x/staking/keeper/genesis.go | 6 ++++++ x/staking/keeper/pool.go | 7 +++++++ 2 files changed, 13 insertions(+) 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/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) From c3a51710d32a29d854bc847d2d74f8b8faeaef57 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Thu, 21 May 2026 18:08:43 -0400 Subject: [PATCH 16/19] add todo to update slashing --- x/staking/keeper/rotation.go | 1 + 1 file changed, 1 insertion(+) diff --git a/x/staking/keeper/rotation.go b/x/staking/keeper/rotation.go index 64b78e39b3c0..9073fda5f0ef 100644 --- a/x/staking/keeper/rotation.go +++ b/x/staking/keeper/rotation.go @@ -180,6 +180,7 @@ func (k Keeper) PruneMaturedConsKeyRotations(ctx context.Context) (err error) { }() for ; iterator.Valid(); iterator.Next() { + // TODO: migrate ValidatorSigningInfo from oldConsAddr to newConsAddr _, valAddr, err := types.ParseConsKeyRotationQueueKey(iterator.Key()) if err != nil { return err From b0137af416c53acdc3f29f27793312023e64b9fb Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Fri, 22 May 2026 09:40:24 -0400 Subject: [PATCH 17/19] block rotations to addresses that are already have a pending rotation --- .../staking/keeper/cons_key_rotation_test.go | 4 +-- x/staking/keeper/msg_server.go | 9 +++--- x/staking/keeper/msg_server_test.go | 14 +++++++++ x/staking/keeper/rotation.go | 31 +++++++++++++++---- x/staking/keeper/rotation_test.go | 4 +-- x/staking/types/keys.go | 28 +++++++++-------- x/staking/types/keys_test.go | 4 +-- 7 files changed, 65 insertions(+), 29 deletions(-) diff --git a/tests/integration/staking/keeper/cons_key_rotation_test.go b/tests/integration/staking/keeper/cons_key_rotation_test.go index 471377c2ad3f..65e7a4ecee25 100644 --- a/tests/integration/staking/keeper/cons_key_rotation_test.go +++ b/tests/integration/staking/keeper/cons_key_rotation_test.go @@ -70,7 +70,7 @@ func TestRotateConsPubKey_MsgServerQueuesAndEndBlockerApplies(t *testing.T) { // rotated cons addr index recorded so the old key still resolves to this // validator for slashing/evidence routing - hasRotated, err := f.stakingKeeper.HasRotatedConsAddr(f.sdkCtx, oldConsAddr) + hasRotated, err := f.stakingKeeper.IsConsAddrLockedByRotation(f.sdkCtx, oldConsAddr) assert.NilError(t, err) assert.Assert(t, hasRotated) @@ -153,7 +153,7 @@ func TestRotateConsPubKey_PruneClearsRotationStateAfterUnbonding(t *testing.T) { assert.NilError(t, err) assert.Assert(t, !hasPending, "per-validator pending index should be pruned") - hasRotated, err := f.stakingKeeper.HasRotatedConsAddr(f.sdkCtx, oldConsAddr) + hasRotated, err := f.stakingKeeper.IsConsAddrLockedByRotation(f.sdkCtx, oldConsAddr) assert.NilError(t, err) assert.Assert(t, !hasRotated, "rotated cons addr index should be pruned") } diff --git a/x/staking/keeper/msg_server.go b/x/staking/keeper/msg_server.go index a1fca8b28ccc..2cfcdebaa5e1 100644 --- a/x/staking/keeper/msg_server.go +++ b/x/staking/keeper/msg_server.go @@ -624,13 +624,14 @@ func (k msgServer) RotateConsPubKey(ctx context.Context, msg *types.MsgRotateCon } newConsAddr := sdk.ConsAddress(newPk.Address()) - // reject reuse of a key that some validator rotated away from inside the - // unbonding window - rotated, err := k.HasRotatedConsAddr(ctx, newConsAddr) + // 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 rotated { + if locked { return nil, types.ErrConsensusPubKeyInRotationHistory } diff --git a/x/staking/keeper/msg_server_test.go b/x/staking/keeper/msg_server_test.go index 0a0999747cc3..282f36048a9b 100644 --- a/x/staking/keeper/msg_server_test.go +++ b/x/staking/keeper/msg_server_test.go @@ -1241,6 +1241,20 @@ func (s *KeeperTestSuite) TestMsgRotateConsPubKey() { }, 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, stakingtypes.DefaultKeyRotationFee)) + return &stakingtypes.MsgRotateConsPubKey{ + ValidatorAddress: valAddr.String(), + NewPubkey: newAny(targetPubKey), + } + }, + expErr: stakingtypes.ErrConsensusPubKeyInRotationHistory.Error(), + }, { name: "valid msg", newRotateConsPubKeyMsg: func() *stakingtypes.MsgRotateConsPubKey { diff --git a/x/staking/keeper/rotation.go b/x/staking/keeper/rotation.go index 9073fda5f0ef..24a1897ca83d 100644 --- a/x/staking/keeper/rotation.go +++ b/x/staking/keeper/rotation.go @@ -26,10 +26,12 @@ func (k Keeper) HasConsKeyRotationInUnbondingWindow(ctx context.Context, valAddr return k.storeService.OpenKVStore(ctx).Has(types.GetValidatorConsKeyRotationKey(valAddr)) } -// HasRotatedConsAddr returns whether the given consensus address was previously -// rotated away from and is still inside its unbonding window. -func (k Keeper) HasRotatedConsAddr(ctx context.Context, consAddr sdk.ConsAddress) (bool, error) { - return k.storeService.OpenKVStore(ctx).Has(types.GetRotatedConsAddrIndexKey(consAddr)) +// 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 @@ -51,6 +53,7 @@ func (k Keeper) SetConsKeyRotation(ctx context.Context, valAddr sdk.ValAddress, maturity := sdkCtx.BlockHeader().Time.Add(unbondingTime) oldConsAddr := sdk.ConsAddress(oldPubKey.Address()) + newConsAddr := sdk.ConsAddress(newPubKey.Address()) store := k.storeService.OpenKVStore(ctx) @@ -65,7 +68,15 @@ func (k Keeper) SetConsKeyRotation(ctx context.Context, valAddr sdk.ValAddress, return err } - if err := store.Set(types.GetRotatedConsAddrIndexKey(oldConsAddr), valAddr); err != nil { + // 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 } @@ -97,6 +108,14 @@ func (k Keeper) ApplyPendingConsKeyRotations(ctx context.Context, powerReduction } 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 { @@ -193,7 +212,7 @@ func (k Keeper) PruneMaturedConsKeyRotations(ctx context.Context) (err error) { if err := store.Delete(types.GetValidatorConsKeyRotationKey(valAddr)); err != nil { return err } - if err := store.Delete(types.GetRotatedConsAddrIndexKey(oldConsAddr)); err != nil { + if err := store.Delete(types.GetRotationLockedConsAddrIndexKey(oldConsAddr)); err != nil { return err } } diff --git a/x/staking/keeper/rotation_test.go b/x/staking/keeper/rotation_test.go index 5b60ed5fa533..5a3f2bd2e5bc 100644 --- a/x/staking/keeper/rotation_test.go +++ b/x/staking/keeper/rotation_test.go @@ -209,7 +209,7 @@ func (s *KeeperTestSuite) TestPruneMaturedConsKeyRotations() { require.NoError(err) require.False(hasPending, "matured per-validator entry should be pruned") - hasCons, err := s.stakingKeeper.HasRotatedConsAddr(s.ctx, e.consAddr) + hasCons, err := s.stakingKeeper.IsConsAddrLockedByRotation(s.ctx, e.consAddr) require.NoError(err) require.False(hasCons, "matured rotated cons addr entry should be pruned") } @@ -223,7 +223,7 @@ func (s *KeeperTestSuite) TestPruneMaturedConsKeyRotations() { require.NoError(err) require.True(hasPending, "future per-validator entry should remain") - hasCons, err := s.stakingKeeper.HasRotatedConsAddr(s.ctx, e.consAddr) + 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/types/keys.go b/x/staking/types/keys.go index 90466a70fc34..9be1e41e877d 100644 --- a/x/staking/types/keys.go +++ b/x/staking/types/keys.go @@ -75,16 +75,17 @@ var ( // by the ConsKeyRotationQueueKey). ValidatorConsKeyRotationKey = []byte{0x92} // prefix for a validator's pending consensus key rotation, keyed by valAddr - // RotatedConsAddrIndexKey allows us to lookup what an old consensus key - // has changed to. This allows us to ensure that a validator does not - // rotate to a consensus key that was previously used within the current - // unbondong period (e.g. val 1 changes from key A->B, val 2 cannot change - // from C->A). This lookup also allows slashing/evidence handling to - // associate an infraction on an old consensus key to the new consensus - // key. This is pruned when the key rotation falls out of the current - // unbonding period in the end blocker (determined by the - // ConsKeyRotationQueueKey). - RotatedConsAddrIndexKey = []byte{0x93} // prefix for the previously rotated consensus address lookup + // 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. @@ -506,9 +507,10 @@ func GetValidatorConsKeyRotationKey(valAddr sdk.ValAddress) []byte { return append(ValidatorConsKeyRotationKey, address.MustLengthPrefix(valAddr)...) } -// GetRotatedConsAddrIndexKey returns the lookup key for a previously rotated consensus address. -func GetRotatedConsAddrIndexKey(oldConsAddr sdk.ConsAddress) []byte { - return append(RotatedConsAddrIndexKey, address.MustLengthPrefix(oldConsAddr)...) +// 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 diff --git a/x/staking/types/keys_test.go b/x/staking/types/keys_test.go index e1dfd8ccc9cb..48a52d504eb8 100644 --- a/x/staking/types/keys_test.go +++ b/x/staking/types/keys_test.go @@ -247,7 +247,7 @@ func TestGetValidatorConsKeyRotationKey(t *testing.T) { } } -func TestGetRotatedConsAddrIndexKey(t *testing.T) { +func TestGetRotationLockedConsAddrIndexKey(t *testing.T) { tests := []struct { consAddr sdk.ConsAddress wantHex string @@ -257,7 +257,7 @@ func TestGetRotatedConsAddrIndexKey(t *testing.T) { {sdk.ConsAddress(keysAddr3), "93143ab62f0d93849be495e21e3e9013a517038f45bd"}, } for i, tt := range tests { - got := hex.EncodeToString(types.GetRotatedConsAddrIndexKey(tt.consAddr)) + got := hex.EncodeToString(types.GetRotationLockedConsAddrIndexKey(tt.consAddr)) require.Equal(t, tt.wantHex, got, "Keys did not match on test case %d", i) } } From d73130780777db6ca7f90d9502c4858ed2e806fd Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Fri, 22 May 2026 12:20:45 -0400 Subject: [PATCH 18/19] dont delete during iteration --- x/staking/keeper/rotation.go | 85 ++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/x/staking/keeper/rotation.go b/x/staking/keeper/rotation.go index 24a1897ca83d..8c47d893581b 100644 --- a/x/staking/keeper/rotation.go +++ b/x/staking/keeper/rotation.go @@ -1,6 +1,7 @@ package keeper import ( + "bytes" "context" "errors" "time" @@ -182,54 +183,87 @@ func (k Keeper) ApplyConsKeyRotation(ctx context.Context, validator types.Valida // 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) (err error) { +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 err + return nil, err } defer func() { err = errors.Join(err, iterator.Close()) }() + // TODO: migrate ValidatorSigningInfo from oldConsAddr to newConsAddr for ; iterator.Valid(); iterator.Next() { - // TODO: migrate ValidatorSigningInfo from oldConsAddr to newConsAddr - _, valAddr, err := types.ParseConsKeyRotationQueueKey(iterator.Key()) - if err != nil { - return err + maturity, valAddr, perr := types.ParseConsKeyRotationQueueKey(iterator.Key()) + if perr != nil { + return nil, perr } oldConsAddr := sdk.ConsAddress(iterator.Value()) - if err := store.Delete(iterator.Key()); err != nil { - return err - } - if err := store.Delete(types.GetValidatorConsKeyRotationKey(valAddr)); err != nil { - return err - } - if err := store.Delete(types.GetRotationLockedConsAddrIndexKey(oldConsAddr)); err != nil { - return err - } + keys = append(keys, + types.GetConsKeyRotationQueueKey(maturity, valAddr), + types.GetValidatorConsKeyRotationKey(valAddr), + types.GetRotationLockedConsAddrIndexKey(oldConsAddr), + ) } - - return nil + return keys, nil } // IterateUnappliedConsKeyRotations walks every rotation queued by the msg -// server that the end blocker has not yet applied, in valAddr sorted order. +// 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, -) (err 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 err + return nil, err } defer func() { err = errors.Join(err, iterator.Close()) @@ -237,16 +271,13 @@ func (k Keeper) IterateUnappliedConsKeyRotations( for ; iterator.Valid(); iterator.Next() { key := iterator.Key() - valAddr := sdk.ValAddress(key[len(types.UnappliedConsKeyRotationKey)+1:]) + 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 err - } - - if err := cb(valAddr, newPubKey); err != nil { - return err + return nil, err } + rotations = append(rotations, unappliedConsKeyRotation{valAddr: valAddr, newPubKey: newPubKey}) } - return nil + return rotations, nil } From 738906e3e2b4a5ee6bfc5a76495d70546c88596e Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Fri, 22 May 2026 13:11:19 -0400 Subject: [PATCH 19/19] remove unused --- x/staking/keeper/msg_server.go | 11 +++++------ x/staking/keeper/msg_server_test.go | 4 ++-- x/staking/keeper/rotation.go | 6 +----- x/staking/keeper/rotation_test.go | 4 ++-- x/staking/keeper/val_state_change_test.go | 2 +- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/x/staking/keeper/msg_server.go b/x/staking/keeper/msg_server.go index 2cfcdebaa5e1..d242c2a54f41 100644 --- a/x/staking/keeper/msg_server.go +++ b/x/staking/keeper/msg_server.go @@ -663,12 +663,12 @@ func (k msgServer) RotateConsPubKey(ctx context.Context, msg *types.MsgRotateCon return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidType, "expecting cryptotypes.PubKey for validator's current key, got %T", validator.ConsensusPubkey.GetCachedValue()) } - // enforce the per validator rotation limit inside the unbonding window - pending, err := k.HasConsKeyRotationInUnbondingWindow(ctx, valAddr) + // enforce the one rotation per validator limit inside the unbonding window + hasRotated, err := k.HasConsKeyRotationInUnbondingWindow(ctx, valAddr) if err != nil { return nil, err } - if pending { + if hasRotated { return nil, types.ErrExceedingMaxConsPubKeyRotations } @@ -676,8 +676,7 @@ func (k msgServer) RotateConsPubKey(ctx context.Context, msg *types.MsgRotateCon // 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. - fee := types.DefaultKeyRotationFee - feeCoins := sdk.NewCoins(fee) + feeCoins := sdk.NewCoins(types.DefaultKeyRotationFee) if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sdk.AccAddress(valAddr), types.KeyRotationFeePoolName, feeCoins); err != nil { return nil, err } @@ -686,7 +685,7 @@ func (k msgServer) RotateConsPubKey(ctx context.Context, msg *types.MsgRotateCon } // record the key rotation in the store - if err := k.SetConsKeyRotation(ctx, valAddr, oldPk, newPk, fee); err != nil { + if err := k.SetConsKeyRotation(ctx, valAddr, oldPk, newPk); err != nil { return nil, err } diff --git a/x/staking/keeper/msg_server_test.go b/x/staking/keeper/msg_server_test.go index 282f36048a9b..82432b680962 100644 --- a/x/staking/keeper/msg_server_test.go +++ b/x/staking/keeper/msg_server_test.go @@ -1233,7 +1233,7 @@ func (s *KeeperTestSuite) TestMsgRotateConsPubKey() { 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(), stakingtypes.DefaultKeyRotationFee)) + require.NoError(s.stakingKeeper.SetConsKeyRotation(s.ctx, dummy, oldPubKey, ed25519.GenPrivKey().PubKey())) return &stakingtypes.MsgRotateConsPubKey{ ValidatorAddress: valAddr.String(), NewPubkey: newAny(oldPubKey), @@ -1247,7 +1247,7 @@ func (s *KeeperTestSuite) TestMsgRotateConsPubKey() { 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, stakingtypes.DefaultKeyRotationFee)) + require.NoError(s.stakingKeeper.SetConsKeyRotation(s.ctx, dummy, ed25519.GenPrivKey().PubKey(), targetPubKey)) return &stakingtypes.MsgRotateConsPubKey{ ValidatorAddress: valAddr.String(), NewPubkey: newAny(targetPubKey), diff --git a/x/staking/keeper/rotation.go b/x/staking/keeper/rotation.go index 8c47d893581b..d845ee989813 100644 --- a/x/staking/keeper/rotation.go +++ b/x/staking/keeper/rotation.go @@ -17,10 +17,6 @@ import ( "github.com/cosmos/cosmos-sdk/x/staking/types" ) -// MaxConsKeyRotations is the maximum number of pending consensus key rotations -// a validator may have inside the unbonding window. -const MaxConsKeyRotations = 1 - // 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) { @@ -44,7 +40,7 @@ func (k Keeper) HasConsKeyRotationQueueEntry(ctx context.Context, maturity time. // 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, fee sdk.Coin) error { +func (k Keeper) SetConsKeyRotation(ctx context.Context, valAddr sdk.ValAddress, oldPubKey, newPubKey cryptotypes.PubKey) error { sdkCtx := sdk.UnwrapSDKContext(ctx) unbondingTime, err := k.UnbondingTime(ctx) diff --git a/x/staking/keeper/rotation_test.go b/x/staking/keeper/rotation_test.go index 5a3f2bd2e5bc..0a42f8deaf5c 100644 --- a/x/staking/keeper/rotation_test.go +++ b/x/staking/keeper/rotation_test.go @@ -116,7 +116,7 @@ func (s *KeeperTestSuite) TestIterateUnappliedConsKeyRotations() { newPk: ed25519.GenPrivKey().PubKey(), } oldPk := ed25519.GenPrivKey().PubKey() - require.NoError(s.stakingKeeper.SetConsKeyRotation(s.ctx, seeded[i].valAddr, oldPk, seeded[i].newPk, stakingtypes.DefaultKeyRotationFee)) + require.NoError(s.stakingKeeper.SetConsKeyRotation(s.ctx, seeded[i].valAddr, oldPk, seeded[i].newPk)) } var observed []entry @@ -163,7 +163,7 @@ func (s *KeeperTestSuite) TestPruneMaturedConsKeyRotations() { 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, stakingtypes.DefaultKeyRotationFee)) + require.NoError(s.stakingKeeper.SetConsKeyRotation(s.ctx, valAddr, oldPk, newPk)) return rec{valAddr, sdk.ConsAddress(oldPk.Address()), maturity} } diff --git a/x/staking/keeper/val_state_change_test.go b/x/staking/keeper/val_state_change_test.go index 99139c8cec37..72e9ac0e8574 100644 --- a/x/staking/keeper/val_state_change_test.go +++ b/x/staking/keeper/val_state_change_test.go @@ -35,7 +35,7 @@ func (s *KeeperTestSuite) TestApplyAndReturnValidatorSetUpdatesWithKeyRotation() // queue a key rotation for the same validator newPk := ed25519.GenPrivKey().PubKey() - require.NoError(s.stakingKeeper.SetConsKeyRotation(s.ctx, valAddr, oldPk, newPk, stakingtypes.DefaultKeyRotationFee)) + require.NoError(s.stakingKeeper.SetConsKeyRotation(s.ctx, valAddr, oldPk, newPk)) updates, err := s.stakingKeeper.ApplyAndReturnValidatorSetUpdates(s.ctx) require.NoError(err)