From e655b460e3cd6abd52c557e1cb9959ed7926eabc Mon Sep 17 00:00:00 2001 From: Oren Laadan Date: Mon, 10 Apr 2023 23:05:26 +0800 Subject: [PATCH 1/5] CNS-352: fixation: add GetAllEntryIndicesWithPrefix() GetAllEntryIndicesWthPrefix() returns list of indices a given prefix. This is useful, for example, to get the list of projects of a subscription. --- common/fixation_entry_index.go | 12 +++++++++--- common/fixation_entry_test.go | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/common/fixation_entry_index.go b/common/fixation_entry_index.go index 9c989e649a..bd3f1c7281 100644 --- a/common/fixation_entry_index.go +++ b/common/fixation_entry_index.go @@ -40,10 +40,11 @@ func (fs FixationStore) removeEntryIndex(ctx sdk.Context, safeIndex string) { store.Delete(types.KeyPrefix(fs.createEntryIndexKey(safeIndex))) } -// GetAllEntryIndex returns all Entry indices -func (fs FixationStore) GetAllEntryIndices(ctx sdk.Context) []string { +// GetAllEntryIndexWithPrefix returns all Entry indices with a given prefix +func (fs FixationStore) GetAllEntryIndicesWithPrefix(ctx sdk.Context, prefix string) []string { store := fs.getEntryIndexStore(ctx) - iterator := sdk.KVStorePrefixIterator(store, []byte{}) + entryPrefix := types.KeyPrefix(fs.createEntryIndexKey(prefix)) + iterator := sdk.KVStorePrefixIterator(store, entryPrefix) defer iterator.Close() // iterate over the store's values and save the indices in a list @@ -57,6 +58,11 @@ func (fs FixationStore) GetAllEntryIndices(ctx sdk.Context) []string { return indexList } +// GetAllEntryIndex returns all Entry indices +func (fs FixationStore) GetAllEntryIndices(ctx sdk.Context) []string { + return fs.GetAllEntryIndicesWithPrefix(ctx, "") +} + func (fs FixationStore) createEntryIndexStoreKey() string { return types.EntryIndexKey + fs.prefix } diff --git a/common/fixation_entry_test.go b/common/fixation_entry_test.go index 89757268a9..612f6ea7e0 100644 --- a/common/fixation_entry_test.go +++ b/common/fixation_entry_test.go @@ -134,6 +134,9 @@ func testWithTemplate(t *testing.T, playbook []template, countObj int, countVS i case "getall": indexList := vs[play.store].GetAllEntryIndices(ctx) require.Equal(t, int(play.count), len(indexList), what) + case "getallprefix": + indexList := vs[play.store].GetAllEntryIndicesWithPrefix(ctx, index) + require.Equal(t, int(play.count), len(indexList), what) } } } @@ -316,3 +319,23 @@ func TestEntriesSort(t *testing.T) { testWithTemplate(t, playbook, 3, 1) } + +func TestGetAllEntries(t *testing.T) { + block0 := int64(10) + + playbook := []template{ + { op: "append", name: "entry #1", index: "prefix1_a", count: block0, coin: 0 }, + { op: "append", name: "entry #1", index: "prefix1_b", count: block0, coin: 1 }, + { op: "append", name: "entry #1", index: "prefix1_c", count: block0, coin: 2 }, + { op: "append", name: "entry #1", index: "prefix2_a", count: block0, coin: 3 }, + { op: "append", name: "entry #1", index: "prefix2_b", count: block0, coin: 4 }, + { op: "append", name: "entry #1", index: "prefix3_a", count: block0, coin: 5 }, + { op: "getall", name: "to check all indices", count: 6 }, + { op: "getallprefix", name: "to check all indices with prefix", index: "prefix", count: 6 }, + { op: "getallprefix", name: "to check indices with prefix1", index: "prefix1", count: 3 }, + { op: "getallprefix", name: "to check indices with prefix2", index: "prefix2", count: 2 }, + { op: "getallprefix", name: "to check indices with prefix3", index: "prefix3", count: 1 }, + } + + testWithTemplate(t, playbook, 6, 1) +} From b9d69dcc9cce8b462f6f20502f739503df3f5ff6 Mon Sep 17 00:00:00 2001 From: Oren Laadan Date: Mon, 3 Apr 2023 17:53:14 +0800 Subject: [PATCH 2/5] CNS-352: re-charge subscription, projects at subscription's month end Re-charge the subscription CU at the end of subscription's month, and also use project's snapshot functionality to re-charge the projects too. Signed-off-by: Oren Laadan --- x/projects/keeper/creation.go | 16 ++++++++++++++-- x/projects/types/keys.go | 2 +- x/projects/types/project.go | 2 +- x/subscription/keeper/epoch_start.go | 5 ++++- x/subscription/keeper/subscription.go | 20 ++++++++++++++++++++ x/subscription/types/expected_keepers.go | 1 + 6 files changed, 41 insertions(+), 5 deletions(-) diff --git a/x/projects/keeper/creation.go b/x/projects/keeper/creation.go index 47ee81e91e..44849f8899 100644 --- a/x/projects/keeper/creation.go +++ b/x/projects/keeper/creation.go @@ -15,7 +15,7 @@ func (k Keeper) CreateAdminProject(ctx sdk.Context, subscriptionAddress string, // add a new project to the subscription func (k Keeper) CreateProject(ctx sdk.Context, subscriptionAddress string, projectName string, adminAddress string, enable bool, totalCU uint64, cuPerEpoch uint64, providers uint64, geolocation uint64, vrfpk string) error { - project := types.CreateProject(subscriptionAddress, projectName) + project := types.NewProject(subscriptionAddress, projectName) var emptyProject types.Project blockHeight := uint64(ctx.BlockHeight()) @@ -38,6 +38,7 @@ func (k Keeper) CreateProject(ctx sdk.Context, subscriptionAddress string, proje } project.Enabled = enable + return k.projectsFS.AppendEntry(ctx, project.Index, blockHeight, &project) } @@ -57,8 +58,19 @@ func (k Keeper) RegisterDeveloperKey(ctx sdk.Context, developerKey string, proje return nil } +// Snapshot all projects of a given subscription +func (k Keeper) SnapshotSubscriptionProjects(ctx sdk.Context, subscriptionAddr string) { + projects := k.projectsFS.GetAllEntryIndicesWithPrefix(ctx, subscriptionAddr) + for _, projectID := range projects { + err := k.snapshotProject(ctx, projectID) + if err != nil { + panic(err) + } + } +} + // snapshot project, create a snapshot of a project and reset the cu -func (k Keeper) SnapshotProject(ctx sdk.Context, projectID string) error { +func (k Keeper) snapshotProject(ctx sdk.Context, projectID string) error { var project types.Project err, found := k.projectsFS.FindEntry(ctx, projectID, uint64(ctx.BlockHeight()), &project) if err != nil || !found { diff --git a/x/projects/types/keys.go b/x/projects/types/keys.go index 116a29a868..520591cfd5 100644 --- a/x/projects/types/keys.go +++ b/x/projects/types/keys.go @@ -19,7 +19,7 @@ const ( // prefix for the projects fixation store ProjectsFixationPrefix = "prj-fs" - // prefix for the projects fixation store + // prefix for the developer keys fixation store DeveloperKeysFixationPrefix = "dev-fs" ) diff --git a/x/projects/types/project.go b/x/projects/types/project.go index ebd989fe0a..dfc02fb7e1 100644 --- a/x/projects/types/project.go +++ b/x/projects/types/project.go @@ -10,7 +10,7 @@ func ProjectIndex(subscriptionAddress string, projectName string) string { return subscriptionAddress + "-" + projectName } -func CreateProject(subscriptionAddress string, projectName string) Project { +func NewProject(subscriptionAddress string, projectName string) Project { return Project{ Index: ProjectIndex(subscriptionAddress, projectName), Subscription: subscriptionAddress, diff --git a/x/subscription/keeper/epoch_start.go b/x/subscription/keeper/epoch_start.go index 12de076d96..16c9e36046 100644 --- a/x/subscription/keeper/epoch_start.go +++ b/x/subscription/keeper/epoch_start.go @@ -58,8 +58,11 @@ func (k Keeper) EpochStart(ctx sdk.Context) { date = nextMonth(date) sub.MonthExpiryTime = uint64(date.Unix()) - // reset CU allowance for this coming month + // reset subscription CU allowance for this coming month sub.MonthCuLeft = sub.MonthCuTotal + + // reset projects' CU allowance for this coming month + k.projectsKeeper.SnapshotSubscriptionProjects(ctx, sub.Consumer) } else { // duration ended, but don't delete yet - keep around for another // EpochsToSave epochs before removing, to allow for payments for diff --git a/x/subscription/keeper/subscription.go b/x/subscription/keeper/subscription.go index d0a4f543ce..a063743d07 100644 --- a/x/subscription/keeper/subscription.go +++ b/x/subscription/keeper/subscription.go @@ -251,3 +251,23 @@ func (k Keeper) CreateSubscription( return nil } + +func (k Keeper) ChargeSubscription(ctx sdk.Context, consumer string, cuAmount uint64) error { + sub, found := k.GetSubscription(ctx, consumer) + if !found { + details := map[string]string{"consumer": consumer} + return utils.LavaError(ctx, k.Logger(ctx), "ChargeSubscription", details, "invalid subscription") + } + + // TODO: if subscription is exhausted, should we push back (and the provider + // may not be paid? + + if sub.MonthCuLeft < cuAmount { + sub.MonthCuLeft = 0 + } else { + sub.MonthCuLeft -= cuAmount + } + + k.SetSubscription(ctx, sub) + return nil +} diff --git a/x/subscription/types/expected_keepers.go b/x/subscription/types/expected_keepers.go index 04945839da..19511f947f 100644 --- a/x/subscription/types/expected_keepers.go +++ b/x/subscription/types/expected_keepers.go @@ -28,6 +28,7 @@ type EpochstorageKeeper interface { type ProjectsKeeper interface { CreateAdminProject(ctx sdk.Context, subscriptionAddress string, totalCU uint64, cuPerEpoch uint64, providers uint64, vrfpk string) error DeleteProject(ctx sdk.Context, index string) error + SnapshotSubscriptionProjects(ctx sdk.Context, subscriptionAddr string) // Methods imported from projectskeeper should be defined here } From b88eb25a6e66551aa07788229e175f2140c37e08 Mon Sep 17 00:00:00 2001 From: Oren Laadan Date: Mon, 3 Apr 2023 17:55:32 +0800 Subject: [PATCH 3/5] CNS-352: add tests for re-charge of subscription, project at month end Signed-off-by: Oren Laadan --- x/subscription/keeper/subscription_test.go | 48 +++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/x/subscription/keeper/subscription_test.go b/x/subscription/keeper/subscription_test.go index 2f6f262274..fbd010a952 100644 --- a/x/subscription/keeper/subscription_test.go +++ b/x/subscription/keeper/subscription_test.go @@ -119,7 +119,10 @@ func (ts *testStruct) expireSubscription(sub types.Subscription) types.Subscript func setupTestStruct(t *testing.T, numPlans int) testStruct { _, keepers, _ctx := keepertest.InitAllKeepers(t) + + _ctx = keepertest.AdvanceEpoch(_ctx, keepers) ctx := sdk.UnwrapSDKContext(_ctx) + plans := createNPlans(&keepers.Plans, ctx, numPlans) ts := testStruct{ @@ -278,7 +281,7 @@ func TestRenewSubscription(t *testing.T) { sub, found := keeper.GetSubscription(ts.ctx, creator) require.True(t, found) - // fast-forward two months + // fast-forward three months sub = ts.expireSubscription(sub) sub = ts.expireSubscription(sub) sub = ts.expireSubscription(sub) @@ -317,6 +320,49 @@ func TestSubscriptionAdminProject(t *testing.T) { require.Nil(t, err) } +func TestMonthlyRechargeCU(t *testing.T) { + ts := setupTestStruct(t, 1) + keeper := ts.keepers.Subscription + projectKeeper := ts.keepers.Projects + + account := common.CreateNewAccount(ts._ctx, *ts.keepers, 10000) + creator := account.Addr.String() + + err := keeper.CreateSubscription(ts.ctx, creator, creator, ts.plans[0].Index, 2, "") + require.Nil(t, err) + + block := uint64(ts.ctx.BlockHeight()) + + sub, found := keeper.GetSubscription(ts.ctx, creator) + require.True(t, found) + + // use the subscription and the project + keeper.ChargeSubscription(ts.ctx, creator, 1000) + require.Equal(t, sub.PrevCuLeft, sub.MonthCuTotal-1000) + err = projectKeeper.AddComputeUnitsToProject(ts.ctx, creator, block, 1000) + require.Nil(t, err) + // verify that project used the CU + proj, _, err := projectKeeper.GetProjectForDeveloper(ts.ctx, creator, block) + require.Nil(t, err) + require.Equal(t, uint64(1000), proj.UsedCu) + + // fast-forward one months + sub = ts.expireSubscription(sub) + require.Equal(t, uint64(1), sub.DurationLeft) + + // check that subscription and project have renewed CUs, and that the + // project created a snapshot for last month + sub, found = keeper.GetSubscription(ts.ctx, creator) + require.True(t, found) + require.Equal(t, sub.MonthCuLeft, sub.MonthCuTotal) + proj, _, err = projectKeeper.GetProjectForDeveloper(ts.ctx, creator, block) + require.Nil(t, err) + require.Equal(t, uint64(1000), proj.UsedCu) + proj, _, err = projectKeeper.GetProjectForDeveloper(ts.ctx, creator, uint64(ts.ctx.BlockHeight())) + require.Nil(t, err) + require.Equal(t, uint64(0), proj.UsedCu) +} + func TestExpiryTime(t *testing.T) { ts := setupTestStruct(t, 1) keeper := ts.keepers.Subscription From e9ca5c0072bf0889dfd735bb319c98439f977d90 Mon Sep 17 00:00:00 2001 From: Oren Laadan Date: Mon, 3 Apr 2023 17:57:10 +0800 Subject: [PATCH 4/5] CNS-352: fix calls to ProjectDelete() in testing Signed-off-by: Oren Laadan --- x/pairing/keeper/pairing_subscription_test.go | 37 +++++++++++++++---- x/subscription/keeper/subscription_test.go | 8 +++- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/x/pairing/keeper/pairing_subscription_test.go b/x/pairing/keeper/pairing_subscription_test.go index a35af1263c..30418dbc02 100644 --- a/x/pairing/keeper/pairing_subscription_test.go +++ b/x/pairing/keeper/pairing_subscription_test.go @@ -15,32 +15,55 @@ import ( func TestGetPairingForSubscription(t *testing.T) { ts := setupForPaymentTest(t) var balance int64 = 10000 - consumer := common.CreateNewAccount(ts.ctx, *ts.keepers, balance) + + consumer := common.CreateNewAccount(ts.ctx, *ts.keepers, balance).Addr.String() vrfpk_stub := "testvrfpk" - _, err := ts.servers.SubscriptionServer.Buy(ts.ctx, &subtypes.MsgBuy{Creator: consumer.Addr.String(), Consumer: consumer.Addr.String(), Index: ts.plan.Index, Duration: 1, Vrfpk: vrfpk_stub}) + msgBuy := &subtypes.MsgBuy{ + Creator: consumer, + Consumer: consumer, + Index: ts.plan.Index, + Duration: 1, + Vrfpk: vrfpk_stub, + } + _, err := ts.servers.SubscriptionServer.Buy(ts.ctx, msgBuy) require.Nil(t, err) ts.ctx = testkeeper.AdvanceEpoch(ts.ctx, ts.keepers) + ctx := sdk.UnwrapSDKContext(ts.ctx) - pairingReq := types.QueryGetPairingRequest{ChainID: ts.spec.Index, Client: consumer.Addr.String()} + pairingReq := types.QueryGetPairingRequest{ + ChainID: ts.spec.Index, + Client: consumer, + } pairing, err := ts.keepers.Pairing.GetPairing(ts.ctx, &pairingReq) require.Nil(t, err) - verifyPairingQuery := &types.QueryVerifyPairingRequest{ChainID: ts.spec.Index, Client: consumer.Addr.String(), Provider: pairing.Providers[0].Address, Block: uint64(sdk.UnwrapSDKContext(ts.ctx).BlockHeight())} + verifyPairingQuery := &types.QueryVerifyPairingRequest{ + ChainID: ts.spec.Index, + Client: consumer, + Provider: pairing.Providers[0].Address, + Block: uint64(ctx.BlockHeight()), + } vefiry, err := ts.keepers.Pairing.VerifyPairing(ts.ctx, verifyPairingQuery) require.Nil(t, err) require.True(t, vefiry.Valid) - project, vrfpk, err := ts.keepers.Projects.GetProjectForDeveloper(sdk.UnwrapSDKContext(ts.ctx), consumer.Addr.String(), uint64(sdk.UnwrapSDKContext(ts.ctx).BlockHeight())) + project, vrfpk, err := ts.keepers.Projects.GetProjectForDeveloper(ctx, consumer, uint64(ctx.BlockHeight())) require.Nil(t, err) require.Equal(t, vrfpk, vrfpk_stub) - err = ts.keepers.Projects.DeleteProject(sdk.UnwrapSDKContext(ts.ctx), project.Index) + + err = ts.keepers.Projects.DeleteProject(ctx, project.Index) require.Nil(t, err) _, err = ts.keepers.Pairing.GetPairing(ts.ctx, &pairingReq) require.NotNil(t, err) - verifyPairingQuery = &types.QueryVerifyPairingRequest{ChainID: ts.spec.Index, Client: consumer.Addr.String(), Provider: pairing.Providers[0].Address, Block: uint64(sdk.UnwrapSDKContext(ts.ctx).BlockHeight())} + verifyPairingQuery = &types.QueryVerifyPairingRequest{ + ChainID: ts.spec.Index, + Client: consumer, + Provider: pairing.Providers[0].Address, + Block: uint64(ctx.BlockHeight()), + } vefiry, err = ts.keepers.Pairing.VerifyPairing(ts.ctx, verifyPairingQuery) require.NotNil(t, err) require.False(t, vefiry.Valid) diff --git a/x/subscription/keeper/subscription_test.go b/x/subscription/keeper/subscription_test.go index fbd010a952..7ae8dad895 100644 --- a/x/subscription/keeper/subscription_test.go +++ b/x/subscription/keeper/subscription_test.go @@ -422,8 +422,10 @@ func TestExpiryTime(t *testing.T) { require.Equal(t, tt.months, sub.DurationTotal) keeper.RemoveSubscription(ts.ctx, creator) + // TODO: remove when RemoveSubscriptions properly removes projects - ts.keepers.Projects.DeleteProject(ts.ctx, projectstypes.ProjectIndex(creator, "default")) + projectID := projectstypes.ProjectIndex(creator, projectstypes.ADMIN_PROJECT_NAME) + ts.keepers.Projects.DeleteProject(ts.ctx, projectID) }) } } @@ -467,8 +469,10 @@ func TestPrice(t *testing.T) { require.Equal(t, balance.Amount.Int64(), int64(10000-tt.cost)) keeper.RemoveSubscription(ts.ctx, creator) + // TODO: remove when RemoveSubscriptions properly removes projects - ts.keepers.Projects.DeleteProject(ts.ctx, projectstypes.ProjectIndex(creator, "default")) + projectID := projectstypes.ProjectIndex(creator, projectstypes.ADMIN_PROJECT_NAME) + ts.keepers.Projects.DeleteProject(ts.ctx, projectID) }) } } From c1132dd179250e47bf7f43255cb11142920861c9 Mon Sep 17 00:00:00 2001 From: Oren Laadan Date: Thu, 13 Apr 2023 18:38:59 +0800 Subject: [PATCH 5/5] CNS-352: [review] test that recharge doesn't break with fixation Signed-off-by: Oren Laadan --- x/subscription/keeper/subscription_test.go | 30 ++++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/x/subscription/keeper/subscription_test.go b/x/subscription/keeper/subscription_test.go index 7ae8dad895..975704ec2f 100644 --- a/x/subscription/keeper/subscription_test.go +++ b/x/subscription/keeper/subscription_test.go @@ -331,34 +331,54 @@ func TestMonthlyRechargeCU(t *testing.T) { err := keeper.CreateSubscription(ts.ctx, creator, creator, ts.plans[0].Index, 2, "") require.Nil(t, err) - block := uint64(ts.ctx.BlockHeight()) + block1 := uint64(ts.ctx.BlockHeight()) sub, found := keeper.GetSubscription(ts.ctx, creator) require.True(t, found) + ts._ctx = keepertest.AdvanceEpoch(ts._ctx, ts.keepers) + ts.ctx = sdk.UnwrapSDKContext(ts._ctx) + // use the subscription and the project keeper.ChargeSubscription(ts.ctx, creator, 1000) require.Equal(t, sub.PrevCuLeft, sub.MonthCuTotal-1000) - err = projectKeeper.AddComputeUnitsToProject(ts.ctx, creator, block, 1000) + err = projectKeeper.AddComputeUnitsToProject(ts.ctx, creator, block1, 1000) require.Nil(t, err) + // verify that project used the CU - proj, _, err := projectKeeper.GetProjectForDeveloper(ts.ctx, creator, block) + proj, _, err := projectKeeper.GetProjectForDeveloper(ts.ctx, creator, block1) require.Nil(t, err) require.Equal(t, uint64(1000), proj.UsedCu) + block2 := uint64(ts.ctx.BlockHeight()) + + // force fixation entry (by adding project key) + projKey := []projectstypes.ProjectKey{ + { + Key: common.CreateNewAccount(ts._ctx, *ts.keepers, 10000).Addr.String(), + Types: []projectstypes.ProjectKey_KEY_TYPE{projectstypes.ProjectKey_ADMIN}, + }, + } + projectKeeper.AddKeysToProject(ts.ctx, projectstypes.ADMIN_PROJECT_NAME, creator, projKey) + // fast-forward one months sub = ts.expireSubscription(sub) require.Equal(t, uint64(1), sub.DurationLeft) + block3 := uint64(ts.ctx.BlockHeight()) + // check that subscription and project have renewed CUs, and that the // project created a snapshot for last month sub, found = keeper.GetSubscription(ts.ctx, creator) require.True(t, found) require.Equal(t, sub.MonthCuLeft, sub.MonthCuTotal) - proj, _, err = projectKeeper.GetProjectForDeveloper(ts.ctx, creator, block) + proj, _, err = projectKeeper.GetProjectForDeveloper(ts.ctx, creator, block1) + require.Nil(t, err) + require.Equal(t, uint64(1000), proj.UsedCu) + proj, _, err = projectKeeper.GetProjectForDeveloper(ts.ctx, creator, block2) require.Nil(t, err) require.Equal(t, uint64(1000), proj.UsedCu) - proj, _, err = projectKeeper.GetProjectForDeveloper(ts.ctx, creator, uint64(ts.ctx.BlockHeight())) + proj, _, err = projectKeeper.GetProjectForDeveloper(ts.ctx, creator, block3) require.Nil(t, err) require.Equal(t, uint64(0), proj.UsedCu) }