diff --git a/api/poktroll/shared/supplier.pulsar.go b/api/poktroll/shared/supplier.pulsar.go index ac65f36e6..bc0dae789 100644 --- a/api/poktroll/shared/supplier.pulsar.go +++ b/api/poktroll/shared/supplier.pulsar.go @@ -66,10 +66,11 @@ func (x *_Supplier_3_list) IsValid() bool { } var ( - md_Supplier protoreflect.MessageDescriptor - fd_Supplier_address protoreflect.FieldDescriptor - fd_Supplier_stake protoreflect.FieldDescriptor - fd_Supplier_services protoreflect.FieldDescriptor + md_Supplier protoreflect.MessageDescriptor + fd_Supplier_address protoreflect.FieldDescriptor + fd_Supplier_stake protoreflect.FieldDescriptor + fd_Supplier_services protoreflect.FieldDescriptor + fd_Supplier_unstake_session_end_height protoreflect.FieldDescriptor ) func init() { @@ -78,6 +79,7 @@ func init() { fd_Supplier_address = md_Supplier.Fields().ByName("address") fd_Supplier_stake = md_Supplier.Fields().ByName("stake") fd_Supplier_services = md_Supplier.Fields().ByName("services") + fd_Supplier_unstake_session_end_height = md_Supplier.Fields().ByName("unstake_session_end_height") } var _ protoreflect.Message = (*fastReflection_Supplier)(nil) @@ -163,6 +165,12 @@ func (x *fastReflection_Supplier) Range(f func(protoreflect.FieldDescriptor, pro return } } + if x.UnstakeSessionEndHeight != uint64(0) { + value := protoreflect.ValueOfUint64(x.UnstakeSessionEndHeight) + if !f(fd_Supplier_unstake_session_end_height, value) { + return + } + } } // Has reports whether a field is populated. @@ -184,6 +192,8 @@ func (x *fastReflection_Supplier) Has(fd protoreflect.FieldDescriptor) bool { return x.Stake != nil case "poktroll.shared.Supplier.services": return len(x.Services) != 0 + case "poktroll.shared.Supplier.unstake_session_end_height": + return x.UnstakeSessionEndHeight != uint64(0) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: poktroll.shared.Supplier")) @@ -206,6 +216,8 @@ func (x *fastReflection_Supplier) Clear(fd protoreflect.FieldDescriptor) { x.Stake = nil case "poktroll.shared.Supplier.services": x.Services = nil + case "poktroll.shared.Supplier.unstake_session_end_height": + x.UnstakeSessionEndHeight = uint64(0) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: poktroll.shared.Supplier")) @@ -234,6 +246,9 @@ func (x *fastReflection_Supplier) Get(descriptor protoreflect.FieldDescriptor) p } listValue := &_Supplier_3_list{list: &x.Services} return protoreflect.ValueOfList(listValue) + case "poktroll.shared.Supplier.unstake_session_end_height": + value := x.UnstakeSessionEndHeight + return protoreflect.ValueOfUint64(value) default: if descriptor.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: poktroll.shared.Supplier")) @@ -262,6 +277,8 @@ func (x *fastReflection_Supplier) Set(fd protoreflect.FieldDescriptor, value pro lv := value.List() clv := lv.(*_Supplier_3_list) x.Services = *clv.list + case "poktroll.shared.Supplier.unstake_session_end_height": + x.UnstakeSessionEndHeight = value.Uint() default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: poktroll.shared.Supplier")) @@ -295,6 +312,8 @@ func (x *fastReflection_Supplier) Mutable(fd protoreflect.FieldDescriptor) proto return protoreflect.ValueOfList(value) case "poktroll.shared.Supplier.address": panic(fmt.Errorf("field address of message poktroll.shared.Supplier is not mutable")) + case "poktroll.shared.Supplier.unstake_session_end_height": + panic(fmt.Errorf("field unstake_session_end_height of message poktroll.shared.Supplier is not mutable")) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: poktroll.shared.Supplier")) @@ -316,6 +335,8 @@ func (x *fastReflection_Supplier) NewField(fd protoreflect.FieldDescriptor) prot case "poktroll.shared.Supplier.services": list := []*SupplierServiceConfig{} return protoreflect.ValueOfList(&_Supplier_3_list{list: &list}) + case "poktroll.shared.Supplier.unstake_session_end_height": + return protoreflect.ValueOfUint64(uint64(0)) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: poktroll.shared.Supplier")) @@ -399,6 +420,9 @@ func (x *fastReflection_Supplier) ProtoMethods() *protoiface.Methods { n += 1 + l + runtime.Sov(uint64(l)) } } + if x.UnstakeSessionEndHeight != 0 { + n += 1 + runtime.Sov(uint64(x.UnstakeSessionEndHeight)) + } if x.unknownFields != nil { n += len(x.unknownFields) } @@ -428,6 +452,11 @@ func (x *fastReflection_Supplier) ProtoMethods() *protoiface.Methods { i -= len(x.unknownFields) copy(dAtA[i:], x.unknownFields) } + if x.UnstakeSessionEndHeight != 0 { + i = runtime.EncodeVarint(dAtA, i, uint64(x.UnstakeSessionEndHeight)) + i-- + dAtA[i] = 0x20 + } if len(x.Services) > 0 { for iNdEx := len(x.Services) - 1; iNdEx >= 0; iNdEx-- { encoded, err := options.Marshal(x.Services[iNdEx]) @@ -616,6 +645,25 @@ func (x *fastReflection_Supplier) ProtoMethods() *protoiface.Methods { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err } iNdEx = postIndex + case 4: + if wireType != 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field UnstakeSessionEndHeight", wireType) + } + x.UnstakeSessionEndHeight = 0 + 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++ + x.UnstakeSessionEndHeight |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := runtime.Skip(dAtA[iNdEx:]) @@ -673,6 +721,9 @@ type Supplier struct { Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` // The Bech32 address of the supplier using cosmos' ScalarDescriptor to ensure deterministic encoding Stake *v1beta1.Coin `protobuf:"bytes,2,opt,name=stake,proto3" json:"stake,omitempty"` // The total amount of uPOKT the supplier has staked Services []*SupplierServiceConfig `protobuf:"bytes,3,rep,name=services,proto3" json:"services,omitempty"` // The service configs this supplier can support + // The session end height at which an actively unbonding supplier unbonds its stake. + // If the supplier did not unstake, this value will be 0. + UnstakeSessionEndHeight uint64 `protobuf:"varint,4,opt,name=unstake_session_end_height,json=unstakeSessionEndHeight,proto3" json:"unstake_session_end_height,omitempty"` } func (x *Supplier) Reset() { @@ -716,6 +767,13 @@ func (x *Supplier) GetServices() []*SupplierServiceConfig { return nil } +func (x *Supplier) GetUnstakeSessionEndHeight() uint64 { + if x != nil { + return x.UnstakeSessionEndHeight + } + return 0 +} + var File_poktroll_shared_supplier_proto protoreflect.FileDescriptor var file_poktroll_shared_supplier_proto_rawDesc = []byte{ @@ -727,7 +785,7 @@ var file_poktroll_shared_supplier_proto_rawDesc = []byte{ 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1d, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2f, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2f, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb3, 0x01, 0x0a, 0x08, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf0, 0x01, 0x0a, 0x08, 0x53, 0x75, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, 0xb4, 0x2d, 0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x72, @@ -739,17 +797,21 @@ var file_poktroll_shared_supplier_proto_rawDesc = []byte{ 0x26, 0x2e, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2e, 0x53, 0x75, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x42, 0xa3, 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x2e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x42, 0x0d, 0x53, 0x75, 0x70, 0x70, 0x6c, - 0x69, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x20, 0x63, 0x6f, 0x73, 0x6d, - 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x6f, 0x6b, - 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2f, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0xa2, 0x02, 0x03, 0x50, - 0x53, 0x58, 0xaa, 0x02, 0x0f, 0x50, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, 0x53, 0x68, - 0x61, 0x72, 0x65, 0x64, 0xca, 0x02, 0x0f, 0x50, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x5c, - 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0xe2, 0x02, 0x1b, 0x50, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, - 0x6c, 0x5c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x10, 0x50, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x3a, - 0x3a, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x12, 0x3b, 0x0a, 0x1a, 0x75, 0x6e, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x5f, 0x73, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x6e, 0x64, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x17, 0x75, 0x6e, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x42, 0xa3, + 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, + 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x42, 0x0d, 0x53, 0x75, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x72, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x20, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, + 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x2f, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0xa2, 0x02, 0x03, 0x50, 0x53, 0x58, 0xaa, + 0x02, 0x0f, 0x50, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, 0x53, 0x68, 0x61, 0x72, 0x65, + 0x64, 0xca, 0x02, 0x0f, 0x50, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x5c, 0x53, 0x68, 0x61, + 0x72, 0x65, 0x64, 0xe2, 0x02, 0x1b, 0x50, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x5c, 0x53, + 0x68, 0x61, 0x72, 0x65, 0x64, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0xea, 0x02, 0x10, 0x50, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x3a, 0x3a, 0x53, 0x68, + 0x61, 0x72, 0x65, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/e2e/tests/init_test.go b/e2e/tests/init_test.go index 084fa26b9..3220b93e7 100644 --- a/e2e/tests/init_test.go +++ b/e2e/tests/init_test.go @@ -36,6 +36,7 @@ import ( apptypes "github.com/pokt-network/poktroll/x/application/types" prooftypes "github.com/pokt-network/poktroll/x/proof/types" sessiontypes "github.com/pokt-network/poktroll/x/session/types" + shared "github.com/pokt-network/poktroll/x/shared" sharedtypes "github.com/pokt-network/poktroll/x/shared/types" suppliertypes "github.com/pokt-network/poktroll/x/supplier/types" ) @@ -442,6 +443,24 @@ func (s *suite) AModuleEndBlockEventIsBroadcast(module, eventType string) { s.waitForNewBlockEvent(newEventTypeMatchFn(module, eventType)) } +func (s *suite) TheSupplierForAccountIsUnbonding(accName string) { + _, ok := accNameToSupplierMap[accName] + require.True(s, ok, "supplier %s not found", accName) + + s.waitForTxResultEvent(newEventMsgTypeMatchFn("supplier", "UnstakeSupplier")) + + supplier := s.getSupplierInfo(accName) + require.True(s, supplier.IsUnbonding()) +} + +func (s *suite) TheUserWaitsForUnbondingPeriodToFinish(accName string) { + _, ok := accNameToSupplierMap[accName] + require.True(s, ok, "supplier %s not found", accName) + + unbondingHeight := s.getSupplierUnbondingHeight(accName) + s.waitForBlockHeight(unbondingHeight) +} + func (s *suite) getStakedAmount(actorType, accName string) (int, bool) { s.Helper() args := []string{ @@ -566,6 +585,47 @@ func (s *suite) validateAmountChange(prevAmount, currAmount int, expectedAmountC } +// getSupplierInfo returns the supplier information for a given supplier address +func (s *suite) getSupplierInfo(supplierAddr string) *sharedtypes.Supplier { + args := []string{ + "query", + "supplier", + "show-supplier", + accNameToAddrMap[supplierAddr], + "--output=json", + } + + res, err := s.pocketd.RunCommandOnHostWithRetry("", numQueryRetries, args...) + require.NoError(s, err, "error getting supplier %s", supplierAddr) + s.pocketd.result = res + + var resp suppliertypes.QueryGetSupplierResponse + responseBz := []byte(strings.TrimSpace(res.Stdout)) + s.cdc.MustUnmarshalJSON(responseBz, &resp) + return &resp.Supplier +} + +// getSupplierUnbondingHeight returns the height at which the supplier will be unbonded. +func (s *suite) getSupplierUnbondingHeight(accName string) int64 { + supplier := s.getSupplierInfo(accName) + + args := []string{ + "query", + "shared", + "params", + "--output=json", + } + + res, err := s.pocketd.RunCommandOnHostWithRetry("", numQueryRetries, args...) + require.NoError(s, err, "error getting shared module params") + + var resp sharedtypes.QueryParamsResponse + responseBz := []byte(strings.TrimSpace(res.Stdout)) + s.cdc.MustUnmarshalJSON(responseBz, &resp) + unbondingHeight := shared.GetSupplierUnbondingHeight(&resp.Params, supplier) + return unbondingHeight +} + // TODO_IMPROVE: use `sessionId` and `supplierName` since those are the two values // used to create the primary composite key on-chain to uniquely distinguish relays. func relayReferenceKey(appName, supplierName string) string { diff --git a/e2e/tests/session_steps_test.go b/e2e/tests/session_steps_test.go index 0a07c6340..63fdfa1fa 100644 --- a/e2e/tests/session_steps_test.go +++ b/e2e/tests/session_steps_test.go @@ -251,6 +251,35 @@ func (s *suite) waitForNewBlockEvent( } } +// waitForBlockHeight waits for a NewBlock event to be observed whose height is +// greater than or equal to the target height. +func (s *suite) waitForBlockHeight(targetHeight int64) { + ctx, done := context.WithCancel(s.ctx) + + // For each observed event, **asynchronously** check if it is greater than + // or equal to the target height + channel.ForEach[*block.CometNewBlockEvent]( + ctx, s.newBlockEventsReplayClient.EventsSequence(ctx), + func(_ context.Context, newBlockEvent *block.CometNewBlockEvent) { + if newBlockEvent == nil { + return + } + + if newBlockEvent.Data.Value.Block.Header.Height >= targetHeight { + done() + return + } + }, + ) + + select { + case <-time.After(eventTimeout): + s.Fatalf("ERROR: timed out waiting for block height", targetHeight) + case <-ctx.Done(): + s.Log("Success; height detected before timeout.") + } +} + // newEventTypeMatchFn returns a function that matches an event based on its type // field. The type URL is constructed from the given module and eventType arguments // where module is the module name and eventType is the protobuf message type name diff --git a/e2e/tests/stake_app.feature b/e2e/tests/stake_app.feature index b88b437f3..ae311847d 100644 --- a/e2e/tests/stake_app.feature +++ b/e2e/tests/stake_app.feature @@ -10,8 +10,7 @@ Feature: Stake App Namespaces Then the user should be able to see standard output containing "txhash:" And the user should be able to see standard output containing "code: 0" And the pocketd binary should exit without error - # TODO_TECHDEBT(@red-0ne): Wait for an admitted stake event instead of a time based waiting. - And the user should wait for "5" seconds + And the user should wait for the "application" module "StakeApplication" message to be submitted And the "application" for account "app2" is staked with "1000070" uPOKT And the account balance of "app2" should be "1000070" uPOKT "less" than before @@ -23,6 +22,6 @@ Feature: Stake App Namespaces Then the user should be able to see standard output containing "txhash:" And the user should be able to see standard output containing "code: 0" And the pocketd binary should exit without error - And the user should wait for "5" seconds + And the user should wait for the "application" module "UnstakeApplication" message to be submitted And the "application" for account "app2" is not staked And the account balance of "app2" should be "1000070" uPOKT "more" than before \ No newline at end of file diff --git a/e2e/tests/stake_gateway.feature b/e2e/tests/stake_gateway.feature index bbfc5bb9c..654d2d7dd 100644 --- a/e2e/tests/stake_gateway.feature +++ b/e2e/tests/stake_gateway.feature @@ -8,8 +8,7 @@ Feature: Stake Gateway Namespaces Then the user should be able to see standard output containing "txhash:" And the user should be able to see standard output containing "code: 0" And the pocketd binary should exit without error - # TODO_TECHDEBT(@red-0ne): Replace these time-based waits with event listening waits - And the user should wait for "5" seconds + And the user should wait for the "gateway" module "StakeGateway" message to be submitted And the "gateway" for account "gateway2" is staked with "1000070" uPOKT And the account balance of "gateway2" should be "1000070" uPOKT "less" than before @@ -21,6 +20,6 @@ Feature: Stake Gateway Namespaces Then the user should be able to see standard output containing "txhash:" And the user should be able to see standard output containing "code: 0" And the pocketd binary should exit without error - And the user should wait for "5" seconds + And the user should wait for the "gateway" module "UnstakeGateway" message to be submitted And the "gateway" for account "gateway2" is not staked And the account balance of "gateway2" should be "1000070" uPOKT "more" than before \ No newline at end of file diff --git a/e2e/tests/stake_supplier.feature b/e2e/tests/stake_supplier.feature index 52f84989d..8521e8286 100644 --- a/e2e/tests/stake_supplier.feature +++ b/e2e/tests/stake_supplier.feature @@ -10,8 +10,7 @@ Feature: Stake Supplier Namespace Then the user should be able to see standard output containing "txhash:" And the user should be able to see standard output containing "code: 0" And the pocketd binary should exit without error - # TODO_TECHDEBT(@red-0ne): Replace these time-based waits with event listening waits - And the user should wait for "5" seconds + And the user should wait for the "supplier" module "StakeSupplier" message to be submitted And the "supplier" for account "supplier2" is staked with "1000070" uPOKT And the account balance of "supplier2" should be "1000070" uPOKT "less" than before @@ -23,6 +22,7 @@ Feature: Stake Supplier Namespace Then the user should be able to see standard output containing "txhash:" And the user should be able to see standard output containing "code: 0" And the pocketd binary should exit without error - And the user should wait for "5" seconds - And the "supplier" for account "supplier2" is not staked + And the supplier for account "supplier2" is unbonding + When the user waits for "supplier2" unbonding period to finish + Then the "supplier" for account "supplier2" is not staked And the account balance of "supplier2" should be "1000070" uPOKT "more" than before \ No newline at end of file diff --git a/proto/poktroll/shared/supplier.proto b/proto/poktroll/shared/supplier.proto index 5553f5673..aa632c511 100644 --- a/proto/poktroll/shared/supplier.proto +++ b/proto/poktroll/shared/supplier.proto @@ -13,5 +13,8 @@ message Supplier { string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the supplier using cosmos' ScalarDescriptor to ensure deterministic encoding cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the supplier has staked repeated SupplierServiceConfig services = 3; // The service configs this supplier can support + // The session end height at which an actively unbonding supplier unbonds its stake. + // If the supplier did not unstake, this value will be 0. + uint64 unstake_session_end_height = 4; } diff --git a/testutil/integration/app.go b/testutil/integration/app.go index 76852d661..a1b1273e2 100644 --- a/testutil/integration/app.go +++ b/testutil/integration/app.go @@ -338,6 +338,7 @@ func NewCompleteIntegrationApp(t *testing.T) *App { logger, authority.String(), bankKeeper, + sharedKeeper, serviceKeeper, ) supplierModule := supplier.NewAppModule( @@ -458,9 +459,6 @@ func NewCompleteIntegrationApp(t *testing.T) *App { // authtypes.RegisterQueryServer(queryHelper, accountKeeper) sessiontypes.RegisterQueryServer(queryHelper, sessionKeeper) - // Need to go to the next block to finalize the genesis and setup - integrationApp.NextBlock(t) - // Set the default params for all the modules err := sharedKeeper.SetParams(integrationApp.GetSdkCtx(), sharedtypes.DefaultParams()) require.NoError(t, err) @@ -475,6 +473,11 @@ func NewCompleteIntegrationApp(t *testing.T) *App { err = applicationKeeper.SetParams(integrationApp.GetSdkCtx(), apptypes.DefaultParams()) require.NoError(t, err) + // Need to go to the next block to finalize the genesis and setup. + // This has to be after the params are set, as the params are stored in the + // store and need to be committed. + integrationApp.NextBlock(t) + // Prepare default testing fixtures // // Construct a keyring to hold the keypairs for the accounts used in the test. @@ -710,7 +713,10 @@ func (app *App) nextBlockUpdateCtx() { newContext := app.BaseApp.NewUncachedContext(true, header). WithBlockHeader(header). WithHeaderInfo(headerInfo). - WithEventManager(prevCtx.EventManager()) + WithEventManager(prevCtx.EventManager()). + // Pass the multi-store to the new context, otherwise the new context will + // create a new multi-store. + WithMultiStore(prevCtx.MultiStore()) *app.sdkCtx = newContext } diff --git a/testutil/keeper/proof.go b/testutil/keeper/proof.go index 03ea7b4b2..80049be35 100644 --- a/testutil/keeper/proof.go +++ b/testutil/keeper/proof.go @@ -199,6 +199,7 @@ func NewProofModuleKeepers(t testing.TB, opts ...ProofKeepersOpt) (_ *ProofModul log.NewNopLogger(), authority.String(), suppliermocks.NewMockBankKeeper(ctrl), + sharedKeeper, serviceKeeper, ) require.NoError(t, supplierKeeper.SetParams(ctx, suppliertypes.DefaultParams())) diff --git a/testutil/keeper/supplier.go b/testutil/keeper/supplier.go index 6f8efb086..b05168340 100644 --- a/testutil/keeper/supplier.go +++ b/testutil/keeper/supplier.go @@ -21,6 +21,7 @@ import ( "github.com/pokt-network/poktroll/testutil/supplier/mocks" servicekeeper "github.com/pokt-network/poktroll/x/service/keeper" + sharedkeeper "github.com/pokt-network/poktroll/x/shared/keeper" sharedtypes "github.com/pokt-network/poktroll/x/shared/types" "github.com/pokt-network/poktroll/x/supplier/keeper" "github.com/pokt-network/poktroll/x/supplier/types" @@ -36,6 +37,9 @@ func SupplierKeeper(t testing.TB) (keeper.Keeper, context.Context) { stateStore.MountStoreWithDB(storeKey, storetypes.StoreTypeIAVL, db) require.NoError(t, stateStore.LoadLatestVersion()) + logger := log.NewTestLogger(t) + sdkCtx := sdk.NewContext(stateStore, cmtproto.Header{}, false, logger) + registry := codectypes.NewInterfaceRegistry() cdc := codec.NewProtoCodec(registry) authority := authtypes.NewModuleAddress(govtypes.ModuleName) @@ -46,6 +50,15 @@ func SupplierKeeper(t testing.TB) (keeper.Keeper, context.Context) { mockBankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), types.ModuleName, gomock.Any(), gomock.Any()).AnyTimes() mockBankKeeper.EXPECT().SpendableCoins(gomock.Any(), gomock.Any()).AnyTimes() + // Construct a real shared keeper. + sharedKeeper := sharedkeeper.NewKeeper( + cdc, + runtime.NewKVStoreService(storeKey), + logger, + authority.String(), + ) + require.NoError(t, sharedKeeper.SetParams(sdkCtx, sharedtypes.DefaultParams())) + serviceKeeper := servicekeeper.NewKeeper( cdc, runtime.NewKVStoreService(storeKey), @@ -60,13 +73,15 @@ func SupplierKeeper(t testing.TB) (keeper.Keeper, context.Context) { log.NewNopLogger(), authority.String(), mockBankKeeper, + sharedKeeper, serviceKeeper, ) - ctx := sdk.NewContext(stateStore, cmtproto.Header{}, false, log.NewNopLogger()) - // Initialize params - require.NoError(t, k.SetParams(ctx, types.DefaultParams())) + require.NoError(t, k.SetParams(sdkCtx, types.DefaultParams())) + + // Move block height to 1 to get a non zero session end height + ctx := SetBlockHeight(sdkCtx, 1) // Add existing services used in the test. serviceKeeper.SetService(ctx, sharedtypes.Service{Id: "svcId"}) diff --git a/testutil/keeper/tokenomics.go b/testutil/keeper/tokenomics.go index a0b5ffc19..b94562e3b 100644 --- a/testutil/keeper/tokenomics.go +++ b/testutil/keeper/tokenomics.go @@ -308,6 +308,7 @@ func NewTokenomicsModuleKeepers( log.NewNopLogger(), authority.String(), bankKeeper, + sharedKeeper, serviceKeeper, ) require.NoError(t, supplierKeeper.SetParams(ctx, suppliertypes.DefaultParams())) diff --git a/x/session/keeper/session_hydrator.go b/x/session/keeper/session_hydrator.go index 84336ce27..1a74da84b 100644 --- a/x/session/keeper/session_hydrator.go +++ b/x/session/keeper/session_hydrator.go @@ -167,6 +167,13 @@ func (k Keeper) hydrateSessionSuppliers(ctx context.Context, sh *sessionHydrator candidateSuppliers := make([]*sharedtypes.Supplier, 0) for _, s := range suppliers { + // Exclude suppliers that are inactive (i.e. currently unbonding). + // TODO_TECHDEBT(#695): Suppliers that stake mid-session SHOULD NOT be included + // in the current session's suppliers list and must wait until the next one. + if !s.IsActive(sh.sessionHeader.SessionEndBlockHeight) { + continue + } + // NB: Allocate a new heap variable as s is a value and we're appending // to a slice of pointers; otherwise, we'd be appending new pointers to // the same memory address containing the last supplier in the loop. diff --git a/x/shared/supplier.go b/x/shared/supplier.go new file mode 100644 index 000000000..637298f9c --- /dev/null +++ b/x/shared/supplier.go @@ -0,0 +1,17 @@ +package shared + +import ( + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" +) + +// GetSupplierUnbondingHeight returns the height at which the given supplier finishes unbonding. +func GetSupplierUnbondingHeight( + sharedParams *sharedtypes.Params, + supplier *sharedtypes.Supplier, +) int64 { + // TODO_UPNEXT(red-0ne): Add a governance parameter called `supplier_unbonding_period` + // equal to the number of blocks required to unbond. The value should enforce + // (when being updated) to be after proof window close height and should still + // round to the end of the nearest session. + return GetProofWindowCloseHeight(sharedParams, int64(supplier.UnstakeSessionEndHeight)) +} diff --git a/x/shared/types/supplier.go b/x/shared/types/supplier.go new file mode 100644 index 000000000..ac6fe4782 --- /dev/null +++ b/x/shared/types/supplier.go @@ -0,0 +1,23 @@ +package types + +// SupplierNotUnstaking is the value of `unstake_session_end_height` if the +// supplier is not actively in the unbonding period. +const SupplierNotUnstaking uint64 = 0 + +// IsUnbonding returns true if the supplier is actively unbonding. +// It determines if the supplier has submitted an unstake message, in which case +// the supplier has its UnstakeSessionEndHeight set. +func (s *Supplier) IsUnbonding() bool { + return s.UnstakeSessionEndHeight != SupplierNotUnstaking +} + +// IsActive returns whether the supplier is allowed to serve requests at the +// given query height. +// A supplier that has not submitted an unstake message is always active. +// A supplier that has submitted an unstake message is active until the end of +// the session containing the height at which unstake message was submitted. +func (s *Supplier) IsActive(queryHeight int64) bool { + // TODO_UPNEXT(@red-0ne): When introducing a governance parameter for unbonding, this + // will also need to be updated to reflect sessions when the supplier is not active. + return !s.IsUnbonding() || uint64(queryHeight) <= s.UnstakeSessionEndHeight +} diff --git a/x/shared/types/supplier.pb.go b/x/shared/types/supplier.pb.go index 716a0c1ac..3e19ef0ea 100644 --- a/x/shared/types/supplier.pb.go +++ b/x/shared/types/supplier.pb.go @@ -29,6 +29,9 @@ type Supplier struct { Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` Stake *types.Coin `protobuf:"bytes,2,opt,name=stake,proto3" json:"stake,omitempty"` Services []*SupplierServiceConfig `protobuf:"bytes,3,rep,name=services,proto3" json:"services,omitempty"` + // The session end height at which an actively unbonding supplier unbonds its stake. + // If the supplier did not unstake, this value will be 0. + UnstakeSessionEndHeight uint64 `protobuf:"varint,4,opt,name=unstake_session_end_height,json=unstakeSessionEndHeight,proto3" json:"unstake_session_end_height,omitempty"` } func (m *Supplier) Reset() { *m = Supplier{} } @@ -85,6 +88,13 @@ func (m *Supplier) GetServices() []*SupplierServiceConfig { return nil } +func (m *Supplier) GetUnstakeSessionEndHeight() uint64 { + if m != nil { + return m.UnstakeSessionEndHeight + } + return 0 +} + func init() { proto.RegisterType((*Supplier)(nil), "poktroll.shared.Supplier") } @@ -92,26 +102,28 @@ func init() { func init() { proto.RegisterFile("poktroll/shared/supplier.proto", fileDescriptor_4a189b52ba503cf2) } var fileDescriptor_4a189b52ba503cf2 = []byte{ - // 289 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x90, 0xbd, 0x4e, 0xf3, 0x30, - 0x18, 0x85, 0xe3, 0xaf, 0xfa, 0xa0, 0xa4, 0x03, 0x52, 0xc4, 0x90, 0x56, 0xc2, 0x8a, 0x18, 0x50, - 0x96, 0xda, 0x6a, 0xb8, 0x02, 0xd2, 0x89, 0x35, 0xd9, 0x58, 0x50, 0x7e, 0x4c, 0x6a, 0x25, 0xcd, - 0x6b, 0xd9, 0x6e, 0x81, 0xbb, 0xe0, 0x5e, 0xe0, 0x22, 0x18, 0x2b, 0x26, 0x46, 0x94, 0xdc, 0x08, - 0x6a, 0xed, 0x74, 0x80, 0xf1, 0xd5, 0xf3, 0x24, 0xe7, 0xf8, 0xb8, 0x58, 0x40, 0xad, 0x25, 0x34, - 0x0d, 0x55, 0xab, 0x4c, 0xb2, 0x92, 0xaa, 0x8d, 0x10, 0x0d, 0x67, 0x92, 0x08, 0x09, 0x1a, 0xbc, - 0xf3, 0x81, 0x13, 0xc3, 0x67, 0xd3, 0x02, 0xd4, 0x1a, 0xd4, 0xc3, 0x01, 0x53, 0x73, 0x18, 0x77, - 0x86, 0xcd, 0x45, 0xf3, 0x4c, 0x31, 0xba, 0x5d, 0xe4, 0x4c, 0x67, 0x0b, 0x5a, 0x00, 0x6f, 0x2d, - 0xbf, 0xfc, 0x93, 0xc5, 0xe4, 0x96, 0x17, 0xcc, 0xe0, 0xab, 0x37, 0xe4, 0x8e, 0x53, 0x9b, 0xee, - 0x45, 0xee, 0x69, 0x56, 0x96, 0x92, 0x29, 0xe5, 0xa3, 0x00, 0x85, 0x67, 0xb1, 0xff, 0xf9, 0x3e, - 0xbf, 0xb0, 0x71, 0xb7, 0x86, 0xa4, 0x5a, 0xf2, 0xb6, 0x4a, 0x06, 0xd1, 0xa3, 0xee, 0x7f, 0xa5, - 0xb3, 0x9a, 0xf9, 0xff, 0x02, 0x14, 0x4e, 0xa2, 0x29, 0xb1, 0xfa, 0xbe, 0x0f, 0xb1, 0x7d, 0xc8, - 0x12, 0x78, 0x9b, 0x18, 0xcf, 0x8b, 0xdd, 0xb1, 0xad, 0xa0, 0xfc, 0x51, 0x30, 0x0a, 0x27, 0xd1, - 0x35, 0xf9, 0xf5, 0x5e, 0x32, 0x34, 0x4a, 0x8d, 0xb8, 0x84, 0xf6, 0x91, 0x57, 0xc9, 0xf1, 0xbb, - 0xf8, 0xee, 0xa3, 0xc3, 0x68, 0xd7, 0x61, 0xf4, 0xdd, 0x61, 0xf4, 0xda, 0x63, 0x67, 0xd7, 0x63, - 0xe7, 0xab, 0xc7, 0xce, 0x3d, 0xad, 0xb8, 0x5e, 0x6d, 0x72, 0x52, 0xc0, 0x9a, 0xee, 0xff, 0x3a, - 0x6f, 0x99, 0x7e, 0x02, 0x59, 0xd3, 0xe3, 0x0c, 0xcf, 0xc3, 0x10, 0xfa, 0x45, 0x30, 0x95, 0x9f, - 0x1c, 0x76, 0xb8, 0xf9, 0x09, 0x00, 0x00, 0xff, 0xff, 0xa0, 0x1a, 0x95, 0x9c, 0x94, 0x01, 0x00, - 0x00, + // 326 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x90, 0x3f, 0x4e, 0xc3, 0x30, + 0x18, 0xc5, 0x6b, 0x5a, 0xa0, 0xa4, 0x03, 0x52, 0x84, 0x44, 0x5a, 0x09, 0x2b, 0x62, 0x40, 0x59, + 0x6a, 0xab, 0x65, 0x64, 0xa2, 0x15, 0x12, 0xac, 0xc9, 0xc6, 0x12, 0xe5, 0x8f, 0x49, 0xac, 0xb6, + 0x76, 0xe4, 0xcf, 0x2d, 0x70, 0x0b, 0x0e, 0xc3, 0x21, 0x18, 0x2b, 0x26, 0x46, 0xd4, 0x5e, 0x80, + 0x23, 0xa0, 0xc6, 0x4e, 0x07, 0x18, 0x3f, 0xfd, 0x7e, 0xc9, 0x7b, 0x7e, 0x0e, 0xae, 0xe4, 0x4c, + 0x2b, 0x39, 0x9f, 0x53, 0x28, 0x13, 0xc5, 0x72, 0x0a, 0xcb, 0xaa, 0x9a, 0x73, 0xa6, 0x48, 0xa5, + 0xa4, 0x96, 0xee, 0x69, 0xc3, 0x89, 0xe1, 0x83, 0x7e, 0x26, 0x61, 0x21, 0x21, 0xae, 0x31, 0x35, + 0x87, 0x71, 0x07, 0xd8, 0x5c, 0x34, 0x4d, 0x80, 0xd1, 0xd5, 0x28, 0x65, 0x3a, 0x19, 0xd1, 0x4c, + 0x72, 0x61, 0xf9, 0xc5, 0xbf, 0x2c, 0xa6, 0x56, 0x3c, 0x63, 0x06, 0x5f, 0xfe, 0x20, 0xa7, 0x1b, + 0xd9, 0x74, 0x77, 0xec, 0x1c, 0x27, 0x79, 0xae, 0x18, 0x80, 0x87, 0x7c, 0x14, 0x9c, 0x4c, 0xbc, + 0xcf, 0xf7, 0xe1, 0x99, 0x8d, 0xbb, 0x35, 0x24, 0xd2, 0x8a, 0x8b, 0x22, 0x6c, 0x44, 0x97, 0x3a, + 0x87, 0xa0, 0x93, 0x19, 0xf3, 0x0e, 0x7c, 0x14, 0xf4, 0xc6, 0x7d, 0x62, 0xf5, 0x5d, 0x1f, 0x62, + 0xfb, 0x90, 0xa9, 0xe4, 0x22, 0x34, 0x9e, 0x3b, 0x71, 0xba, 0xb6, 0x02, 0x78, 0x6d, 0xbf, 0x1d, + 0xf4, 0xc6, 0x57, 0xe4, 0xcf, 0x7b, 0x49, 0xd3, 0x28, 0x32, 0xe2, 0x54, 0x8a, 0x27, 0x5e, 0x84, + 0xfb, 0xef, 0xdc, 0x1b, 0x67, 0xb0, 0x14, 0xf5, 0xef, 0x62, 0x60, 0x00, 0x5c, 0x8a, 0x98, 0x89, + 0x3c, 0x2e, 0x19, 0x2f, 0x4a, 0xed, 0x75, 0x7c, 0x14, 0x74, 0xc2, 0x73, 0x6b, 0x44, 0x46, 0xb8, + 0x13, 0xf9, 0x7d, 0x8d, 0x27, 0x0f, 0x1f, 0x1b, 0x8c, 0xd6, 0x1b, 0x8c, 0xbe, 0x37, 0x18, 0xbd, + 0x6d, 0x71, 0x6b, 0xbd, 0xc5, 0xad, 0xaf, 0x2d, 0x6e, 0x3d, 0xd2, 0x82, 0xeb, 0x72, 0x99, 0x92, + 0x4c, 0x2e, 0xe8, 0xae, 0xd2, 0x50, 0x30, 0xfd, 0x2c, 0xd5, 0x8c, 0xee, 0x37, 0x7c, 0x69, 0x56, + 0xd4, 0xaf, 0x15, 0x83, 0xf4, 0xa8, 0x1e, 0xf1, 0xfa, 0x37, 0x00, 0x00, 0xff, 0xff, 0x7d, 0x97, + 0x5a, 0x2e, 0xd1, 0x01, 0x00, 0x00, } func (m *Supplier) Marshal() (dAtA []byte, err error) { @@ -134,6 +146,11 @@ func (m *Supplier) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.UnstakeSessionEndHeight != 0 { + i = encodeVarintSupplier(dAtA, i, uint64(m.UnstakeSessionEndHeight)) + i-- + dAtA[i] = 0x20 + } if len(m.Services) > 0 { for iNdEx := len(m.Services) - 1; iNdEx >= 0; iNdEx-- { { @@ -201,6 +218,9 @@ func (m *Supplier) Size() (n int) { n += 1 + l + sovSupplier(uint64(l)) } } + if m.UnstakeSessionEndHeight != 0 { + n += 1 + sovSupplier(uint64(m.UnstakeSessionEndHeight)) + } return n } @@ -341,6 +361,25 @@ func (m *Supplier) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field UnstakeSessionEndHeight", wireType) + } + m.UnstakeSessionEndHeight = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowSupplier + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.UnstakeSessionEndHeight |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipSupplier(dAtA[iNdEx:]) diff --git a/x/supplier/keeper/keeper.go b/x/supplier/keeper/keeper.go index af1753c79..134c4045e 100644 --- a/x/supplier/keeper/keeper.go +++ b/x/supplier/keeper/keeper.go @@ -22,6 +22,7 @@ type ( authority string bankKeeper types.BankKeeper + sharedKeeper types.SharedKeeper serviceKeeper types.ServiceKeeper } ) @@ -33,6 +34,7 @@ func NewKeeper( authority string, bankKeeper types.BankKeeper, + sharedKeeper types.SharedKeeper, serviceKeeper types.ServiceKeeper, ) Keeper { if _, err := sdk.AccAddressFromBech32(authority); err != nil { @@ -46,6 +48,7 @@ func NewKeeper( logger: logger, bankKeeper: bankKeeper, + sharedKeeper: sharedKeeper, serviceKeeper: serviceKeeper, } } diff --git a/x/supplier/keeper/msg_server_stake_supplier.go b/x/supplier/keeper/msg_server_stake_supplier.go index 56cd1ee5e..093937d93 100644 --- a/x/supplier/keeper/msg_server_stake_supplier.go +++ b/x/supplier/keeper/msg_server_stake_supplier.go @@ -56,6 +56,9 @@ func (k msgServer) StakeSupplier(ctx context.Context, msg *types.MsgStakeSupplie return nil, err } logger.Info(fmt.Sprintf("Supplier is going to escrow an additional %+v coins", coinsToEscrow)) + + // If the supplier has initiated an unstake action, cancel it since they are staking again. + supplier.UnstakeSessionEndHeight = sharedtypes.SupplierNotUnstaking } // Must always stake or upstake (> 0 delta) diff --git a/x/supplier/keeper/msg_server_unstake_supplier.go b/x/supplier/keeper/msg_server_unstake_supplier.go index dd36c7f2e..c11b1892b 100644 --- a/x/supplier/keeper/msg_server_unstake_supplier.go +++ b/x/supplier/keeper/msg_server_unstake_supplier.go @@ -7,6 +7,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/pokt-network/poktroll/telemetry" + "github.com/pokt-network/poktroll/x/shared" "github.com/pokt-network/poktroll/x/supplier/types" ) @@ -37,23 +38,25 @@ func (k msgServer) UnstakeSupplier( } logger.Info(fmt.Sprintf("Supplier found. Unstaking supplier for address %s", msg.Address)) - // Retrieve the address of the supplier - supplierAddress, err := sdk.AccAddressFromBech32(msg.Address) - if err != nil { - logger.Error(fmt.Sprintf("could not parse address %s", msg.Address)) - return nil, err - } - - // Send the coins from the supplier pool back to the supplier - err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, supplierAddress, []sdk.Coin{*supplier.Stake}) - if err != nil { - logger.Error(fmt.Sprintf("could not send %v coins from %s module to %s account due to %v", supplier.Stake, supplierAddress, types.ModuleName, err)) - return nil, err + // Check if the supplier has already initiated the unstake action. + if supplier.IsUnbonding() { + logger.Warn(fmt.Sprintf("Supplier %s still unbonding from previous unstaking", msg.Address)) + return nil, types.ErrSupplierIsUnstaking } - // Update the Supplier in the store - k.RemoveSupplier(ctx, supplierAddress.String()) - logger.Info(fmt.Sprintf("Successfully removed the supplier: %+v", supplier)) + sdkCtx := sdk.UnwrapSDKContext(ctx) + currentHeight := sdkCtx.BlockHeight() + sharedParams := k.sharedKeeper.GetParams(ctx) + + // Mark the supplier as unstaking by recording the height at which it should stop + // providing service. + // The supplier MUST continue to provide service until the end of the current + // session. I.e., on-chain sessions' suppliers list MUST NOT change mid-session. + // Removing it right away could have undesired effects on the network + // (e.g. a session with less than the minimum or 0 number of suppliers, + // off-chain actors that need to listen to session supplier's change mid-session, etc). + supplier.UnstakeSessionEndHeight = uint64(shared.GetSessionEndHeight(&sharedParams, currentHeight)) + k.SetSupplier(ctx, supplier) isSuccessful = true return &types.MsgUnstakeSupplierResponse{}, nil diff --git a/x/supplier/keeper/msg_server_unstake_supplier_test.go b/x/supplier/keeper/msg_server_unstake_supplier_test.go index fc601f9d9..2c6c5415a 100644 --- a/x/supplier/keeper/msg_server_unstake_supplier_test.go +++ b/x/supplier/keeper/msg_server_unstake_supplier_test.go @@ -18,53 +18,117 @@ func TestMsgServer_UnstakeSupplier_Success(t *testing.T) { k, ctx := keepertest.SupplierKeeper(t) srv := keeper.NewMsgServerImpl(k) - // Generate an address for the supplier - supplierAddr := sample.AccAddress() + // Generate two addresses for an unstaking and a non-unstaking suppliers + unstakingSupplierAddr := sample.AccAddress() // Verify that the supplier does not exist yet - _, isSupplierFound := k.GetSupplier(ctx, supplierAddr) + _, isSupplierFound := k.GetSupplier(ctx, unstakingSupplierAddr) require.False(t, isSupplierFound) - // Prepare the supplier - initialStake := sdk.NewCoin("upokt", math.NewInt(100)) - stakeMsg := &types.MsgStakeSupplier{ - Address: supplierAddr, - Stake: &initialStake, - Services: []*sharedtypes.SupplierServiceConfig{ - { - Service: &sharedtypes.Service{ - Id: "svcId", - }, - Endpoints: []*sharedtypes.SupplierEndpoint{ - { - Url: "http://localhost:8080", - RpcType: sharedtypes.RPCType_JSON_RPC, - Configs: make([]*sharedtypes.ConfigOption, 0), - }, - }, - }, - }, - } + initialStake := int64(100) + stakeMsg := createStakeMsg(unstakingSupplierAddr, initialStake) // Stake the supplier _, err := srv.StakeSupplier(ctx, stakeMsg) require.NoError(t, err) // Verify that the supplier exists - foundSupplier, isSupplierFound := k.GetSupplier(ctx, supplierAddr) + foundSupplier, isSupplierFound := k.GetSupplier(ctx, unstakingSupplierAddr) require.True(t, isSupplierFound) - require.Equal(t, supplierAddr, foundSupplier.Address) - require.Equal(t, initialStake.Amount, foundSupplier.Stake.Amount) + require.Equal(t, unstakingSupplierAddr, foundSupplier.Address) + require.Equal(t, math.NewInt(initialStake), foundSupplier.Stake.Amount) require.Len(t, foundSupplier.Services, 1) - // Unstake the supplier - unstakeMsg := &types.MsgUnstakeSupplier{Address: supplierAddr} + // Create and stake another supplier that will not be unstaked to assert that only the + // unstaking supplier is removed from the suppliers list when the unbonding period is over. + nonUnstakingSupplierAddr := sample.AccAddress() + stakeMsg = createStakeMsg(nonUnstakingSupplierAddr, initialStake) + _, err = srv.StakeSupplier(ctx, stakeMsg) + require.NoError(t, err) + + // Verify that the non-unstaking supplier exists + _, isSupplierFound = k.GetSupplier(ctx, nonUnstakingSupplierAddr) + require.True(t, isSupplierFound) + + // Initiate the supplier unstaking + unstakeMsg := &types.MsgUnstakeSupplier{Address: unstakingSupplierAddr} _, err = srv.UnstakeSupplier(ctx, unstakeMsg) require.NoError(t, err) - // Make sure the supplier can no longer be found after unstaking - _, isSupplierFound = k.GetSupplier(ctx, supplierAddr) + // Make sure the supplier entered the unbonding period + foundSupplier, isSupplierFound = k.GetSupplier(ctx, unstakingSupplierAddr) + require.True(t, isSupplierFound) + require.True(t, foundSupplier.IsUnbonding()) + + // Move block height to the end of the unbonding period + unbondingHeight := k.GetSupplierUnbondingHeight(ctx, &foundSupplier) + ctx = keepertest.SetBlockHeight(ctx, unbondingHeight) + + // Run the endblocker to unbond suppliers + err = k.EndBlockerUnbondSuppliers(ctx) + require.NoError(t, err) + + // Make sure the unstaking supplier is removed from the suppliers list when the + // unbonding period is over + _, isSupplierFound = k.GetSupplier(ctx, unstakingSupplierAddr) require.False(t, isSupplierFound) + + // Verify that the non-unstaking supplier still exists + nonUnstakingSupplier, isSupplierFound := k.GetSupplier(ctx, nonUnstakingSupplierAddr) + require.True(t, isSupplierFound) + require.False(t, nonUnstakingSupplier.IsUnbonding()) +} + +func TestMsgServer_UnstakeSupplier_CancelUnbondingIfRestaked(t *testing.T) { + k, ctx := keepertest.SupplierKeeper(t) + srv := keeper.NewMsgServerImpl(k) + + // Generate an address for the supplier + supplierAddr := sample.AccAddress() + + // Stake the supplier + initialStake := int64(100) + stakeMsg := createStakeMsg(supplierAddr, initialStake) + _, err := srv.StakeSupplier(ctx, stakeMsg) + require.NoError(t, err) + + // Verify that the supplier exists with no unbonding height + foundSupplier, isSupplierFound := k.GetSupplier(ctx, supplierAddr) + require.True(t, isSupplierFound) + require.False(t, foundSupplier.IsUnbonding()) + + // Initiate the supplier unstaking + unstakeMsg := &types.MsgUnstakeSupplier{Address: supplierAddr} + _, err = srv.UnstakeSupplier(ctx, unstakeMsg) + require.NoError(t, err) + + // Make sure the supplier entered the unbonding period + foundSupplier, isSupplierFound = k.GetSupplier(ctx, supplierAddr) + require.True(t, isSupplierFound) + require.True(t, foundSupplier.IsUnbonding()) + + unbondingHeight := k.GetSupplierUnbondingHeight(ctx, &foundSupplier) + + // Stake the supplier again + stakeMsg = createStakeMsg(supplierAddr, initialStake+1) + _, err = srv.StakeSupplier(ctx, stakeMsg) + require.NoError(t, err) + + // Make sure the supplier is no longer in the unbonding period + foundSupplier, isSupplierFound = k.GetSupplier(ctx, supplierAddr) + require.True(t, isSupplierFound) + require.False(t, foundSupplier.IsUnbonding()) + + ctx = keepertest.SetBlockHeight(ctx, int64(unbondingHeight)) + + // Run the EndBlocker, the supplier should not be unbonding. + err = k.EndBlockerUnbondSuppliers(ctx) + require.NoError(t, err) + + // Make sure the supplier is still in the suppliers list with an unbonding height of 0 + foundSupplier, isSupplierFound = k.GetSupplier(ctx, supplierAddr) + require.True(t, isSupplierFound) + require.False(t, foundSupplier.IsUnbonding()) } func TestMsgServer_UnstakeSupplier_FailIfNotStaked(t *testing.T) { @@ -78,7 +142,7 @@ func TestMsgServer_UnstakeSupplier_FailIfNotStaked(t *testing.T) { _, isSupplierFound := k.GetSupplier(ctx, supplierAddr) require.False(t, isSupplierFound) - // Unstake the supplier + // Initiate the supplier unstaking unstakeMsg := &types.MsgUnstakeSupplier{Address: supplierAddr} _, err := srv.UnstakeSupplier(ctx, unstakeMsg) require.Error(t, err) @@ -87,3 +151,50 @@ func TestMsgServer_UnstakeSupplier_FailIfNotStaked(t *testing.T) { _, isSupplierFound = k.GetSupplier(ctx, supplierAddr) require.False(t, isSupplierFound) } + +func TestMsgServer_UnstakeSupplier_FailIfCurrentlyUnstaking(t *testing.T) { + k, ctx := keepertest.SupplierKeeper(t) + srv := keeper.NewMsgServerImpl(k) + + // Generate an address for the supplier + supplierAddr := sample.AccAddress() + + // Stake the supplier + initialStake := int64(100) + stakeMsg := createStakeMsg(supplierAddr, initialStake) + _, err := srv.StakeSupplier(ctx, stakeMsg) + require.NoError(t, err) + + // Initiate the supplier unstaking + unstakeMsg := &types.MsgUnstakeSupplier{Address: supplierAddr} + _, err = srv.UnstakeSupplier(ctx, unstakeMsg) + require.NoError(t, err) + + sdkCtx := sdk.UnwrapSDKContext(ctx) + ctx = keepertest.SetBlockHeight(ctx, int64(sdkCtx.BlockHeight()+1)) + + _, err = srv.UnstakeSupplier(ctx, unstakeMsg) + require.ErrorIs(t, err, types.ErrSupplierIsUnstaking) +} + +func createStakeMsg(supplierAddr string, stakeAmount int64) *types.MsgStakeSupplier { + initialStake := sdk.NewCoin("upokt", math.NewInt(stakeAmount)) + return &types.MsgStakeSupplier{ + Address: supplierAddr, + Stake: &initialStake, + Services: []*sharedtypes.SupplierServiceConfig{ + { + Service: &sharedtypes.Service{ + Id: "svcId", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + } +} diff --git a/x/supplier/keeper/unbond_suppliers.go b/x/supplier/keeper/unbond_suppliers.go new file mode 100644 index 000000000..63c08a3c4 --- /dev/null +++ b/x/supplier/keeper/unbond_suppliers.go @@ -0,0 +1,73 @@ +package keeper + +import ( + "context" + "fmt" + + cosmostypes "github.com/cosmos/cosmos-sdk/types" + "github.com/pokt-network/poktroll/x/shared" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/types" +) + +// EndBlockerUnbondSuppliers unbonds suppliers whose unbonding period has elapsed. +func (k Keeper) EndBlockerUnbondSuppliers(ctx context.Context) error { + sdkCtx := cosmostypes.UnwrapSDKContext(ctx) + currentHeight := sdkCtx.BlockHeight() + + logger := k.Logger().With("method", "UnbondSupplier") + + // Iterate over all suppliers and unbond suppliers that have finished the unbonding period. + // TODO_IMPROVE: Use an index to iterate over suppliers that have initiated the + // unbonding action instead of iterating over all suppliers. + for _, supplier := range k.GetAllSuppliers(ctx) { + // Ignore suppliers that have not initiated the unbonding action. + if !supplier.IsUnbonding() { + continue + } + + unbondingHeight := k.GetSupplierUnbondingHeight(ctx, &supplier) + + // If the unbonding height is ahead of the current height, the supplier + // stays in the unbonding state. + if unbondingHeight > currentHeight { + continue + } + + // Retrieve the address of the supplier. + supplierAddress, err := cosmostypes.AccAddressFromBech32(supplier.Address) + if err != nil { + logger.Error(fmt.Sprintf("could not parse address %s", supplier.Address)) + return err + } + + // Send the coins from the supplier pool back to the supplier. + if err = k.bankKeeper.SendCoinsFromModuleToAccount( + ctx, types.ModuleName, supplierAddress, []cosmostypes.Coin{*supplier.Stake}, + ); err != nil { + logger.Error(fmt.Sprintf( + "could not send %s coins from %s module to %s account due to %s", + supplier.Stake.String(), supplierAddress, types.ModuleName, err, + )) + return err + } + + // Remove the supplier from the store. + k.RemoveSupplier(ctx, supplierAddress.String()) + logger.Info(fmt.Sprintf("Successfully removed the supplier: %+v", supplier)) + } + + return nil +} + +// GetSupplierUnbondingHeight returns the height at which the supplier can be unbonded. +// TODO_REFACTOR(@red-0ne): Make this a static function in the shared pkg alongside +// the window height helpers. +func (k Keeper) GetSupplierUnbondingHeight( + ctx context.Context, + supplier *sharedtypes.Supplier, +) int64 { + sharedParams := k.sharedKeeper.GetParams(ctx) + + return shared.GetSupplierUnbondingHeight(&sharedParams, supplier) +} diff --git a/x/supplier/module/abci.go b/x/supplier/module/abci.go new file mode 100644 index 000000000..86e463ea5 --- /dev/null +++ b/x/supplier/module/abci.go @@ -0,0 +1,17 @@ +package supplier + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/pokt-network/poktroll/x/supplier/keeper" +) + +// EndBlocker is called every block and handles supplier related updates. +func EndBlocker(ctx sdk.Context, k keeper.Keeper) error { + // TODO_IMPROVE(@red-0ne): Add logs and/or telemetry on the number of unbonded suppliers. + if err := k.EndBlockerUnbondSuppliers(ctx); err != nil { + return err + } + + return nil +} diff --git a/x/supplier/module/module.go b/x/supplier/module/module.go index fd71be1b8..0f57e31ae 100644 --- a/x/supplier/module/module.go +++ b/x/supplier/module/module.go @@ -95,10 +95,10 @@ func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *r type AppModule struct { AppModuleBasic - keeper keeper.Keeper - accountKeeper types.AccountKeeper - bankKeeper types.BankKeeper - serviceKeeper types.ServiceKeeper + supplierKeeper keeper.Keeper + accountKeeper types.AccountKeeper + bankKeeper types.BankKeeper + serviceKeeper types.ServiceKeeper } func NewAppModule( @@ -110,7 +110,7 @@ func NewAppModule( ) AppModule { return AppModule{ AppModuleBasic: NewAppModuleBasic(cdc), - keeper: keeper, + supplierKeeper: keeper, accountKeeper: accountKeeper, bankKeeper: bankKeeper, serviceKeeper: serviceKeeper, @@ -119,8 +119,8 @@ func NewAppModule( // RegisterServices registers a gRPC query service to respond to the module-specific gRPC queries func (am AppModule) RegisterServices(cfg module.Configurator) { - types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper)) - types.RegisterQueryServer(cfg.QueryServer(), am.keeper) + types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.supplierKeeper)) + types.RegisterQueryServer(cfg.QueryServer(), am.supplierKeeper) } // RegisterInvariants registers the invariants of the module. If an invariant deviates from its predicted value, the InvariantRegistry triggers appropriate logic (most often the chain will be halted) @@ -132,12 +132,12 @@ func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, gs json.Ra // Initialize global index to index in genesis state cdc.MustUnmarshalJSON(gs, &genState) - InitGenesis(ctx, am.keeper, genState) + InitGenesis(ctx, am.supplierKeeper, genState) } // ExportGenesis returns the module's exported genesis state as raw JSON bytes. func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { - genState := ExportGenesis(ctx, am.keeper) + genState := ExportGenesis(ctx, am.supplierKeeper) return cdc.MustMarshalJSON(genState) } @@ -154,8 +154,9 @@ func (am AppModule) BeginBlock(_ context.Context) error { // EndBlock contains the logic that is automatically triggered at the end of each block. // The end block implementation is optional. -func (am AppModule) EndBlock(_ context.Context) error { - return nil +func (am AppModule) EndBlock(goCtx context.Context) error { + ctx := sdk.UnwrapSDKContext(goCtx) + return EndBlocker(ctx, am.supplierKeeper) } // IsOnePerModuleType implements the depinject.OnePerModuleType interface. @@ -182,6 +183,7 @@ type ModuleInputs struct { AccountKeeper types.AccountKeeper BankKeeper types.BankKeeper + SharedKeeper types.SharedKeeper ServiceKeeper types.ServiceKeeper } @@ -204,6 +206,7 @@ func ProvideModule(in ModuleInputs) ModuleOutputs { in.Logger, authority.String(), in.BankKeeper, + in.SharedKeeper, in.ServiceKeeper, ) m := NewAppModule( diff --git a/x/supplier/module/simulation.go b/x/supplier/module/simulation.go index 7f4ed529c..cf9e6236f 100644 --- a/x/supplier/module/simulation.go +++ b/x/supplier/module/simulation.go @@ -67,7 +67,7 @@ func (am AppModule) WeightedOperations(simState module.SimulationState) []simtyp ) operations = append(operations, simulation.NewWeightedOperation( weightMsgStakeSupplier, - suppliersimulation.SimulateMsgStakeSupplier(am.accountKeeper, am.bankKeeper, am.keeper), + suppliersimulation.SimulateMsgStakeSupplier(am.accountKeeper, am.bankKeeper, am.supplierKeeper), )) var weightMsgUnstakeSupplier int @@ -78,7 +78,7 @@ func (am AppModule) WeightedOperations(simState module.SimulationState) []simtyp ) operations = append(operations, simulation.NewWeightedOperation( weightMsgUnstakeSupplier, - suppliersimulation.SimulateMsgUnstakeSupplier(am.accountKeeper, am.bankKeeper, am.keeper), + suppliersimulation.SimulateMsgUnstakeSupplier(am.accountKeeper, am.bankKeeper, am.supplierKeeper), )) // this line is used by starport scaffolding # simapp/module/operation @@ -93,7 +93,7 @@ func (am AppModule) ProposalMsgs(simState module.SimulationState) []simtypes.Wei opWeightMsgStakeSupplier, defaultWeightMsgStakeSupplier, func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) sdk.Msg { - suppliersimulation.SimulateMsgStakeSupplier(am.accountKeeper, am.bankKeeper, am.keeper) + suppliersimulation.SimulateMsgStakeSupplier(am.accountKeeper, am.bankKeeper, am.supplierKeeper) return nil }, ), @@ -101,7 +101,7 @@ func (am AppModule) ProposalMsgs(simState module.SimulationState) []simtypes.Wei opWeightMsgUnstakeSupplier, defaultWeightMsgUnstakeSupplier, func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) sdk.Msg { - suppliersimulation.SimulateMsgUnstakeSupplier(am.accountKeeper, am.bankKeeper, am.keeper) + suppliersimulation.SimulateMsgUnstakeSupplier(am.accountKeeper, am.bankKeeper, am.supplierKeeper) return nil }, ), diff --git a/x/supplier/types/errors.go b/x/supplier/types/errors.go index 6ceca8f4e..ce2675285 100644 --- a/x/supplier/types/errors.go +++ b/x/supplier/types/errors.go @@ -16,5 +16,7 @@ var ( ErrSupplierInvalidSessionId = sdkerrors.Register(ModuleName, 1107, "invalid session ID") ErrSupplierInvalidService = sdkerrors.Register(ModuleName, 1108, "invalid service in supplier") ErrSupplierInvalidSessionEndHeight = sdkerrors.Register(ModuleName, 1109, "invalid session ending height") - ErrSupplierServiceNotFound = sdkerrors.Register(ModuleName, 1110, "service not found") + ErrSupplierIsUnstaking = sdkerrors.Register(ModuleName, 1110, "supplier is in unbonding period") + ErrSupplierParamsInvalid = sdkerrors.Register(ModuleName, 1111, "invalid supplier params") + ErrSupplierServiceNotFound = sdkerrors.Register(ModuleName, 1112, "service not found") ) diff --git a/x/supplier/types/expected_keepers.go b/x/supplier/types/expected_keepers.go index 90d24fee0..97e20c46a 100644 --- a/x/supplier/types/expected_keepers.go +++ b/x/supplier/types/expected_keepers.go @@ -23,6 +23,12 @@ type BankKeeper interface { SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error } +// SharedKeeper defines the expected interface needed to retrieve shared information. +type SharedKeeper interface { + GetParams(ctx context.Context) sharedtypes.Params + GetSessionEndHeight(ctx context.Context, queryHeight int64) int64 +} + // ServiceKeeper defines the expected interface for the Service module. type ServiceKeeper interface { GetService(ctx context.Context, serviceId string) (sharedtypes.Service, bool) diff --git a/x/supplier/types/genesis_test.go b/x/supplier/types/genesis_test.go index 23b16b327..6898d96d3 100644 --- a/x/supplier/types/genesis_test.go +++ b/x/supplier/types/genesis_test.go @@ -58,7 +58,6 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "valid genesis state", genState: &types.GenesisState{ - SupplierList: []sharedtypes.Supplier{ { Address: addr1,