diff --git a/app/app.go b/app/app.go index 2d531f642e..66cafa9f48 100644 --- a/app/app.go +++ b/app/app.go @@ -750,12 +750,12 @@ func New( slashingtypes.ModuleName, govtypes.ModuleName, crisistypes.ModuleName, + dualstakingmoduletypes.ModuleName, genutiltypes.ModuleName, evidencetypes.ModuleName, ibctransfertypes.ModuleName, ibcexported.ModuleName, specmoduletypes.ModuleName, - dualstakingmoduletypes.ModuleName, subscriptionmoduletypes.ModuleName, downtimemoduletypes.ModuleName, pairingmoduletypes.ModuleName, diff --git a/proto/lavanet/lava/subscription/cu_tracker.proto b/proto/lavanet/lava/subscription/cu_tracker.proto index 2ba3516b1f..dd99edcfc1 100644 --- a/proto/lavanet/lava/subscription/cu_tracker.proto +++ b/proto/lavanet/lava/subscription/cu_tracker.proto @@ -2,7 +2,14 @@ syntax = "proto3"; package lavanet.lava.subscription; option go_package = "github.com/lavanet/lava/x/subscription/types"; +import "cosmos/base/v1beta1/coin.proto"; +import "gogoproto/gogo.proto"; message TrackedCu { uint64 cu = 1; // CU counter for CU after QoS consideration +} + +message CuTrackerTimerData { + uint64 block = 1; // sub block + cosmos.base.v1beta1.Coin credit = 2 [(gogoproto.nullable) = false]; // credit to be used for rewards } \ No newline at end of file diff --git a/scripts/init_chain.sh b/scripts/init_chain.sh index c0c41e0727..5402ed55e8 100755 --- a/scripts/init_chain.sh +++ b/scripts/init_chain.sh @@ -108,4 +108,4 @@ lavad add-genesis-account validators_rewards_allocation_pool 30000000000000ulava lavad add-genesis-account providers_rewards_allocation_pool 30000000000000ulava --module-account lavad gentx alice 10000000000000ulava --chain-id lava lavad collect-gentxs -lavad start --pruning=nothing +lavad start --pruning=nothing \ No newline at end of file diff --git a/testutil/common/tester.go b/testutil/common/tester.go index b84a8562a7..369a050664 100644 --- a/testutil/common/tester.go +++ b/testutil/common/tester.go @@ -479,10 +479,10 @@ func (ts *Tester) TxSubscriptionAddProject(creator string, pd projectstypes.Proj } // TxSubscriptionDelProject: implement 'tx subscription del-project' -func (ts *Tester) TxSubscriptionDelProject(creator, projectID string) error { +func (ts *Tester) TxSubscriptionDelProject(creator, projectName string) error { msg := &subscriptiontypes.MsgDelProject{ Creator: creator, - Name: projectID, + Name: projectName, } _, err := ts.Servers.SubscriptionServer.DelProject(ts.GoCtx, msg) return err diff --git a/x/pairing/keeper/cu_tracker_test.go b/x/pairing/keeper/cu_tracker_test.go index a7b5c3a356..c0ec6c471e 100644 --- a/x/pairing/keeper/cu_tracker_test.go +++ b/x/pairing/keeper/cu_tracker_test.go @@ -254,6 +254,7 @@ func TestTrackedCuWithQos(t *testing.T) { // advance month + blocksToSave + 1 to trigger the provider monthly payment ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) balance1 := ts.GetBalance(provider1Acc.Addr) @@ -261,6 +262,7 @@ func TestTrackedCuWithQos(t *testing.T) { reward, err := ts.QueryDualstakingDelegatorRewards(provider1Acc.Addr.String(), provider1Acc.Addr.String(), ts.spec.Index) require.Nil(ts.T, err) + require.Len(t, reward.Rewards, 1) require.Equal(ts.T, tt.p1ExpectedReward, reward.Rewards[0].Amount.Amount.Int64()) _, err = ts.TxDualstakingClaimRewards(provider1Acc.Addr.String(), provider1Acc.Addr.String()) require.Nil(ts.T, err) @@ -363,6 +365,7 @@ func TestTrackedCuPlanPriceChange(t *testing.T) { // advance month + blocksToSave + 1 to trigger the provider monthly payment ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) reward, err := ts.QueryDualstakingDelegatorRewards(providerAcc.Addr.String(), providerAcc.Addr.String(), ts.spec.Index) @@ -606,6 +609,7 @@ func TestProviderMonthlyPayoutQueryWithContributor(t *testing.T) { require.NoError(t, err) ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) _, err = ts.TxDualstakingClaimRewards(providerAcct.Addr.String(), providerAcct.Addr.String()) @@ -646,6 +650,7 @@ func TestFrozenProviderGetReward(t *testing.T) { // advance month + blocksToSave + 1 to trigger the provider monthly payment ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) planPrice := ts.plan.Price.Amount.Int64() diff --git a/x/pairing/keeper/helpers_test.go b/x/pairing/keeper/helpers_test.go index 2555f71ab3..f07e13d57e 100644 --- a/x/pairing/keeper/helpers_test.go +++ b/x/pairing/keeper/helpers_test.go @@ -227,6 +227,7 @@ func (ts *tester) payAndVerifyBalance( // advance month + blocksToSave + 1 to trigger the provider monthly payment ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) // verify provider's balance diff --git a/x/pairing/keeper/msg_server_relay_payment_gov_test.go b/x/pairing/keeper/msg_server_relay_payment_gov_test.go index 7d6e2e032c..6fb969045b 100644 --- a/x/pairing/keeper/msg_server_relay_payment_gov_test.go +++ b/x/pairing/keeper/msg_server_relay_payment_gov_test.go @@ -101,6 +101,7 @@ func TestRelayPaymentGovQosWeightChange(t *testing.T) { // advance month + blocksToSave + 1 to trigger the provider monthly payment ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) _, err = ts.TxDualstakingClaimRewards(providerAcct.Addr.String(), providerAcct.Addr.String()) diff --git a/x/pairing/keeper/msg_server_relay_payment_test.go b/x/pairing/keeper/msg_server_relay_payment_test.go index b813d0d76b..20e4190ece 100644 --- a/x/pairing/keeper/msg_server_relay_payment_test.go +++ b/x/pairing/keeper/msg_server_relay_payment_test.go @@ -216,6 +216,7 @@ func TestRelayPaymentUnstakingProviderForUnresponsivenessWithBadDataInput(t *tes _, err := ts.TxPairingRelayPayment(provider1Addr, relays...) require.NoError(t, err) ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) // reward + before == after diff --git a/x/projects/keeper/creation.go b/x/projects/keeper/creation.go index 196c9b96a4..75828b99da 100644 --- a/x/projects/keeper/creation.go +++ b/x/projects/keeper/creation.go @@ -103,6 +103,10 @@ func (k Keeper) CreateProject(ctx sdk.Context, subAddr string, projectData types } // project name per subscription is unique: check for duplicates + // we also check !isDeleted because when a new subscription is created + // in the same epoch that a subscription from the same creator was expired, + // we'll find the old admin project but since it's deleted we want to ignore + // it and allow creating a new admin project var emptyProject types.Project if found := k.projectsFS.FindEntry(ctx, project.Index, epoch, &emptyProject); found { return utils.LavaFormatWarning("create project failed", @@ -160,7 +164,7 @@ func (k Keeper) DeleteProject(ctx sdk.Context, creator, projectID string) error } for _, projectKey := range project.GetProjectKeys() { - err = k.unregisterKey(ctx, projectKey, &project, nextEpoch) + err := k.unregisterKey(ctx, projectKey, &project, nextEpoch) if err != nil { return err } @@ -195,8 +199,6 @@ func (k Keeper) registerKey(ctx sdk.Context, key types.ProjectKey, project *type ) } - project.AppendKey(types.ProjectDeveloperKey(key.Key)) - // by now, the key was either not found, or found and belongs to us already. // if the former, then we surely need to add it. if !found { @@ -212,6 +214,8 @@ func (k Keeper) registerKey(ctx sdk.Context, key types.ProjectKey, project *type ) } } + + project.AppendKey(types.ProjectDeveloperKey(key.Key)) } return nil diff --git a/x/rewards/keeper/providers_test.go b/x/rewards/keeper/providers_test.go index 9b0af96620..175824c95b 100644 --- a/x/rewards/keeper/providers_test.go +++ b/x/rewards/keeper/providers_test.go @@ -68,6 +68,7 @@ func TestBasicBoostProvidersRewards(t *testing.T) { // first months there are no bonus rewards, just payment ffrom the subscription ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) res, err := ts.QueryDualstakingDelegatorRewards(providerAcc.Addr.String(), providerAcc.Addr.String(), "") @@ -110,6 +111,7 @@ func TestSpecAllocationProvidersRewards(t *testing.T) { // first months there are no bonus rewards, just payment ffrom the subscription ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) res, err := ts.QueryDualstakingDelegatorRewards(providerAcc.Addr.String(), providerAcc.Addr.String(), "") @@ -155,6 +157,7 @@ func TestProvidersDiminishingRewards(t *testing.T) { // first months there are no bonus rewards, just payment ffrom the subscription ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) res, err := ts.QueryDualstakingDelegatorRewards(providerAcc.Addr.String(), providerAcc.Addr.String(), "") @@ -203,6 +206,7 @@ func TestProvidersEndRewards(t *testing.T) { // first months there are no bonus rewards, just payment ffrom the subscription ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) res, err := ts.QueryDualstakingDelegatorRewards(providerAcc.Addr.String(), providerAcc.Addr.String(), "") @@ -262,6 +266,7 @@ func Test2SpecsZeroShares(t *testing.T) { // first months there are no bonus rewards, just payment ffrom the subscription ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) res, err := ts.QueryDualstakingDelegatorRewards(providerAcc.Addr.String(), providerAcc.Addr.String(), "") @@ -325,6 +330,7 @@ func Test2SpecsDoubleShares(t *testing.T) { // first months there are no bonus rewards, just payment ffrom the subscription ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) res, err := ts.QueryDualstakingDelegatorRewards(providerAcc.Addr.String(), providerAcc.Addr.String(), "") @@ -385,6 +391,7 @@ func TestBonusRewards3Providers(t *testing.T) { // first months there are no bonus rewards, just payment ffrom the subscription ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) res, err := ts.QueryDualstakingDelegatorRewards(providerAcc1.Addr.String(), "", "") @@ -474,6 +481,7 @@ func TestValidatorsAndCommunityParticipation(t *testing.T) { // first months there are no bonus rewards, just payment from the subscription ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) expectedReward := sdk.NewIntFromUint64(baserewards * subscription.LIMIT_TOKEN_PER_CU) @@ -523,6 +531,7 @@ func TestBonusReward49months(t *testing.T) { // first months there are no bonus rewards, just payment ffrom the subscription ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) res, err := ts.QueryDualstakingDelegatorRewards(providerAcc.Addr.String(), providerAcc.Addr.String(), "") @@ -574,6 +583,7 @@ func TestBonusRewardsEquall5Providers(t *testing.T) { // first months there are no bonus rewards, just payment ffrom the subscription ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) for _, providerAcc := range providerAccs { @@ -640,6 +650,7 @@ func TestBonusRewards5Providers(t *testing.T) { // first months there are no bonus rewards, just payment ffrom the subscription ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) for _, providerAcc := range providerAccs { @@ -714,6 +725,7 @@ func TestCommunityTaxOne(t *testing.T) { // first months there are no bonus rewards, just payment from the subscription ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) expectedReward := sdk.NewIntFromUint64(baserewards * subscription.LIMIT_TOKEN_PER_CU) diff --git a/x/subscription/keeper/cu_tracker.go b/x/subscription/keeper/cu_tracker.go index 46228fd8ac..9e8f1f88aa 100644 --- a/x/subscription/keeper/cu_tracker.go +++ b/x/subscription/keeper/cu_tracker.go @@ -83,29 +83,18 @@ type trackedCuInfo struct { block uint64 } -func (k Keeper) GetSubTrackedCuInfo(ctx sdk.Context, sub string, subBlockStr string) (trackedCuList []trackedCuInfo, totalCuTracked uint64) { +func (k Keeper) GetSubTrackedCuInfo(ctx sdk.Context, sub string, block uint64) (trackedCuList []trackedCuInfo, totalCuTracked uint64) { keys := k.GetAllSubTrackedCuIndices(ctx, sub) for _, key := range keys { _, provider, chainID := types.DecodeCuTrackerKey(key) - block, err := strconv.ParseUint(subBlockStr, 10, 64) - if err != nil { - utils.LavaFormatError("cannot remove cu tracker", err, - utils.Attribute{Key: "sub", Value: sub}, - utils.Attribute{Key: "provider", Value: provider}, - utils.Attribute{Key: "chain_id", Value: chainID}, - utils.Attribute{Key: "block_str", Value: subBlockStr}, - ) - continue - } - cu, found, _ := k.GetTrackedCu(ctx, sub, provider, chainID, block) if !found { utils.LavaFormatWarning("cannot remove cu tracker", legacyerrors.ErrKeyNotFound, utils.Attribute{Key: "sub", Value: sub}, utils.Attribute{Key: "provider", Value: provider}, utils.Attribute{Key: "chain_id", Value: chainID}, - utils.Attribute{Key: "block", Value: subBlockStr}, + utils.Attribute{Key: "block", Value: strconv.FormatUint(block, 10)}, ) continue } @@ -124,17 +113,17 @@ func (k Keeper) GetSubTrackedCuInfo(ctx sdk.Context, sub string, subBlockStr str // remove only before the sub is deleted func (k Keeper) RewardAndResetCuTracker(ctx sdk.Context, cuTrackerTimerKeyBytes []byte, cuTrackerTimerData []byte) { sub := string(cuTrackerTimerKeyBytes) - blockStr := string(cuTrackerTimerData) - _, err := strconv.ParseUint(blockStr, 10, 64) + var timerData types.CuTrackerTimerData + err := k.cdc.Unmarshal(cuTrackerTimerData, &timerData) if err != nil { - utils.LavaFormatError(types.ErrCuTrackerPayoutFailed.Error(), err, - utils.Attribute{Key: "blockStr", Value: blockStr}, + utils.LavaFormatError(types.ErrCuTrackerPayoutFailed.Error(), fmt.Errorf("invalid data from cu tracker timer"), + utils.Attribute{Key: "timer_data", Value: timerData.String()}, + utils.Attribute{Key: "consumer", Value: sub}, ) return } - trackedCuList, totalCuTracked := k.GetSubTrackedCuInfo(ctx, sub, blockStr) + trackedCuList, totalCuTracked := k.GetSubTrackedCuInfo(ctx, sub, timerData.Block) - var block uint64 if len(trackedCuList) == 0 || totalCuTracked == 0 { // no tracked CU for this sub, nothing to do return @@ -142,21 +131,9 @@ func (k Keeper) RewardAndResetCuTracker(ctx sdk.Context, cuTrackerTimerKeyBytes // Note: We take the subscription from the FixationStore, based on the given block. // So, even if the plan changed during the month, we still take the original plan, based on the given block. - block = trackedCuList[0].block - subObj, _, found := k.GetSubscriptionForBlock(ctx, sub, block) - if !found { - utils.LavaFormatError("cannot find subscription's plan", types.ErrCuTrackerPayoutFailed, - utils.Attribute{Key: "sub_consumer", Value: sub}, - ) - return - } + block := trackedCuList[0].block - var totalTokenAmount math.Int - if subObj.DurationLeft == 0 { - totalTokenAmount = subObj.Credit.Amount - } else { - totalTokenAmount = subObj.Credit.Amount.QuoRaw(int64(subObj.DurationLeft)) - } + totalTokenAmount := timerData.Credit.Amount if totalTokenAmount.Quo(sdk.NewIntFromUint64(totalCuTracked)).GT(sdk.NewIntFromUint64(LIMIT_TOKEN_PER_CU)) { totalTokenAmount = sdk.NewIntFromUint64(LIMIT_TOKEN_PER_CU * totalCuTracked) } @@ -241,15 +218,13 @@ func (k Keeper) RewardAndResetCuTracker(ctx sdk.Context, cuTrackerTimerKeyBytes ) } else { utils.LogLavaEvent(ctx, k.Logger(ctx), types.MonthlyCuTrackerProviderRewardEventName, map[string]string{ - "provider": provider, - "sub": sub, - "plan": subObj.PlanIndex, - "tracked_cu": strconv.FormatUint(trackedCu, 10), - "credit_used": creditToSub.String(), - "credit_remaining": subObj.Credit.String(), - "reward": providerReward.String(), - "block": strconv.FormatInt(ctx.BlockHeight(), 10), - "adjustment_raw": providerAdjustment.String(), + "provider": provider, + "sub": sub, + "tracked_cu": strconv.FormatUint(trackedCu, 10), + "credit_used": creditToSub.String(), + "reward": providerReward.String(), + "block": strconv.FormatInt(ctx.BlockHeight(), 10), + "adjustment_raw": providerAdjustment.String(), }, "Provider got monthly reward successfully") } } @@ -257,7 +232,7 @@ func (k Keeper) RewardAndResetCuTracker(ctx sdk.Context, cuTrackerTimerKeyBytes rewardsRemainder := totalTokenAmount.Sub(totalTokenRewarded) var latestSub types.Subscription - latestEntryBlock, _, _, found := k.subsFS.FindEntryDetailed(ctx, subObj.Consumer, uint64(ctx.BlockHeight()), &latestSub) + latestEntryBlock, _, _, found := k.subsFS.FindEntryDetailed(ctx, sub, uint64(ctx.BlockHeight()), &latestSub) if found { // return rewards remainder to credit if !rewardsRemainder.IsZero() { @@ -284,6 +259,11 @@ func (k Keeper) RewardAndResetCuTracker(ctx sdk.Context, cuTrackerTimerKeyBytes } } } + utils.LogLavaEvent(ctx, k.Logger(ctx), types.RemainingCreditEventName, map[string]string{ + "sub": sub, + "credit_remaining": latestSub.Credit.String(), + "block": strconv.FormatInt(ctx.BlockHeight(), 10), + }, "CU tracker reward and reset executed successfully, printing remaining subscription credit") } func (k Keeper) CalcTotalMonthlyReward(ctx sdk.Context, totalAmount math.Int, trackedCu uint64, totalCuUsedBySub uint64) math.Int { diff --git a/x/subscription/keeper/keeper.go b/x/subscription/keeper/keeper.go index 21242fbbcd..651188f3bb 100644 --- a/x/subscription/keeper/keeper.go +++ b/x/subscription/keeper/keeper.go @@ -35,7 +35,7 @@ type ( subsTS timerstoretypes.TimerStore cuTrackerFS fixationtypes.FixationStore // key: " ", value: month aggregated CU - cuTrackerTS timerstoretypes.TimerStore + cuTrackerTS timerstoretypes.TimerStore // key: sub, value: credit for reward } ) diff --git a/x/subscription/keeper/subscription.go b/x/subscription/keeper/subscription.go index ca0b6b4c18..9fe7627ce1 100644 --- a/x/subscription/keeper/subscription.go +++ b/x/subscription/keeper/subscription.go @@ -445,7 +445,26 @@ func (k Keeper) addCuTrackerTimerForSubscription(ctx sdk.Context, block uint64, utils.Attribute{Key: "block", Value: block}, ) } else { - k.cuTrackerTS.AddTimerByBlockHeight(ctx, block+blocksToSave-1, []byte(sub.Consumer), []byte(strconv.FormatUint(sub.Block, 10))) + nextEpoch, err := k.epochstorageKeeper.GetNextEpoch(ctx, block) + if err != nil { + utils.LavaFormatError("critical: failed assigning CU tracker callback. can't get next epoch, skipping", err, + utils.Attribute{Key: "block", Value: block}, + ) + } else { + creditReward := sub.Credit.Amount.QuoRaw(int64(sub.DurationLeft)) + timerData := types.CuTrackerTimerData{ + Block: sub.Block, + Credit: sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), creditReward), + } + marshaledTimerData, err := k.cdc.Marshal(&timerData) + if err != nil { + utils.LavaFormatError("critical: failed assigning CU tracker callback. can't marshal cu tracker timer data, skipping", err, + utils.Attribute{Key: "block", Value: block}, + ) + return + } + k.cuTrackerTS.AddTimerByBlockHeight(ctx, nextEpoch+blocksToSave-1, []byte(sub.Consumer), marshaledTimerData) + } } } @@ -635,8 +654,17 @@ func (k Keeper) RemoveExpiredSubscription(ctx sdk.Context, consumer string, bloc // delete all projects before deleting k.delAllProjectsFromSubscription(ctx, consumer) - // delete subscription effective now (don't wait for end of epoch) - err := k.subsFS.DelEntry(ctx, consumer, block) + // delete subscription effective next epoch + nextEpoch, err := k.epochstorageKeeper.GetNextEpoch(ctx, block) + if err != nil { + utils.LavaFormatError("deleting expired subscription failed. can't get next epoch", err, + utils.Attribute{Key: "consumer", Value: consumer}, + utils.Attribute{Key: "block", Value: strconv.FormatUint(block, 10)}, + ) + return + } + + err = k.subsFS.DelEntry(ctx, consumer, nextEpoch) if err != nil { utils.LavaFormatError("deleting expired subscription failed", err, utils.Attribute{Key: "consumer", Value: consumer}, diff --git a/x/subscription/keeper/subscription_test.go b/x/subscription/keeper/subscription_test.go index 6d17e1a34b..f7b350379c 100644 --- a/x/subscription/keeper/subscription_test.go +++ b/x/subscription/keeper/subscription_test.go @@ -1284,7 +1284,7 @@ func TestPlanRemovedWhenSubscriptionExpires(t *testing.T) { // expire the subscription ts.AdvanceMonths(1) - ts.AdvanceBlock(6) + ts.AdvanceEpoch() res, err := ts.QuerySubscriptionCurrent(sub1) require.NoError(t, err) require.Nil(t, res.Sub) @@ -1540,8 +1540,9 @@ func TestSubscriptionCuExhaustAndUpgrade(t *testing.T) { // Send relay under the premium-plus subscription sendRelayPayment() - // Advance month + blocksToSave + 1 to trigger the provider monthly payment + // Advance month + epoch + blocksToSave + 1 to trigger the provider monthly payment (cu tracker timer is from next epoch) ts.AdvanceMonths(1) + ts.AdvanceEpoch() ts.AdvanceBlocks(ts.BlocksToSave() + 1) // Query provider's rewards @@ -2281,6 +2282,113 @@ func TestSubscriptionUpgradeAffectsTimer(t *testing.T) { }) } +// TestBuySubscriptionImmediatelyAfterExpiration buys a subcription a block after a subscription +// of the same user is expired. Should fail because the subscription is deleted in the next epoch +func TestBuySubscriptionImmediatelyAfterExpiration(t *testing.T) { + ts := newTester(t) + ts.SetupAccounts(1, 0, 0) // 1 sub, 0 adm, 0 dev + _, consumerAddr := ts.Account("sub1") + ctxBlock := ts.BlockHeight() + + freePlan := ts.Plan("free") + _, err := ts.TxSubscriptionBuy(consumerAddr, consumerAddr, freePlan.Index, 1, false, false) + require.NoError(t, err) + sub, found := ts.getSubscription(consumerAddr) + require.True(t, found) + require.Equal(t, ctxBlock, sub.Block) + + ts.AdvanceMonths(1) + ts.AdvanceBlock() + + _, found = ts.getSubscription(consumerAddr) + require.True(t, found) + + _, err = ts.TxSubscriptionBuy(consumerAddr, consumerAddr, freePlan.Index, 1, false, false) + require.Error(t, err) +} + +// TestChangeProjectJustBeforeSubExpiry tests the following scenario: +// a subscription is just about to end and the subscription's project's policy is changed +// since policy changes are applied after an epoch, the policy changes "after" the sub is expired +// the expected result should be no unexpected errors and the project change should not exist +func TestChangeProjectJustBeforeSubExpiry(t *testing.T) { + ts := newTester(t) + ts.SetupAccounts(1, 0, 0) // 1 sub, 0 adm, 0 dev + _, consumerAddr := ts.Account("sub1") + ctxBlock := ts.BlockHeight() + + freePlan := ts.Plan("free") + _, err := ts.TxSubscriptionBuy(consumerAddr, consumerAddr, freePlan.Index, 1, false, false) + require.NoError(t, err) + sub, found := ts.getSubscription(consumerAddr) + require.True(t, found) + require.Equal(t, ctxBlock, sub.Block) + + ts.AdvanceMonths(1) + + // get project and change its policy + proj, err := ts.GetProjectForDeveloper(consumerAddr, ctxBlock) + require.NoError(t, err) + newPolicy := common.CreateMockPolicy() + _, err = ts.TxProjectSetPolicy(proj.Index, consumerAddr, &newPolicy) + require.NoError(t, err) + + // advance epoch. the subscription and project should be deleted + ts.AdvanceEpoch() + _, err = ts.GetProjectForBlock(proj.Index, uint64(ts.Ctx.BlockHeight())) + require.Error(t, err) + + _, err = ts.GetProjectForDeveloper(consumerAddr, uint64(ts.Ctx.BlockHeight())) + require.Error(t, err) + + // do the checks again for sanity + ts.AdvanceEpoch() + _, err = ts.GetProjectForBlock(proj.Index, uint64(ts.Ctx.BlockHeight())) + require.Error(t, err) + + _, err = ts.GetProjectForDeveloper(consumerAddr, uint64(ts.Ctx.BlockHeight())) + require.Error(t, err) +} + +// TestAddProjectChangePolicyJustAfterSubExpiry tests the following scenario: +// the subscription expires. Right after, you try to add a project to the project +// and edit the sub's admin project's policy. Both should fail since the sub is expired +func TestAddProjectChangePolicyJustAfterSubExpiry(t *testing.T) { + ts := newTester(t) + ts.SetupAccounts(1, 0, 0) // 1 sub, 0 adm, 0 dev + _, consumerAddr := ts.Account("sub1") + ctxBlock := ts.BlockHeight() + + freePlan := ts.Plan("free") + _, err := ts.TxSubscriptionBuy(consumerAddr, consumerAddr, freePlan.Index, 1, false, false) + require.NoError(t, err) + sub, found := ts.getSubscription(consumerAddr) + require.True(t, found) + require.Equal(t, ctxBlock, sub.Block) + + res, err := ts.QuerySubscriptionListProjects(consumerAddr) + require.NoError(t, err) + require.Len(t, res.Projects, 1) // admin project only + adminProj := res.Projects[0] + + ts.AdvanceMonths(1) + ts.AdvanceEpoch() // advance epoch because the sub deletion triggers on the next epoch + + // try to add a project to the expired subscription + policy := common.CreateMockPolicy() + err = ts.TxSubscriptionAddProject(consumerAddr, projectstypes.ProjectData{ + Name: "dummy", + Enabled: true, + ProjectKeys: []projectstypes.ProjectKey{projectstypes.ProjectAdminKey(consumerAddr)}, + Policy: &policy, + }) + require.Error(t, err) + + // try to edit the admin project's policy + _, err = ts.TxProjectSetPolicy(adminProj, consumerAddr, &policy) + require.Error(t, err) +} + // TestProjectsNumEnforcement tests the plan policy's projects num enforcement. // scenario: a consumer buy a plan with 2 allowed projects. The consumer should be // able to add only one more project (buying the subscription auto-creates an admin project) diff --git a/x/subscription/types/cu_tracker.pb.go b/x/subscription/types/cu_tracker.pb.go index 44d53df53d..d36ed37bbe 100644 --- a/x/subscription/types/cu_tracker.pb.go +++ b/x/subscription/types/cu_tracker.pb.go @@ -5,6 +5,8 @@ package types import ( fmt "fmt" + types "github.com/cosmos/cosmos-sdk/types" + _ "github.com/cosmos/gogoproto/gogoproto" proto "github.com/cosmos/gogoproto/proto" io "io" math "math" @@ -66,8 +68,61 @@ func (m *TrackedCu) GetCu() uint64 { return 0 } +type CuTrackerTimerData struct { + Block uint64 `protobuf:"varint,1,opt,name=block,proto3" json:"block,omitempty"` + Credit types.Coin `protobuf:"bytes,2,opt,name=credit,proto3" json:"credit"` +} + +func (m *CuTrackerTimerData) Reset() { *m = CuTrackerTimerData{} } +func (m *CuTrackerTimerData) String() string { return proto.CompactTextString(m) } +func (*CuTrackerTimerData) ProtoMessage() {} +func (*CuTrackerTimerData) Descriptor() ([]byte, []int) { + return fileDescriptor_5974e118ddf7c543, []int{1} +} +func (m *CuTrackerTimerData) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *CuTrackerTimerData) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_CuTrackerTimerData.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 *CuTrackerTimerData) XXX_Merge(src proto.Message) { + xxx_messageInfo_CuTrackerTimerData.Merge(m, src) +} +func (m *CuTrackerTimerData) XXX_Size() int { + return m.Size() +} +func (m *CuTrackerTimerData) XXX_DiscardUnknown() { + xxx_messageInfo_CuTrackerTimerData.DiscardUnknown(m) +} + +var xxx_messageInfo_CuTrackerTimerData proto.InternalMessageInfo + +func (m *CuTrackerTimerData) GetBlock() uint64 { + if m != nil { + return m.Block + } + return 0 +} + +func (m *CuTrackerTimerData) GetCredit() types.Coin { + if m != nil { + return m.Credit + } + return types.Coin{} +} + func init() { proto.RegisterType((*TrackedCu)(nil), "lavanet.lava.subscription.TrackedCu") + proto.RegisterType((*CuTrackerTimerData)(nil), "lavanet.lava.subscription.CuTrackerTimerData") } func init() { @@ -75,17 +130,24 @@ func init() { } var fileDescriptor_5974e118ddf7c543 = []byte{ - // 158 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xd2, 0xca, 0x49, 0x2c, 0x4b, - 0xcc, 0x4b, 0x2d, 0xd1, 0x07, 0xd1, 0xfa, 0xc5, 0xa5, 0x49, 0xc5, 0xc9, 0x45, 0x99, 0x05, 0x25, - 0x99, 0xf9, 0x79, 0xfa, 0xc9, 0xa5, 0xf1, 0x25, 0x45, 0x89, 0xc9, 0xd9, 0xa9, 0x45, 0x7a, 0x05, - 0x45, 0xf9, 0x25, 0xf9, 0x42, 0x92, 0x50, 0xb5, 0x7a, 0x20, 0x5a, 0x0f, 0x59, 0xad, 0x92, 0x34, - 0x17, 0x67, 0x08, 0x58, 0x6d, 0x8a, 0x73, 0xa9, 0x10, 0x1f, 0x17, 0x53, 0x72, 0xa9, 0x04, 0xa3, - 0x02, 0xa3, 0x06, 0x4b, 0x10, 0x53, 0x72, 0xa9, 0x93, 0xdb, 0x89, 0x47, 0x72, 0x8c, 0x17, 0x1e, - 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, 0xe1, 0xb1, 0x1c, 0xc3, 0x85, 0xc7, 0x72, 0x0c, 0x37, - 0x1e, 0xcb, 0x31, 0x44, 0xe9, 0xa4, 0x67, 0x96, 0x64, 0x94, 0x26, 0xe9, 0x25, 0xe7, 0xe7, 0xea, - 0xa3, 0x38, 0xa4, 0x02, 0xd5, 0x29, 0x25, 0x95, 0x05, 0xa9, 0xc5, 0x49, 0x6c, 0x60, 0x67, 0x18, - 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xc4, 0xd8, 0xe8, 0x75, 0xb4, 0x00, 0x00, 0x00, + // 268 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x54, 0x90, 0xcd, 0x4a, 0x03, 0x31, + 0x14, 0x85, 0x27, 0x43, 0x2d, 0x18, 0xc1, 0xc5, 0xd0, 0x45, 0x5b, 0x21, 0x96, 0xae, 0x8a, 0x48, + 0x42, 0x75, 0xe1, 0xbe, 0x23, 0x3e, 0x40, 0xe9, 0xca, 0x8d, 0x24, 0xb7, 0x61, 0x0c, 0xed, 0xcc, + 0x1d, 0xf2, 0x53, 0xf4, 0x2d, 0x7c, 0xac, 0x2e, 0xbb, 0x74, 0x25, 0x32, 0xf3, 0x22, 0x32, 0x3f, + 0x0b, 0xbb, 0x3a, 0x27, 0xc9, 0x17, 0xee, 0xb9, 0x87, 0xde, 0xed, 0xe5, 0x41, 0x16, 0xda, 0x8b, + 0x46, 0x85, 0x0b, 0xca, 0x81, 0x35, 0xa5, 0x37, 0x58, 0x08, 0x08, 0x6f, 0xde, 0x4a, 0xd8, 0x69, + 0xcb, 0x4b, 0x8b, 0x1e, 0x93, 0x49, 0xcf, 0xf2, 0x46, 0xf9, 0x7f, 0x76, 0xca, 0x00, 0x5d, 0x8e, + 0x4e, 0x28, 0xe9, 0xb4, 0x38, 0x2c, 0x95, 0xf6, 0x72, 0x29, 0x00, 0x4d, 0xd1, 0x7d, 0x9d, 0x8e, + 0x32, 0xcc, 0xb0, 0xb5, 0xa2, 0x71, 0xdd, 0xed, 0xfc, 0x86, 0x5e, 0x6e, 0xda, 0x09, 0xdb, 0x34, + 0x24, 0xd7, 0x34, 0x86, 0x30, 0x26, 0x33, 0xb2, 0x18, 0xac, 0x63, 0x08, 0x73, 0xa0, 0x49, 0x1a, + 0xba, 0x67, 0xbb, 0x31, 0xb9, 0xb6, 0xcf, 0xd2, 0xcb, 0x64, 0x44, 0x2f, 0xd4, 0x1e, 0x61, 0xd7, + 0x83, 0xdd, 0x21, 0x79, 0xa2, 0x43, 0xb0, 0x7a, 0x6b, 0xfc, 0x38, 0x9e, 0x91, 0xc5, 0xd5, 0xc3, + 0x84, 0x77, 0x79, 0x78, 0x93, 0x87, 0xf7, 0x79, 0x78, 0x8a, 0xa6, 0x58, 0x0d, 0x8e, 0x3f, 0xb7, + 0xd1, 0xba, 0xc7, 0x57, 0x2f, 0xc7, 0x8a, 0x91, 0x53, 0xc5, 0xc8, 0x6f, 0xc5, 0xc8, 0x57, 0xcd, + 0xa2, 0x53, 0xcd, 0xa2, 0xef, 0x9a, 0x45, 0xaf, 0xf7, 0x99, 0xf1, 0xef, 0x41, 0x71, 0xc0, 0x5c, + 0x9c, 0x75, 0xf4, 0x71, 0xde, 0x92, 0xff, 0x2c, 0xb5, 0x53, 0xc3, 0x76, 0xa1, 0xc7, 0xbf, 0x00, + 0x00, 0x00, 0xff, 0xff, 0x69, 0xac, 0xb1, 0xfe, 0x4f, 0x01, 0x00, 0x00, } func (m *TrackedCu) Marshal() (dAtA []byte, err error) { @@ -116,6 +178,44 @@ func (m *TrackedCu) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *CuTrackerTimerData) 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 *CuTrackerTimerData) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *CuTrackerTimerData) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.Credit.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintCuTracker(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + if m.Block != 0 { + i = encodeVarintCuTracker(dAtA, i, uint64(m.Block)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + func encodeVarintCuTracker(dAtA []byte, offset int, v uint64) int { offset -= sovCuTracker(v) base := offset @@ -139,6 +239,20 @@ func (m *TrackedCu) Size() (n int) { return n } +func (m *CuTrackerTimerData) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Block != 0 { + n += 1 + sovCuTracker(uint64(m.Block)) + } + l = m.Credit.Size() + n += 1 + l + sovCuTracker(uint64(l)) + return n +} + func sovCuTracker(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -214,6 +328,108 @@ func (m *TrackedCu) Unmarshal(dAtA []byte) error { } return nil } +func (m *CuTrackerTimerData) 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 ErrIntOverflowCuTracker + } + 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: CuTrackerTimerData: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CuTrackerTimerData: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Block", wireType) + } + m.Block = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCuTracker + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Block |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Credit", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCuTracker + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthCuTracker + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthCuTracker + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Credit.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipCuTracker(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthCuTracker + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipCuTracker(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/x/subscription/types/types.go b/x/subscription/types/types.go index 313b18da30..a7751856fd 100644 --- a/x/subscription/types/types.go +++ b/x/subscription/types/types.go @@ -11,4 +11,5 @@ const ( DelProjectEventName = "del_project_to_subscription_event" AddTrackedCuEventName = "add_tracked_cu_event" MonthlyCuTrackerProviderRewardEventName = "monthly_cu_tracker_provider_reward" + RemainingCreditEventName = "subscription_remaining_credit" )