From 165e61270eeb20bb73e4b53481734a53972b0251 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 22 Jun 2022 11:18:13 -0400 Subject: [PATCH] feat(x/staking): add app wiring support for StakingHooks (#12291) ## Description Ref #12036 Depends on app wiring for x/distribution getting merged first (#12292) --- ### Author Checklist *All items are required. Please add a note to the item if the item is not applicable and please add links to any relevant follow up issues.* I have... - [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] added `!` to the type prefix if API or client breaking change - [ ] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#pr-targeting)) - [ ] provided a link to the relevant issue or specification - [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/main/docs/building-modules) - [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#testing) - [ ] added a changelog entry to `CHANGELOG.md` - [ ] included comments for [documenting Go code](https://blog.golang.org/godoc) - [ ] updated the relevant documentation or specification - [ ] reviewed "Files changed" and left comments if necessary - [ ] confirmed all CI checks have passed ### Reviewers Checklist *All items are required. Please add a note if the item is not applicable and please add your handle next to the items reviewed if you only reviewed selected items.* I have... - [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] confirmed `!` in the type prefix if API or client breaking change - [ ] confirmed all author checklist items have been addressed - [ ] reviewed state machine logic - [ ] reviewed API design and naming - [ ] reviewed documentation is accurate - [ ] reviewed tests and test coverage - [ ] manually tested (if applicable) --- api/cosmos/staking/module/v1/module.pulsar.go | 178 ++++++++++++++++-- core/appconfig/config.go | 5 + core/appmodule/option.go | 18 ++ core/internal/registry.go | 2 + depinject/config.go | 39 ++++ depinject/container.go | 40 +++- depinject/features/invoke.feature | 30 +++ depinject/invoke_test.go | 71 +++++++ proto/cosmos/staking/module/v1/module.proto | 7 +- simapp/app.go | 4 - x/distribution/module.go | 8 +- x/evidence/keeper/keeper_test.go | 5 - x/slashing/module.go | 16 +- x/staking/module.go | 50 ++++- x/staking/types/expected_keepers.go | 7 + 15 files changed, 437 insertions(+), 43 deletions(-) create mode 100644 depinject/features/invoke.feature create mode 100644 depinject/invoke_test.go diff --git a/api/cosmos/staking/module/v1/module.pulsar.go b/api/cosmos/staking/module/v1/module.pulsar.go index f1e7eedfa0d6..51eb8445b087 100644 --- a/api/cosmos/staking/module/v1/module.pulsar.go +++ b/api/cosmos/staking/module/v1/module.pulsar.go @@ -13,13 +13,61 @@ import ( sync "sync" ) +var _ protoreflect.List = (*_Module_1_list)(nil) + +type _Module_1_list struct { + list *[]string +} + +func (x *_Module_1_list) Len() int { + if x.list == nil { + return 0 + } + return len(*x.list) +} + +func (x *_Module_1_list) Get(i int) protoreflect.Value { + return protoreflect.ValueOfString((*x.list)[i]) +} + +func (x *_Module_1_list) Set(i int, value protoreflect.Value) { + valueUnwrapped := value.String() + concreteValue := valueUnwrapped + (*x.list)[i] = concreteValue +} + +func (x *_Module_1_list) Append(value protoreflect.Value) { + valueUnwrapped := value.String() + concreteValue := valueUnwrapped + *x.list = append(*x.list, concreteValue) +} + +func (x *_Module_1_list) AppendMutable() protoreflect.Value { + panic(fmt.Errorf("AppendMutable can not be called on message Module at list field HooksOrder as it is not of Message kind")) +} + +func (x *_Module_1_list) Truncate(n int) { + *x.list = (*x.list)[:n] +} + +func (x *_Module_1_list) NewElement() protoreflect.Value { + v := "" + return protoreflect.ValueOfString(v) +} + +func (x *_Module_1_list) IsValid() bool { + return x.list != nil +} + var ( - md_Module protoreflect.MessageDescriptor + md_Module protoreflect.MessageDescriptor + fd_Module_hooks_order protoreflect.FieldDescriptor ) func init() { file_cosmos_staking_module_v1_module_proto_init() md_Module = File_cosmos_staking_module_v1_module_proto.Messages().ByName("Module") + fd_Module_hooks_order = md_Module.Fields().ByName("hooks_order") } var _ protoreflect.Message = (*fastReflection_Module)(nil) @@ -87,6 +135,12 @@ func (x *fastReflection_Module) Interface() protoreflect.ProtoMessage { // While iterating, mutating operations may only be performed // on the current field descriptor. func (x *fastReflection_Module) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { + if len(x.HooksOrder) != 0 { + value := protoreflect.ValueOfList(&_Module_1_list{list: &x.HooksOrder}) + if !f(fd_Module_hooks_order, value) { + return + } + } } // Has reports whether a field is populated. @@ -102,6 +156,8 @@ func (x *fastReflection_Module) Range(f func(protoreflect.FieldDescriptor, proto // a repeated field is populated if it is non-empty. func (x *fastReflection_Module) Has(fd protoreflect.FieldDescriptor) bool { switch fd.FullName() { + case "cosmos.staking.module.v1.Module.hooks_order": + return len(x.HooksOrder) != 0 default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.staking.module.v1.Module")) @@ -118,6 +174,8 @@ func (x *fastReflection_Module) Has(fd protoreflect.FieldDescriptor) bool { // Clear is a mutating operation and unsafe for concurrent use. func (x *fastReflection_Module) Clear(fd protoreflect.FieldDescriptor) { switch fd.FullName() { + case "cosmos.staking.module.v1.Module.hooks_order": + x.HooksOrder = nil default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.staking.module.v1.Module")) @@ -134,6 +192,12 @@ func (x *fastReflection_Module) Clear(fd protoreflect.FieldDescriptor) { // of the value; to obtain a mutable reference, use Mutable. func (x *fastReflection_Module) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { switch descriptor.FullName() { + case "cosmos.staking.module.v1.Module.hooks_order": + if len(x.HooksOrder) == 0 { + return protoreflect.ValueOfList(&_Module_1_list{}) + } + listValue := &_Module_1_list{list: &x.HooksOrder} + return protoreflect.ValueOfList(listValue) default: if descriptor.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.staking.module.v1.Module")) @@ -154,6 +218,10 @@ func (x *fastReflection_Module) Get(descriptor protoreflect.FieldDescriptor) pro // Set is a mutating operation and unsafe for concurrent use. func (x *fastReflection_Module) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { switch fd.FullName() { + case "cosmos.staking.module.v1.Module.hooks_order": + lv := value.List() + clv := lv.(*_Module_1_list) + x.HooksOrder = *clv.list default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.staking.module.v1.Module")) @@ -174,6 +242,12 @@ func (x *fastReflection_Module) Set(fd protoreflect.FieldDescriptor, value proto // Mutable is a mutating operation and unsafe for concurrent use. func (x *fastReflection_Module) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { + case "cosmos.staking.module.v1.Module.hooks_order": + if x.HooksOrder == nil { + x.HooksOrder = []string{} + } + value := &_Module_1_list{list: &x.HooksOrder} + return protoreflect.ValueOfList(value) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.staking.module.v1.Module")) @@ -187,6 +261,9 @@ func (x *fastReflection_Module) Mutable(fd protoreflect.FieldDescriptor) protore // For lists, maps, and messages, this returns a new, empty, mutable value. func (x *fastReflection_Module) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { + case "cosmos.staking.module.v1.Module.hooks_order": + list := []string{} + return protoreflect.ValueOfList(&_Module_1_list{list: &list}) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.staking.module.v1.Module")) @@ -256,6 +333,12 @@ func (x *fastReflection_Module) ProtoMethods() *protoiface.Methods { var n int var l int _ = l + if len(x.HooksOrder) > 0 { + for _, s := range x.HooksOrder { + l = len(s) + n += 1 + l + runtime.Sov(uint64(l)) + } + } if x.unknownFields != nil { n += len(x.unknownFields) } @@ -285,6 +368,15 @@ func (x *fastReflection_Module) ProtoMethods() *protoiface.Methods { i -= len(x.unknownFields) copy(dAtA[i:], x.unknownFields) } + if len(x.HooksOrder) > 0 { + for iNdEx := len(x.HooksOrder) - 1; iNdEx >= 0; iNdEx-- { + i -= len(x.HooksOrder[iNdEx]) + copy(dAtA[i:], x.HooksOrder[iNdEx]) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.HooksOrder[iNdEx]))) + i-- + dAtA[i] = 0xa + } + } if input.Buf != nil { input.Buf = append(input.Buf, dAtA...) } else { @@ -334,6 +426,38 @@ func (x *fastReflection_Module) ProtoMethods() *protoiface.Methods { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: Module: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { + case 1: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field HooksOrder", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.HooksOrder = append(x.HooksOrder, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := runtime.Skip(dAtA[iNdEx:]) @@ -387,6 +511,11 @@ type Module struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + + // hooks_order specifies the order of staking hooks and should be a list + // of module names which provide a staking hooks instance. If no order is + // provided, then hooks will be applied in alphabetical order of module names. + HooksOrder []string `protobuf:"bytes,1,rep,name=hooks_order,json=hooksOrder,proto3" json:"hooks_order,omitempty"` } func (x *Module) Reset() { @@ -409,6 +538,13 @@ func (*Module) Descriptor() ([]byte, []int) { return file_cosmos_staking_module_v1_module_proto_rawDescGZIP(), []int{0} } +func (x *Module) GetHooksOrder() []string { + if x != nil { + return x.HooksOrder + } + return nil +} + var File_cosmos_staking_module_v1_module_proto protoreflect.FileDescriptor var file_cosmos_staking_module_v1_module_proto_rawDesc = []byte{ @@ -418,25 +554,27 @@ var file_cosmos_staking_module_v1_module_proto_rawDesc = []byte{ 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x20, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x22, 0x38, 0x0a, 0x06, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x3a, 0x2e, 0xba, - 0xc0, 0x96, 0xda, 0x01, 0x28, 0x0a, 0x26, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2d, - 0x73, 0x64, 0x6b, 0x2f, 0x78, 0x2f, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x42, 0xe2, 0x01, - 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x73, 0x74, 0x61, - 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x0b, - 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x32, 0x63, - 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2f, - 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x2f, 0x6d, - 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x76, - 0x31, 0xa2, 0x02, 0x03, 0x43, 0x53, 0x4d, 0xaa, 0x02, 0x18, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, - 0x2e, 0x53, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, - 0x56, 0x31, 0xca, 0x02, 0x18, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x53, 0x74, 0x61, 0x6b, - 0x69, 0x6e, 0x67, 0x5c, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x24, - 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x53, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x5c, 0x4d, - 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1b, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x53, - 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x3a, 0x3a, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x3a, 0x3a, - 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6f, 0x74, 0x6f, 0x22, 0x59, 0x0a, 0x06, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x1f, 0x0a, + 0x0b, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0a, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x3a, 0x2e, + 0xba, 0xc0, 0x96, 0xda, 0x01, 0x28, 0x0a, 0x26, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, + 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x78, 0x2f, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x42, 0xe2, + 0x01, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x73, 0x74, + 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x42, + 0x0b, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x32, + 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x2f, + 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, + 0x76, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x53, 0x4d, 0xaa, 0x02, 0x18, 0x43, 0x6f, 0x73, 0x6d, 0x6f, + 0x73, 0x2e, 0x53, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, + 0x2e, 0x56, 0x31, 0xca, 0x02, 0x18, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x53, 0x74, 0x61, + 0x6b, 0x69, 0x6e, 0x67, 0x5c, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0xe2, 0x02, + 0x24, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x53, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x5c, + 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1b, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, + 0x53, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x3a, 0x3a, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x3a, + 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/core/appconfig/config.go b/core/appconfig/config.go index 6aabcfcec957..97553d8c3789 100644 --- a/core/appconfig/config.go +++ b/core/appconfig/config.go @@ -13,6 +13,7 @@ import ( "sigs.k8s.io/yaml" appv1alpha1 "cosmossdk.io/api/cosmos/app/v1alpha1" + "github.com/cosmos/cosmos-sdk/depinject" "cosmossdk.io/core/internal" @@ -95,6 +96,10 @@ func Compose(appConfig *appv1alpha1.Config) depinject.Config { for _, provider := range init.Providers { opts = append(opts, depinject.ProvideInModule(module.Name, provider)) } + + for _, invoker := range init.Invokers { + opts = append(opts, depinject.InvokeInModule(module.Name, invoker)) + } } return depinject.Configs(opts...) diff --git a/core/appmodule/option.go b/core/appmodule/option.go index 98c5518afba7..53121e703ca5 100644 --- a/core/appmodule/option.go +++ b/core/appmodule/option.go @@ -32,3 +32,21 @@ func Provide(providers ...interface{}) Option { return nil }) } + +// Invoke registers invokers to run with depinject. Each invoker will be called +// at the end of dependency graph configuration in the order in which it was defined. Invokers may not define output +// parameters, although they may return an error, and all of their input parameters will be marked as optional so that +// invokers impose no additional constraints on the dependency graph. Invoker functions should nil-check all inputs. +func Invoke(invokers ...interface{}) Option { + return funcOption(func(initializer *internal.ModuleInitializer) error { + for _, invoker := range invokers { + desc, err := depinject.ExtractProviderDescriptor(invoker) + if err != nil { + return err + } + + initializer.Invokers = append(initializer.Invokers, desc) + } + return nil + }) +} diff --git a/core/internal/registry.go b/core/internal/registry.go index 0b8d5804d557..49693f8e8322 100644 --- a/core/internal/registry.go +++ b/core/internal/registry.go @@ -8,6 +8,7 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" appv1alpha1 "cosmossdk.io/api/cosmos/app/v1alpha1" + "github.com/cosmos/cosmos-sdk/depinject" ) @@ -21,6 +22,7 @@ type ModuleInitializer struct { ConfigProtoMessage proto.Message Error error Providers []depinject.ProviderDescriptor + Invokers []depinject.ProviderDescriptor } // ModulesByProtoMessageName should be used to retrieve modules by their protobuf name. diff --git a/depinject/config.go b/depinject/config.go index 664ad0e1c460..54deb9e8d2f2 100644 --- a/depinject/config.go +++ b/depinject/config.go @@ -48,6 +48,45 @@ func provide(ctr *container, key *moduleKey, providers []interface{}) error { return nil } +// Invoke defines a container configuration which registers the provided invoker functions. Each invoker will be called +// at the end of dependency graph configuration in the order in which it was defined. Invokers may not define output +// parameters, although they may return an error, and all of their input parameters will be marked as optional so that +// invokers impose no additional constraints on the dependency graph. Invoker functions should nil-check all inputs. +func Invoke(invokers ...interface{}) Config { + return containerConfig(func(ctr *container) error { + return invoke(ctr, nil, invokers) + }) +} + +// InvokeInModule defines a container configuration which registers the provided invoker functions to run in the +// provided module scope. Each invoker will be called +// at the end of dependency graph configuration in the order in which it was defined. Invokers may not define output +// parameters, although they may return an error, and all of their input parameters will be marked as optional so that +// invokers impose no additional constraints on the dependency graph. Invoker functions should nil-check all inputs. +func InvokeInModule(moduleName string, invokers ...interface{}) Config { + return containerConfig(func(ctr *container) error { + if moduleName == "" { + return errors.Errorf("expected non-empty module name") + } + + return invoke(ctr, ctr.createOrGetModuleKey(moduleName), invokers) + }) +} + +func invoke(ctr *container, key *moduleKey, invokers []interface{}) error { + for _, c := range invokers { + rc, err := ExtractProviderDescriptor(c) + if err != nil { + return errors.WithStack(err) + } + err = ctr.addInvoker(&rc, key) + if err != nil { + return err + } + } + return nil +} + // BindInterface defines a container configuration for an explicit interface binding of inTypeName to outTypeName // in global scope. The example below demonstrates a configuration where the container always provides a Canvasback // instance when an interface of type Duck is requested as an input. diff --git a/depinject/container.go b/depinject/container.go index 91a95224e2d4..706a7573d171 100644 --- a/depinject/container.go +++ b/depinject/container.go @@ -3,9 +3,11 @@ package depinject import ( "bytes" "fmt" - "github.com/cosmos/cosmos-sdk/depinject/internal/graphviz" - "github.com/pkg/errors" "reflect" + + "github.com/pkg/errors" + + "github.com/cosmos/cosmos-sdk/depinject/internal/graphviz" ) type container struct { @@ -13,6 +15,7 @@ type container struct { resolvers map[string]resolver interfaceBindings map[string]interfaceBinding + invokers []invoker moduleKeys map[string]*moduleKey @@ -21,6 +24,11 @@ type container struct { callerMap map[Location]bool } +type invoker struct { + fn *ProviderDescriptor + modKey *moduleKey +} + type resolveFrame struct { loc Location typ reflect.Type @@ -353,6 +361,26 @@ func (c *container) supply(value reflect.Value, location Location) error { return nil } +func (c *container) addInvoker(provider *ProviderDescriptor, key *moduleKey) error { + // make sure there are no outputs + if len(provider.Outputs) > 0 { + return fmt.Errorf("invoker function %s should not return any outputs", provider.Location) + } + + // make all inputs optional + for i, input := range provider.Inputs { + input.Optional = true + provider.Inputs[i] = input + } + + c.invokers = append(c.invokers, invoker{ + fn: provider, + modKey: key, + }) + + return nil +} + func (c *container) resolve(in ProviderInput, moduleKey *moduleKey, caller Location) (reflect.Value, error) { c.resolveStack = append(c.resolveStack, resolveFrame{loc: caller, typ: in.Type}) @@ -462,6 +490,14 @@ func (c *container) build(loc Location, outputs ...interface{}) error { return err } c.logf("Done building container") + c.logf("Calling invokers") + for _, inv := range c.invokers { + _, err := c.call(inv.fn, inv.modKey) + if err != nil { + return err + } + } + c.logf("Done calling invokers") return nil } diff --git a/depinject/features/invoke.feature b/depinject/features/invoke.feature new file mode 100644 index 000000000000..7f4e9ee1836d --- /dev/null +++ b/depinject/features/invoke.feature @@ -0,0 +1,30 @@ +Feature: invokers + + Invokers are functions the will always get called, have strictly optional + dependencies and no return outputs (other than error). + + Background: + + Rule: invokers get called even if their dependencies can't be resolved + Example: no providers + Given an invoker requesting an int and string pointer + When the container is built + Then the invoker will get the int parameter set to 0 + And the invoker will get the string pointer parameter set to nil + + Rule: invokers get called with dependencies if they are provided + Example: int and string pointer providers + Given an invoker requesting an int and string pointer + And an int provider returning 5 + And a string pointer provider pointing to "foo" + When the container is built + Then the invoker will get the int parameter set to 5 + And the invoker will get the string pointer parameter set to "foo" + + Rule: invokers get module scoped dependencies + Example: module-scoped int + Given an invoker requesting an int and string pointer run in module "foo" + And a module-scoped int provider which returns the length of the module name + When the container is built + Then the invoker will get the int parameter set to 3 + And the invoker will get the string pointer parameter set to nil diff --git a/depinject/invoke_test.go b/depinject/invoke_test.go new file mode 100644 index 000000000000..5d7822d4c86f --- /dev/null +++ b/depinject/invoke_test.go @@ -0,0 +1,71 @@ +package depinject_test + +import ( + "testing" + + "github.com/regen-network/gocuke" + "gotest.tools/v3/assert" + + "github.com/cosmos/cosmos-sdk/depinject" +) + +func TestInvoke(t *testing.T) { + gocuke.NewRunner(t, &invokeSuite{}). + Path("features/invoke.feature"). + Run() +} + +type invokeSuite struct { + gocuke.TestingT + configs []depinject.Config + i int + sp *string +} + +func (s *invokeSuite) AnInvokerRequestingAnIntAndStringPointer() { + s.configs = append(s.configs, depinject.Invoke(s.intStringPointerInvoker)) +} + +func (s *invokeSuite) intStringPointerInvoker(i int, sp *string) { + s.i = i + s.sp = sp +} + +func (s *invokeSuite) TheContainerIsBuilt() { + assert.NilError(s, depinject.Inject(depinject.Configs(s.configs...))) +} + +func (s *invokeSuite) TheInvokerWillGetTheIntParameterSetTo(a int64) { + assert.Equal(s, int(a), s.i) +} + +func (s *invokeSuite) TheInvokerWillGetTheStringPointerParameterSetToNil() { + if s.sp != nil { + s.Fatalf("expected a nil string pointer, got %s", *s.sp) + } +} + +func (s *invokeSuite) AnIntProviderReturning(a int64) { + s.configs = append(s.configs, depinject.Provide(func() int { return int(a) })) +} + +func (s *invokeSuite) AStringPointerProviderPointingTo(a string) { + s.configs = append(s.configs, depinject.Provide(func() *string { return &a })) +} + +func (s *invokeSuite) TheInvokerWillGetTheStringPointerParameterSetTo(a string) { + if s.sp == nil { + s.Fatalf("expected a non-nil string pointer") + } + assert.Equal(s, a, *s.sp) +} + +func (s *invokeSuite) AnInvokerRequestingAnIntAndStringPointerRunInModule(a string) { + s.configs = append(s.configs, depinject.InvokeInModule(a, s.intStringPointerInvoker)) +} + +func (s *invokeSuite) AModulescopedIntProviderWhichReturnsTheLengthOfTheModuleName() { + s.configs = append(s.configs, depinject.Provide(func(key depinject.ModuleKey) int { + return len(key.Name()) + })) +} diff --git a/proto/cosmos/staking/module/v1/module.proto b/proto/cosmos/staking/module/v1/module.proto index bf7e1cc48eb8..51a612046afd 100644 --- a/proto/cosmos/staking/module/v1/module.proto +++ b/proto/cosmos/staking/module/v1/module.proto @@ -9,4 +9,9 @@ message Module { option (cosmos.app.v1alpha1.module) = { go_import: "github.com/cosmos/cosmos-sdk/x/staking" }; -} \ No newline at end of file + + // hooks_order specifies the order of staking hooks and should be a list + // of module names which provide a staking hooks instance. If no order is + // provided, then hooks will be applied in alphabetical order of module names. + repeated string hooks_order = 1; +} diff --git a/simapp/app.go b/simapp/app.go index 8beea90b13e8..c0512b9e95cf 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -248,10 +248,6 @@ func NewSimApp( app.GetSubspace(crisistypes.ModuleName), invCheckPeriod, app.BankKeeper, authtypes.FeeCollectorName, ) - app.StakingKeeper.SetHooks( - stakingtypes.NewMultiStakingHooks(app.DistrKeeper.Hooks(), app.SlashingKeeper.Hooks()), - ) - // register the proposal types govRouter := govv1beta1.NewRouter() govRouter.AddRoute(govtypes.RouterKey, govv1beta1.ProposalHandler). diff --git a/x/distribution/module.go b/x/distribution/module.go index c45d3ad49e95..24c783f3b229 100644 --- a/x/distribution/module.go +++ b/x/distribution/module.go @@ -22,6 +22,7 @@ import ( simtypes "github.com/cosmos/cosmos-sdk/types/simulation" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" + staking "github.com/cosmos/cosmos-sdk/x/staking/types" modulev1 "cosmossdk.io/api/cosmos/distribution/module/v1" "github.com/cosmos/cosmos-sdk/x/distribution/client/cli" @@ -238,11 +239,16 @@ type distrOutputs struct { DistrKeeper keeper.Keeper Module runtime.AppModuleWrapper + Hooks staking.StakingHooksWrapper } func provideModule(in distrInputs) distrOutputs { k := keeper.NewKeeper(in.Cdc, in.Key, in.Subspace, in.AccountKeeper, in.BankKeeper, in.StakingKeeper, authtypes.FeeCollectorName) m := NewAppModule(in.Cdc, k, in.AccountKeeper, in.BankKeeper, in.StakingKeeper) - return distrOutputs{DistrKeeper: k, Module: runtime.WrapAppModule(m)} + return distrOutputs{ + DistrKeeper: k, + Module: runtime.WrapAppModule(m), + Hooks: staking.StakingHooksWrapper{StakingHooks: k.Hooks()}, + } } diff --git a/x/evidence/keeper/keeper_test.go b/x/evidence/keeper/keeper_test.go index 2b80e9869ab7..a05bdbf66d03 100644 --- a/x/evidence/keeper/keeper_test.go +++ b/x/evidence/keeper/keeper_test.go @@ -27,7 +27,6 @@ import ( minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) var ( @@ -124,10 +123,6 @@ func (suite *KeeperTestSuite) SetupTest() { suite.accountKeeper.SetAccount(suite.ctx, authtypes.NewBaseAccount(addr, pubkeys[i], uint64(i), 0)) } - suite.stakingKeeper.SetHooks( - stakingtypes.NewMultiStakingHooks(suite.slashingKeeper.Hooks()), - ) - queryHelper := baseapp.NewQueryServerTestHelper(suite.ctx, suite.interfaceRegistry) types.RegisterQueryServer(queryHelper, evidenceKeeper) suite.queryClient = types.NewQueryClient(queryHelper) diff --git a/x/slashing/module.go b/x/slashing/module.go index 92597af883d4..bdec7d7054cf 100644 --- a/x/slashing/module.go +++ b/x/slashing/module.go @@ -8,6 +8,8 @@ import ( modulev1 "cosmossdk.io/api/cosmos/slashing/module/v1" "cosmossdk.io/core/appmodule" + staking "github.com/cosmos/cosmos-sdk/x/staking/types" + gwruntime "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/spf13/cobra" abci "github.com/tendermint/tendermint/abci/types" @@ -199,18 +201,22 @@ type slashingInputs struct { Subspace paramstypes.Subspace } -type outputInputs struct { +type slashingOutputs struct { depinject.Out - Keeper keeper.Keeper `key:"cosmos.slashing.v1.Keeper"` + Keeper keeper.Keeper Module runtime.AppModuleWrapper + Hooks staking.StakingHooksWrapper } -func provideModule(in slashingInputs) outputInputs { - +func provideModule(in slashingInputs) slashingOutputs { k := keeper.NewKeeper(in.Cdc, in.Key, in.StakingKeeper, in.Subspace) m := NewAppModule(in.Cdc, k, in.AccountKeeper, in.BankKeeper, in.StakingKeeper) - return outputInputs{Keeper: k, Module: runtime.WrapAppModule(m)} + return slashingOutputs{ + Keeper: k, + Module: runtime.WrapAppModule(m), + Hooks: staking.StakingHooksWrapper{StakingHooks: k.Hooks()}, + } } // _____________________________________________________________________________________ diff --git a/x/staking/module.go b/x/staking/module.go index 0eed87d059e0..4bbbf2d05cfb 100644 --- a/x/staking/module.go +++ b/x/staking/module.go @@ -5,6 +5,10 @@ import ( "encoding/json" "fmt" "math/rand" + "sort" + + gwruntime "github.com/grpc-ecosystem/grpc-gateway/runtime" + "golang.org/x/exp/maps" modulev1 "cosmossdk.io/api/cosmos/staking/module/v1" "cosmossdk.io/core/appmodule" @@ -12,7 +16,6 @@ import ( "github.com/cosmos/cosmos-sdk/runtime" store "github.com/cosmos/cosmos-sdk/store/types" paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" - gwruntime "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/spf13/cobra" abci "github.com/tendermint/tendermint/abci/types" @@ -182,10 +185,8 @@ func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.Val func init() { appmodule.Register( &modulev1.Module{}, - appmodule.Provide( - provideModuleBasic, - provideModule, - ), + appmodule.Provide(provideModuleBasic, provideModule), + appmodule.Invoke(invokeSetStakingHooks), ) } @@ -218,6 +219,45 @@ func provideModule(in stakingInputs) stakingOutputs { return stakingOutputs{StakingKeeper: k, Module: runtime.WrapAppModule(m)} } +func invokeSetStakingHooks( + config *modulev1.Module, + keeper *keeper.Keeper, + stakingHooks map[string]types.StakingHooksWrapper, +) error { + // all arguments to invokers are optional + if keeper == nil || config == nil { + return nil + } + + modNames := maps.Keys(stakingHooks) + order := config.HooksOrder + if len(order) == 0 { + order = modNames + sort.Strings(order) + } + + if len(order) != len(modNames) { + return fmt.Errorf("len(hooks_order: %v) != len(hooks modules: %v)", order, modNames) + } + + if len(modNames) == 0 { + return nil + } + + var multiHooks types.MultiStakingHooks + for _, modName := range order { + hook, ok := stakingHooks[modName] + if !ok { + return fmt.Errorf("can't find staking hooks for module %s", modName) + } + + multiHooks = append(multiHooks, hook) + } + + keeper.SetHooks(multiHooks) + return nil +} + // AppModuleSimulation functions // GenerateGenesisState creates a randomized GenState of the staking module. diff --git a/x/staking/types/expected_keepers.go b/x/staking/types/expected_keepers.go index 4eb85458179b..871adcba9202 100644 --- a/x/staking/types/expected_keepers.go +++ b/x/staking/types/expected_keepers.go @@ -2,6 +2,7 @@ package types import ( "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) @@ -103,3 +104,9 @@ type StakingHooks interface { AfterDelegationModified(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) error BeforeValidatorSlashed(ctx sdk.Context, valAddr sdk.ValAddress, fraction sdk.Dec) error } + +// StakingHooksWrapper is a wrapper for modules to inject StakingHooks using depinject. +type StakingHooksWrapper struct{ StakingHooks } + +// IsOnePerModuleType implements the depinject.OnePerModuleType interface. +func (StakingHooksWrapper) IsOnePerModuleType() {}