diff --git a/x/bundles/keeper/logic_bundles.go b/x/bundles/keeper/logic_bundles.go index 7488597c..65b25956 100644 --- a/x/bundles/keeper/logic_bundles.go +++ b/x/bundles/keeper/logic_bundles.go @@ -514,7 +514,7 @@ func (k Keeper) GetVoteDistribution(ctx sdk.Context, poolId uint64) (voteDistrib } // tallyBundleProposal evaluates the votes of a bundle proposal and determines the outcome -func (k msgServer) tallyBundleProposal(ctx sdk.Context, bundleProposal types.BundleProposal, poolId uint64) (types.TallyResult, error) { +func (k Keeper) tallyBundleProposal(ctx sdk.Context, bundleProposal types.BundleProposal, poolId uint64) (types.TallyResult, error) { // Increase points of stakers who did not vote at all + slash + remove if necessary. // The protocol requires everybody to stay always active. k.handleNonVoters(ctx, poolId) diff --git a/x/bundles/keeper/logic_end_block_handle_upload_timeout.go b/x/bundles/keeper/logic_end_block_handle_upload_timeout.go index fe668431..25d9aedf 100644 --- a/x/bundles/keeper/logic_end_block_handle_upload_timeout.go +++ b/x/bundles/keeper/logic_end_block_handle_upload_timeout.go @@ -69,16 +69,52 @@ func (k Keeper) HandleUploadTimeout(goCtx context.Context) { // We now know that the pool is active and the upload timeout has been reached. - // Now we increase the points of the valaccount - // (if he is still participating in the pool) and select a new one. - if k.stakerKeeper.DoesValaccountExist(ctx, pool.Id, bundleProposal.NextUploader) { - k.addPoint(ctx, pool.Id, bundleProposal.NextUploader) - } + timedoutUploader := bundleProposal.NextUploader - // Update bundle proposal and choose next uploader - bundleProposal.NextUploader = k.chooseNextUploader(ctx, pool.Id) - bundleProposal.UpdatedAt = uint64(ctx.BlockTime().Unix()) + // Check if we have a bundle proposal to validate. + if bundleProposal.StorageId != "" { + // Previous round contains a bundle which needs to be validated now. + result, err := k.tallyBundleProposal(ctx, bundleProposal, pool.Id) + if err != nil { + // If we have an error here we might have an inconsistent state. + continue + } + + switch result.Status { + case types.TallyResultValid: + // Get next uploader from stakers who voted `valid` + nextUploader := k.chooseNextUploaderFromList(ctx, pool.Id, bundleProposal.VotersValid) + + // Finalize bundle by adding it to the store + k.finalizeCurrentBundleProposal(ctx, pool.Id, result.VoteDistribution, result.FundersPayout, result.InflationPayout, result.BundleReward, nextUploader) + + // Register empty bundle with next uploader + bundleProposal = types.BundleProposal{ + PoolId: pool.Id, + NextUploader: nextUploader, + UpdatedAt: uint64(ctx.BlockTime().Unix()), + } + k.SetBundleProposal(ctx, bundleProposal) + default: + // In every other case the bundle is dropped. - k.SetBundleProposal(ctx, bundleProposal) + // Get next uploader from all pool stakers + nextUploader := k.chooseNextUploader(ctx, pool.Id) + + // Drop current bundle and set next uploader + k.dropCurrentBundleProposal(ctx, pool.Id, result.VoteDistribution, nextUploader) + } + } else { + // Update bundle proposal and choose next uploader + bundleProposal.NextUploader = k.chooseNextUploader(ctx, pool.Id) + bundleProposal.UpdatedAt = uint64(ctx.BlockTime().Unix()) + k.SetBundleProposal(ctx, bundleProposal) + } + + // Now we increase the points of the valaccount + // (if he is still participating in the pool) + if k.stakerKeeper.DoesValaccountExist(ctx, pool.Id, timedoutUploader) { + k.addPoint(ctx, pool.Id, timedoutUploader) + } } } diff --git a/x/bundles/keeper/logic_end_block_handle_upload_timeout_test.go b/x/bundles/keeper/logic_end_block_handle_upload_timeout_test.go index 13d79f97..761a1e8b 100644 --- a/x/bundles/keeper/logic_end_block_handle_upload_timeout_test.go +++ b/x/bundles/keeper/logic_end_block_handle_upload_timeout_test.go @@ -27,7 +27,9 @@ TEST CASES - logic_end_block_handle_upload_timeout.go * Staker is next uploader of genesis bundle and upload timeout does pass together with upload interval * Staker is next uploader of bundle proposal and upload interval does not pass * Staker is next uploader of bundle proposal and upload timeout does not pass -* Staker is next uploader of bundle proposal and upload timeout passes +* Staker is next uploader of bundle proposal and upload timeout passes with the previous bundle being valid +* Staker is next uploader of bundle proposal and upload timeout passes with the previous bundle not reaching quorum +* Staker is next uploader of bundle proposal and upload timeout passes with the previous bundle being invalid * Staker with already max points is next uploader of bundle proposal and upload timeout passes * A bundle proposal with no quorum does not reach the upload interval * A bundle proposal with no quorum does reach the upload interval @@ -508,7 +510,7 @@ var _ = Describe("logic_end_block_handle_upload_timeout.go", Ordered, func() { Expect(s.App().DelegationKeeper.GetDelegationAmount(s.Ctx(), i.STAKER_1)).To(Equal(100 * i.KYVE)) }) - It("Staker is next uploader of bundle proposal and upload timeout passes", func() { + It("Staker is next uploader of bundle proposal and upload timeout passes with the previous bundle being valid", func() { // ARRANGE s.RunTxBundlesSuccess(&bundletypes.MsgClaimUploaderRole{ Creator: i.VALADDRESS_0_A, @@ -548,24 +550,191 @@ var _ = Describe("logic_end_block_handle_upload_timeout.go", Ordered, func() { // ASSERT bundleProposal, _ := s.App().BundlesKeeper.GetBundleProposal(s.Ctx(), 0) Expect(bundleProposal.NextUploader).To(Equal(i.STAKER_0)) - Expect(bundleProposal.StorageId).To(Equal("y62A3tfbSNcNYDGoL-eXwzyV-Zc9Q0OVtDvR1biJmNI")) + Expect(bundleProposal.StorageId).To(Equal("")) - // check if next uploader got not removed from pool + // check that previous bundle got finalized + finalizedBundle, _ := s.App().BundlesKeeper.GetFinalizedBundle(s.Ctx(), 0, 0) + Expect(finalizedBundle.Uploader).To(Equal(i.STAKER_0)) + Expect(finalizedBundle.StorageId).To(Equal("y62A3tfbSNcNYDGoL-eXwzyV-Zc9Q0OVtDvR1biJmNI")) + + // check that nobody got removed from the pool poolStakers := s.App().StakersKeeper.GetAllStakerAddressesOfPool(s.Ctx(), 0) Expect(poolStakers).To(HaveLen(2)) - // check if next uploader received a point - valaccount, _ := s.App().StakersKeeper.GetValaccount(s.Ctx(), 0, i.STAKER_1) + // check that staker 0 has no points + valaccount, _ := s.App().StakersKeeper.GetValaccount(s.Ctx(), 0, i.STAKER_0) + Expect(valaccount.Points).To(Equal(uint64(0))) + + // check that staker 1 (next uploader) received a point for not uploading + valaccount, _ = s.App().StakersKeeper.GetValaccount(s.Ctx(), 0, i.STAKER_1) Expect(valaccount.Points).To(Equal(uint64(1))) - _, found := s.App().StakersKeeper.GetStaker(s.Ctx(), i.STAKER_1) - Expect(found).To(BeTrue()) + // check that nobody got slashed + expectedBalance := 100 * i.KYVE + Expect(expectedBalance).To(Equal(s.App().DelegationKeeper.GetDelegationAmountOfDelegator(s.Ctx(), i.STAKER_0, i.STAKER_0))) + Expect(expectedBalance).To(Equal(s.App().DelegationKeeper.GetDelegationAmountOfDelegator(s.Ctx(), i.STAKER_1, i.STAKER_1))) + // pool delegations equals delegations of staker 0 & 1 Expect(s.App().DelegationKeeper.GetDelegationOfPool(s.Ctx(), 0)).To(Equal(200 * i.KYVE)) + }) - // check if next uploader not got slashed + It("Staker is next uploader of bundle proposal and upload timeout passes with the previous bundle not reaching quorum", func() { + // ARRANGE + s.RunTxBundlesSuccess(&bundletypes.MsgClaimUploaderRole{ + Creator: i.VALADDRESS_0_A, + Staker: i.STAKER_0, + PoolId: 0, + }) + + s.CommitAfterSeconds(60) + + s.RunTxBundlesSuccess(&bundletypes.MsgSubmitBundleProposal{ + Creator: i.VALADDRESS_0_A, + Staker: i.STAKER_0, + PoolId: 0, + StorageId: "y62A3tfbSNcNYDGoL-eXwzyV-Zc9Q0OVtDvR1biJmNI", + DataSize: 100, + DataHash: "test_hash", + FromIndex: 0, + BundleSize: 100, + FromKey: "0", + ToKey: "99", + BundleSummary: "test_value", + }) + + // ACT + s.CommitAfterSeconds(s.App().BundlesKeeper.GetUploadTimeout(s.Ctx())) + s.CommitAfterSeconds(60) + s.CommitAfterSeconds(1) + + // ASSERT + bundleProposal, _ := s.App().BundlesKeeper.GetBundleProposal(s.Ctx(), 0) + Expect(bundleProposal.NextUploader).To(Equal(i.STAKER_0)) + Expect(bundleProposal.StorageId).To(Equal("")) + + // check that bundle didn't get finalized + _, found := s.App().BundlesKeeper.GetFinalizedBundle(s.Ctx(), 0, 0) + Expect(found).To(BeFalse()) + + // check that nobody got removed from pool + poolStakers := s.App().StakersKeeper.GetAllStakerAddressesOfPool(s.Ctx(), 0) + Expect(poolStakers).To(HaveLen(2)) + Expect(poolStakers).To(ContainElements(i.STAKER_0, i.STAKER_1)) + + // check that staker 0 (uploader) has no points + valaccount, _ := s.App().StakersKeeper.GetValaccount(s.Ctx(), 0, i.STAKER_0) + Expect(valaccount.Points).To(Equal(uint64(0))) + + // check that staker 1 received a point for not voting + valaccount, _ = s.App().StakersKeeper.GetValaccount(s.Ctx(), 0, i.STAKER_1) + Expect(valaccount.Points).To(Equal(uint64(1))) + + // check that nobody got slashed expectedBalance := 100 * i.KYVE + Expect(expectedBalance).To(Equal(s.App().DelegationKeeper.GetDelegationAmountOfDelegator(s.Ctx(), i.STAKER_0, i.STAKER_0))) Expect(expectedBalance).To(Equal(s.App().DelegationKeeper.GetDelegationAmountOfDelegator(s.Ctx(), i.STAKER_1, i.STAKER_1))) + + _, found = s.App().StakersKeeper.GetStaker(s.Ctx(), i.STAKER_1) + Expect(found).To(BeTrue()) + + // pool delegations equals delegations of staker 0 & 1 + Expect(s.App().DelegationKeeper.GetDelegationOfPool(s.Ctx(), 0)).To(Equal(200 * i.KYVE)) + }) + + It("Staker is next uploader of bundle proposal and upload timeout passes with the previous bundle being invalid", func() { + // ARRANGE + s.RunTxStakersSuccess(&stakertypes.MsgCreateStaker{ + Creator: i.STAKER_2, + Amount: 100 * i.KYVE, + }) + + s.RunTxStakersSuccess(&stakertypes.MsgJoinPool{ + Creator: i.STAKER_2, + PoolId: 0, + Valaddress: i.VALADDRESS_2_A, + }) + + s.RunTxBundlesSuccess(&bundletypes.MsgClaimUploaderRole{ + Creator: i.VALADDRESS_0_A, + Staker: i.STAKER_0, + PoolId: 0, + }) + + s.CommitAfterSeconds(60) + + s.RunTxBundlesSuccess(&bundletypes.MsgSubmitBundleProposal{ + Creator: i.VALADDRESS_0_A, + Staker: i.STAKER_0, + PoolId: 0, + StorageId: "y62A3tfbSNcNYDGoL-eXwzyV-Zc9Q0OVtDvR1biJmNI", + DataSize: 100, + DataHash: "test_hash", + FromIndex: 0, + BundleSize: 100, + FromKey: "0", + ToKey: "99", + BundleSummary: "test_value", + }) + + s.RunTxBundlesSuccess(&bundletypes.MsgVoteBundleProposal{ + Creator: i.VALADDRESS_1_A, + Staker: i.STAKER_1, + PoolId: 0, + StorageId: "y62A3tfbSNcNYDGoL-eXwzyV-Zc9Q0OVtDvR1biJmNI", + Vote: bundletypes.VOTE_TYPE_INVALID, + }) + + s.RunTxBundlesSuccess(&bundletypes.MsgVoteBundleProposal{ + Creator: i.VALADDRESS_2_A, + Staker: i.STAKER_2, + PoolId: 0, + StorageId: "y62A3tfbSNcNYDGoL-eXwzyV-Zc9Q0OVtDvR1biJmNI", + Vote: bundletypes.VOTE_TYPE_INVALID, + }) + + Expect(s.App().DelegationKeeper.GetDelegationOfPool(s.Ctx(), 0)).To(Equal(300 * i.KYVE)) + + // ACT + s.CommitAfterSeconds(s.App().BundlesKeeper.GetUploadTimeout(s.Ctx())) + s.CommitAfterSeconds(60) + s.CommitAfterSeconds(1) + + // ASSERT + bundleProposal, _ := s.App().BundlesKeeper.GetBundleProposal(s.Ctx(), 0) + Expect(bundleProposal.NextUploader).To(Equal(i.STAKER_2)) + Expect(bundleProposal.StorageId).To(Equal("")) + + // check that bundle didn't get finalized + _, found := s.App().BundlesKeeper.GetFinalizedBundle(s.Ctx(), 0, 0) + Expect(found).To(BeFalse()) + + // check that staker 0 (uploader) got removed from pool because his proposal was voted invalid + poolStakers := s.App().StakersKeeper.GetAllStakerAddressesOfPool(s.Ctx(), 0) + Expect(poolStakers).To(HaveLen(2)) + Expect(poolStakers).To(ContainElements(i.STAKER_1, i.STAKER_2)) + + // check that staker 1 (next uploader) received a point for missing the upload + valaccount, _ := s.App().StakersKeeper.GetValaccount(s.Ctx(), 0, i.STAKER_1) + Expect(valaccount.Points).To(Equal(uint64(1))) + + // check that staker 2 has a no points + valaccount, _ = s.App().StakersKeeper.GetValaccount(s.Ctx(), 0, i.STAKER_2) + Expect(valaccount.Points).To(Equal(uint64(0))) + + // check that staker 0 (uploader) got slashed + expectedBalance := 80 * i.KYVE + Expect(expectedBalance).To(Equal(s.App().DelegationKeeper.GetDelegationAmountOfDelegator(s.Ctx(), i.STAKER_0, i.STAKER_0))) + + // check that staker 1 (next uploader) didn't get slashed + expectedBalance = 100 * i.KYVE + Expect(expectedBalance).To(Equal(s.App().DelegationKeeper.GetDelegationAmountOfDelegator(s.Ctx(), i.STAKER_1, i.STAKER_1))) + + // check that staker 2 didn't get slashed + expectedBalance = 100 * i.KYVE + Expect(expectedBalance).To(Equal(s.App().DelegationKeeper.GetDelegationAmountOfDelegator(s.Ctx(), i.STAKER_2, i.STAKER_2))) + + // pool delegations equals delegations of staker 1 & 2 + Expect(s.App().DelegationKeeper.GetDelegationOfPool(s.Ctx(), 0)).To(Equal(200 * i.KYVE)) }) It("Staker with already max points is next uploader of bundle proposal and upload timeout passes", func() { @@ -661,26 +830,40 @@ var _ = Describe("logic_end_block_handle_upload_timeout.go", Ordered, func() { // ASSERT bundleProposal, _ = s.App().BundlesKeeper.GetBundleProposal(s.Ctx(), 0) Expect(bundleProposal.NextUploader).To(Equal(i.STAKER_0)) - Expect(bundleProposal.StorageId).To(Equal("P9edn0bjEfMU_lecFDIPLvGO2v2ltpFNUMWp5kgPddg")) + Expect(bundleProposal.StorageId).To(Equal("")) - // check if next uploader got not removed from pool + // check that previous bundle got finalized + finalizedBundle, _ := s.App().BundlesKeeper.GetFinalizedBundle(s.Ctx(), 0, 0) + Expect(finalizedBundle.Uploader).To(Equal(i.STAKER_0)) + Expect(finalizedBundle.StorageId).To(Equal("y62A3tfbSNcNYDGoL-eXwzyV-Zc9Q0OVtDvR1biJmNI")) + + // check that staker 2 (next uploader) got removed from pool because he didn't upload poolStakers := s.App().StakersKeeper.GetAllStakerAddressesOfPool(s.Ctx(), 0) Expect(poolStakers).To(HaveLen(2)) + Expect(poolStakers).To(ContainElements(i.STAKER_0, i.STAKER_1)) - // check if next uploader received a point - _, valaccountFound := s.App().StakersKeeper.GetValaccount(s.Ctx(), 0, i.STAKER_2) - Expect(valaccountFound).To(BeFalse()) + // check that staker 0 (uploader) has no points + valaccount, _ := s.App().StakersKeeper.GetValaccount(s.Ctx(), 0, i.STAKER_1) + Expect(valaccount.Points).To(Equal(uint64(0))) - _, found := s.App().StakersKeeper.GetStaker(s.Ctx(), i.STAKER_2) - Expect(found).To(BeTrue()) + // check that staker 2 has a no points + valaccount, _ = s.App().StakersKeeper.GetValaccount(s.Ctx(), 0, i.STAKER_2) + Expect(valaccount.Points).To(Equal(uint64(0))) - Expect(s.App().DelegationKeeper.GetDelegationOfPool(s.Ctx(), 0)).To(Equal(200 * i.KYVE)) + // check that staker 0 (uploader) didn't get slashed + expectedBalance := 100 * i.KYVE + Expect(expectedBalance).To(Equal(s.App().DelegationKeeper.GetDelegationAmountOfDelegator(s.Ctx(), i.STAKER_0, i.STAKER_0))) - // check if next uploader not got slashed - slashAmountRatio := s.App().DelegationKeeper.GetTimeoutSlash(s.Ctx()) - expectedBalance := 100*i.KYVE - uint64(math.LegacyNewDec(int64(100*i.KYVE)).Mul(slashAmountRatio).TruncateInt64()) + // check that staker 1 didn't get slashed + expectedBalance = 100 * i.KYVE + Expect(expectedBalance).To(Equal(s.App().DelegationKeeper.GetDelegationAmountOfDelegator(s.Ctx(), i.STAKER_1, i.STAKER_1))) + // check that staker 2 (next uploader) got slashed + expectedBalance = 98 * i.KYVE Expect(expectedBalance).To(Equal(s.App().DelegationKeeper.GetDelegationAmountOfDelegator(s.Ctx(), i.STAKER_2, i.STAKER_2))) + + // pool delegations equals delegations of staker 0 & 1 + Expect(s.App().DelegationKeeper.GetDelegationOfPool(s.Ctx(), 0)).To(Equal(200 * i.KYVE)) }) It("A bundle proposal with no quorum does not reach the upload interval", func() { @@ -910,7 +1093,12 @@ var _ = Describe("logic_end_block_handle_upload_timeout.go", Ordered, func() { // ASSERT bundleProposal, _ := s.App().BundlesKeeper.GetBundleProposal(s.Ctx(), 0) Expect(bundleProposal.NextUploader).To(Equal(i.STAKER_0)) - Expect(bundleProposal.StorageId).To(Equal("y62A3tfbSNcNYDGoL-eXwzyV-Zc9Q0OVtDvR1biJmNI")) + Expect(bundleProposal.StorageId).To(Equal("")) + + // check if previous bundle got finalized + finalizedBundle, _ := s.App().BundlesKeeper.GetFinalizedBundle(s.Ctx(), 0, 0) + Expect(finalizedBundle.Uploader).To(Equal(i.STAKER_0)) + Expect(finalizedBundle.StorageId).To(Equal("y62A3tfbSNcNYDGoL-eXwzyV-Zc9Q0OVtDvR1biJmNI")) // check if next uploader got removed from pool poolStakers := s.App().StakersKeeper.GetAllStakerAddressesOfPool(s.Ctx(), 0) @@ -992,19 +1180,23 @@ var _ = Describe("logic_end_block_handle_upload_timeout.go", Ordered, func() { // ASSERT bundleProposal, _ := s.App().BundlesKeeper.GetBundleProposal(s.Ctx(), 0) Expect(bundleProposal.NextUploader).To(Equal(i.STAKER_2)) - Expect(bundleProposal.StorageId).To(Equal("y62A3tfbSNcNYDGoL-eXwzyV-Zc9Q0OVtDvR1biJmNI")) + Expect(bundleProposal.StorageId).To(Equal("")) - // check if next uploader got removed from pool + // check that bundle didn't get finalized + _, found := s.App().BundlesKeeper.GetFinalizedBundle(s.Ctx(), 0, 0) + Expect(found).To(BeFalse()) + + // check that next uploader got removed from pool poolStakers := s.App().StakersKeeper.GetAllStakerAddressesOfPool(s.Ctx(), 0) Expect(poolStakers).To(HaveLen(2)) - _, found := s.App().StakersKeeper.GetStaker(s.Ctx(), i.STAKER_0) + _, found = s.App().StakersKeeper.GetStaker(s.Ctx(), i.STAKER_0) Expect(found).To(BeTrue()) Expect(s.App().DelegationKeeper.GetDelegationOfPool(s.Ctx(), 0)).To(Equal(200 * i.KYVE)) - // check if next uploader not got slashed - expectedBalance := 100 * i.KYVE + // check that next uploader got slashed for voting invalid + expectedBalance := 80 * i.KYVE Expect(expectedBalance).To(Equal(s.App().DelegationKeeper.GetDelegationAmountOfDelegator(s.Ctx(), i.STAKER_0, i.STAKER_0))) }) @@ -1140,7 +1332,11 @@ var _ = Describe("logic_end_block_handle_upload_timeout.go", Ordered, func() { // ASSERT bundleProposal, _ = s.App().BundlesKeeper.GetBundleProposal(s.Ctx(), 1) Expect(bundleProposal.NextUploader).To(Equal(i.STAKER_0)) - Expect(bundleProposal.StorageId).To(Equal("P9edn0bjEfMU_lecFDIPLvGO2v2ltpFNUMWp5kgPddg")) + Expect(bundleProposal.StorageId).To(Equal("")) + + // check that bundle didn't get finalized + _, found := s.App().BundlesKeeper.GetFinalizedBundle(s.Ctx(), 0, 0) + Expect(found).To(BeFalse()) // check if next uploader got not removed from pool poolStakers := s.App().StakersKeeper.GetAllStakerAddressesOfPool(s.Ctx(), 1) @@ -1150,7 +1346,7 @@ var _ = Describe("logic_end_block_handle_upload_timeout.go", Ordered, func() { _, valaccountFound := s.App().StakersKeeper.GetValaccount(s.Ctx(), 1, i.STAKER_2) Expect(valaccountFound).To(BeFalse()) - _, found := s.App().StakersKeeper.GetStaker(s.Ctx(), i.STAKER_2) + _, found = s.App().StakersKeeper.GetStaker(s.Ctx(), i.STAKER_2) Expect(found).To(BeTrue()) Expect(s.App().DelegationKeeper.GetDelegationOfPool(s.Ctx(), 1)).To(Equal(200 * i.KYVE))