diff --git a/x/bundles/keeper/logic_bundles.go b/x/bundles/keeper/logic_bundles.go index da1f5f0d..70fecfff 100644 --- a/x/bundles/keeper/logic_bundles.go +++ b/x/bundles/keeper/logic_bundles.go @@ -3,6 +3,7 @@ package keeper import ( "cosmossdk.io/errors" "cosmossdk.io/math" + poolTypes "github.com/KYVENetwork/chain/x/pool/types" delegationTypes "github.com/KYVENetwork/chain/x/delegation/types" @@ -510,3 +511,91 @@ func (k Keeper) GetVoteDistribution(ctx sdk.Context, poolId uint64) (voteDistrib return } + +// 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) { + // 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) + + // evaluate all votes and determine status based on the votes weighted with stake + delegation + voteDistribution := k.GetVoteDistribution(ctx, poolId) + + // Handle tally outcome + switch voteDistribution.Status { + case types.BUNDLE_STATUS_VALID: + // charge the funders of the pool + fundersPayout, err := k.fundersKeeper.ChargeFundersOfPool(ctx, poolId) + if err != nil { + return types.TallyResult{}, err + } + + // charge the inflation pool + inflationPayout, err := k.poolKeeper.ChargeInflationPool(ctx, poolId) + if err != nil { + return types.TallyResult{}, err + } + + // calculate payouts to the different stakeholders like treasury, uploader and delegators + bundleReward := k.calculatePayouts(ctx, poolId, fundersPayout+inflationPayout) + + // payout rewards to treasury + if err := util.TransferFromModuleToTreasury(k.accountKeeper, k.distrkeeper, ctx, poolTypes.ModuleName, bundleReward.Treasury); err != nil { + return types.TallyResult{}, err + } + + // payout rewards to uploader through commission rewards + if err := k.stakerKeeper.IncreaseStakerCommissionRewards(ctx, bundleProposal.Uploader, bundleReward.Uploader); err != nil { + return types.TallyResult{}, err + } + + // payout rewards to delegators through delegation rewards + if err := k.delegationKeeper.PayoutRewards(ctx, bundleProposal.Uploader, bundleReward.Delegation, poolTypes.ModuleName); err != nil { + return types.TallyResult{}, err + } + + // slash stakers who voted incorrectly + for _, voter := range bundleProposal.VotersInvalid { + k.slashDelegatorsAndRemoveStaker(ctx, poolId, voter, delegationTypes.SLASH_TYPE_VOTE) + } + + return types.TallyResult{ + Status: types.TallyResultValid, + VoteDistribution: voteDistribution, + FundersPayout: fundersPayout, + InflationPayout: inflationPayout, + BundleReward: bundleReward, + }, nil + case types.BUNDLE_STATUS_INVALID: + // If the bundles is invalid, everybody who voted incorrectly gets slashed. + // The bundle provided by the message-sender is of no mean, because the previous bundle + // turned out to be incorrect. + // There this round needs to start again and the message-sender stays uploader. + + // slash stakers who voted incorrectly - uploader receives upload slash + for _, voter := range bundleProposal.VotersValid { + if voter == bundleProposal.Uploader { + k.slashDelegatorsAndRemoveStaker(ctx, poolId, voter, delegationTypes.SLASH_TYPE_UPLOAD) + } else { + k.slashDelegatorsAndRemoveStaker(ctx, poolId, voter, delegationTypes.SLASH_TYPE_VOTE) + } + } + + return types.TallyResult{ + Status: types.TallyResultInvalid, + VoteDistribution: voteDistribution, + FundersPayout: 0, + InflationPayout: 0, + BundleReward: types.BundleReward{}, + }, nil + default: + // If the bundle is neither valid nor invalid the quorum has not been reached yet. + return types.TallyResult{ + Status: types.TallyResultNoQuorum, + VoteDistribution: voteDistribution, + FundersPayout: 0, + InflationPayout: 0, + BundleReward: types.BundleReward{}, + }, nil + } +} diff --git a/x/bundles/keeper/msg_server_skip_uploader_role.go b/x/bundles/keeper/msg_server_skip_uploader_role.go index 34bff46f..bc5d41e1 100644 --- a/x/bundles/keeper/msg_server_skip_uploader_role.go +++ b/x/bundles/keeper/msg_server_skip_uploader_role.go @@ -15,19 +15,43 @@ func (k msgServer) SkipUploaderRole(goCtx context.Context, msg *types.MsgSkipUpl return nil, err } - pool, _ := k.poolKeeper.GetPool(ctx, msg.PoolId) bundleProposal, _ := k.GetBundleProposal(ctx, msg.PoolId) // reset points of uploader as node has proven to be active k.resetPoints(ctx, msg.PoolId, msg.Staker) + // Previous round contains a bundle which needs to be validated now + result, err := k.tallyBundleProposal(ctx, bundleProposal, msg.PoolId) + if err != nil { + return nil, err + } + // Get next uploader, except the one who skipped nextUploader := k.chooseNextUploader(ctx, msg.PoolId, msg.Staker) - bundleProposal.NextUploader = nextUploader - bundleProposal.UpdatedAt = uint64(ctx.BlockTime().Unix()) + switch result.Status { + case types.TallyResultValid: + // Finalize bundle by adding it to the store + k.finalizeCurrentBundleProposal(ctx, msg.PoolId, result.VoteDistribution, result.FundersPayout, result.InflationPayout, result.BundleReward, nextUploader) - k.SetBundleProposal(ctx, bundleProposal) + // Register empty bundle with next uploader + bundleProposal = types.BundleProposal{ + PoolId: msg.PoolId, + NextUploader: nextUploader, + UpdatedAt: uint64(ctx.BlockTime().Unix()), + } + k.SetBundleProposal(ctx, bundleProposal) + case types.TallyResultInvalid: + // Drop current bundle. + k.dropCurrentBundleProposal(ctx, msg.PoolId, result.VoteDistribution, nextUploader) + case types.TallyResultNoQuorum: + // Set next uploader and update the bundle proposal + bundleProposal.NextUploader = nextUploader + bundleProposal.UpdatedAt = uint64(ctx.BlockTime().Unix()) + k.SetBundleProposal(ctx, bundleProposal) + } + + pool, _ := k.poolKeeper.GetPool(ctx, msg.PoolId) _ = ctx.EventManager().EmitTypedEvent(&types.EventSkippedUploaderRole{ PoolId: msg.PoolId, diff --git a/x/bundles/keeper/msg_server_skip_uploader_role_test.go b/x/bundles/keeper/msg_server_skip_uploader_role_test.go index 8cc5c1f0..a1c5aa0f 100644 --- a/x/bundles/keeper/msg_server_skip_uploader_role_test.go +++ b/x/bundles/keeper/msg_server_skip_uploader_role_test.go @@ -17,6 +17,8 @@ TEST CASES - msg_server_skip_uploader_role.go * Skip uploader role on data bundle if staker is next uploader * Skip uploader on data bundle after uploader role has already been skipped * Skip uploader role on dropped bundle +* Skip uploader role on data bundle with current round containing a valid bundle +* Skip uploader role on data bundle with current round containing an invalid bundle */ @@ -87,6 +89,17 @@ var _ = Describe("msg_server_skip_uploader_role.go", Ordered, func() { PoolId: 0, }) + 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.Commit() s.WaitSeconds(60) @@ -142,6 +155,10 @@ var _ = Describe("msg_server_skip_uploader_role.go", Ordered, func() { // here the next uploader should be always be different after skipping Expect(bundleProposal.NextUploader).To(Equal(i.STAKER_0)) + + // check that the bundle is not finalized + _, found = s.App().BundlesKeeper.GetFinalizedBundle(s.Ctx(), 0, 0) + Expect(found).To(BeFalse()) }) It("Skip uploader on data bundle after uploader role has already been skipped", func() { @@ -186,7 +203,11 @@ var _ = Describe("msg_server_skip_uploader_role.go", Ordered, func() { Expect(bundleProposal.VotersAbstain).To(BeEmpty()) // here the next uploader should be always be different after skipping - Expect(bundleProposal.NextUploader).To(Equal(i.STAKER_1)) + Expect(bundleProposal.NextUploader).NotTo(Equal(i.STAKER_0)) + + // check that the bundle is not finalized + _, found = s.App().BundlesKeeper.GetFinalizedBundle(s.Ctx(), 0, 0) + Expect(found).To(BeFalse()) }) It("Skip uploader role on dropped bundle", func() { @@ -225,6 +246,110 @@ var _ = Describe("msg_server_skip_uploader_role.go", Ordered, func() { Expect(bundleProposal.VotersAbstain).To(BeEmpty()) // here the next uploader should be always be different after skipping - Expect(bundleProposal.NextUploader).To(Equal(i.STAKER_1)) + Expect(bundleProposal.NextUploader).NotTo(Equal(i.STAKER_0)) + + // check that the bundle is not finalized + _, found = s.App().BundlesKeeper.GetFinalizedBundle(s.Ctx(), 0, 0) + Expect(found).To(BeFalse()) + }) + + It("Skip uploader role on data bundle with current round containing a valid bundle", func() { + // ARRANGE + s.RunTxBundlesSuccess(&bundletypes.MsgVoteBundleProposal{ + Creator: i.VALADDRESS_1_A, + Staker: i.STAKER_1, + PoolId: 0, + StorageId: "y62A3tfbSNcNYDGoL-eXwzyV-Zc9Q0OVtDvR1biJmNI", + Vote: bundletypes.VOTE_TYPE_VALID, + }) + + s.Commit() + s.WaitSeconds(60) + + // ACT + s.RunTxBundlesSuccess(&bundletypes.MsgSkipUploaderRole{ + Creator: i.VALADDRESS_1_A, + Staker: i.STAKER_1, + PoolId: 0, + FromIndex: 100, + }) + + // ASSERT + bundleProposal, found := s.App().BundlesKeeper.GetBundleProposal(s.Ctx(), 0) + Expect(found).To(BeTrue()) + + Expect(bundleProposal.PoolId).To(Equal(uint64(0))) + Expect(bundleProposal.NextUploader).To(Equal(i.STAKER_0)) + Expect(bundleProposal.UpdatedAt).NotTo(BeZero()) + Expect(bundleProposal.StorageId).To(BeEmpty()) + Expect(bundleProposal.Uploader).To(BeEmpty()) + Expect(bundleProposal.DataSize).To(BeZero()) + Expect(bundleProposal.DataHash).To(BeEmpty()) + Expect(bundleProposal.BundleSize).To(BeZero()) + Expect(bundleProposal.FromKey).To(BeEmpty()) + Expect(bundleProposal.ToKey).To(BeEmpty()) + Expect(bundleProposal.BundleSummary).To(BeEmpty()) + Expect(bundleProposal.UpdatedAt).NotTo(BeZero()) + Expect(bundleProposal.VotersValid).To(BeEmpty()) + Expect(bundleProposal.VotersInvalid).To(BeEmpty()) + Expect(bundleProposal.VotersAbstain).To(BeEmpty()) + + finalizedBundle, found := s.App().BundlesKeeper.GetFinalizedBundle(s.Ctx(), 0, 0) + Expect(found).To(BeTrue()) + + Expect(finalizedBundle.PoolId).To(Equal(uint64(0))) + Expect(finalizedBundle.Uploader).To(Equal(i.STAKER_0)) + }) + + It("Skip uploader role on data bundle with current round containing an invalid bundle", func() { + // ARRANGE + 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, + }) + + s.Commit() + s.WaitSeconds(60) + + // ACT + s.RunTxBundlesSuccess(&bundletypes.MsgSkipUploaderRole{ + Creator: i.VALADDRESS_1_A, + Staker: i.STAKER_1, + PoolId: 0, + FromIndex: 100, + }) + + // ASSERT + bundleProposal, found := s.App().BundlesKeeper.GetBundleProposal(s.Ctx(), 0) + Expect(found).To(BeTrue()) + + Expect(bundleProposal.PoolId).To(Equal(uint64(0))) + Expect(bundleProposal.NextUploader).To(Equal(i.STAKER_2)) + Expect(bundleProposal.StorageId).To(BeEmpty()) + Expect(bundleProposal.Uploader).To(BeEmpty()) + Expect(bundleProposal.DataSize).To(BeZero()) + Expect(bundleProposal.DataHash).To(BeEmpty()) + Expect(bundleProposal.BundleSize).To(BeZero()) + Expect(bundleProposal.FromKey).To(BeEmpty()) + Expect(bundleProposal.ToKey).To(BeEmpty()) + Expect(bundleProposal.BundleSummary).To(BeEmpty()) + Expect(bundleProposal.UpdatedAt).NotTo(BeZero()) + Expect(bundleProposal.VotersValid).To(BeEmpty()) + Expect(bundleProposal.VotersInvalid).To(BeEmpty()) + Expect(bundleProposal.VotersAbstain).To(BeEmpty()) + + // check that the bundle is not finalized + _, found = s.App().BundlesKeeper.GetFinalizedBundle(s.Ctx(), 0, 0) + Expect(found).To(BeFalse()) }) }) diff --git a/x/bundles/keeper/msg_server_submit_bundle_proposal.go b/x/bundles/keeper/msg_server_submit_bundle_proposal.go index 2185b45e..b37ffd48 100644 --- a/x/bundles/keeper/msg_server_submit_bundle_proposal.go +++ b/x/bundles/keeper/msg_server_submit_bundle_proposal.go @@ -3,14 +3,8 @@ package keeper import ( "context" - "github.com/KYVENetwork/chain/util" "github.com/KYVENetwork/chain/x/bundles/types" sdk "github.com/cosmos/cosmos-sdk/types" - - // Delegation - delegationTypes "github.com/KYVENetwork/chain/x/delegation/types" - // Pool - poolTypes "github.com/KYVENetwork/chain/x/pool/types" ) // SubmitBundleProposal handles the logic of an SDK message that allows protocol nodes to submit a new bundle proposal. @@ -42,99 +36,30 @@ func (k msgServer) SubmitBundleProposal(goCtx context.Context, msg *types.MsgSub } // Previous round contains a bundle which needs to be validated now. + result, err := k.tallyBundleProposal(ctx, bundleProposal, msg.PoolId) + if err != nil { + return nil, err + } - // 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, msg.PoolId) - - // evaluate all votes and determine status based on the votes weighted with stake + delegation - voteDistribution := k.GetVoteDistribution(ctx, msg.PoolId) - - // Handle tally outcome - switch voteDistribution.Status { - - case types.BUNDLE_STATUS_VALID: - // If a bundle is valid the following things happen: - // 1. Funders and Inflation Pool are charged. The total payout is divided - // between the uploader, its delegators and the treasury. - // The appropriate funds are deducted from the total pool funds - // 2. The next uploader is randomly selected based on everybody who - // voted valid on this bundle. - // 3. The bundle is finalized by added it permanently to the state. - // 4. The sender immediately starts the next round by registering - // his new bundle proposal. - - pool, _ := k.poolKeeper.GetPool(ctx, msg.PoolId) - - // charge the funders of the pool - fundersPayout, err := k.fundersKeeper.ChargeFundersOfPool(ctx, msg.PoolId) - if err != nil { - return &types.MsgSubmitBundleProposalResponse{}, err - } - - // charge the inflation pool - inflationPayout, err := k.poolKeeper.ChargeInflationPool(ctx, msg.PoolId) - if err != nil { - return &types.MsgSubmitBundleProposalResponse{}, err - } - - // calculate payouts to the different stakeholders like treasury, uploader and delegators - bundleReward := k.calculatePayouts(ctx, msg.PoolId, fundersPayout+inflationPayout) - - // payout rewards to treasury - if err := util.TransferFromModuleToTreasury(k.accountKeeper, k.distrkeeper, ctx, poolTypes.ModuleName, bundleReward.Treasury); err != nil { - return nil, err - } - - // payout rewards to uploader through commission rewards - if err := k.stakerKeeper.IncreaseStakerCommissionRewards(ctx, bundleProposal.Uploader, bundleReward.Uploader); err != nil { - return nil, err - } - - // payout rewards to delegators through delegation rewards - if err := k.delegationKeeper.PayoutRewards(ctx, bundleProposal.Uploader, bundleReward.Delegation, poolTypes.ModuleName); err != nil { - return nil, err - } - - // slash stakers who voted incorrectly - for _, voter := range bundleProposal.VotersInvalid { - k.slashDelegatorsAndRemoveStaker(ctx, msg.PoolId, voter, delegationTypes.SLASH_TYPE_VOTE) - } - - // Determine next uploader and register next bundle - + switch result.Status { + case types.TallyResultValid: // Get next uploader from stakers who voted `valid` nextUploader := k.chooseNextUploaderFromList(ctx, msg.PoolId, bundleProposal.VotersValid) - k.finalizeCurrentBundleProposal(ctx, pool.Id, voteDistribution, fundersPayout, inflationPayout, bundleReward, nextUploader) + // Finalize bundle by adding it to the store + k.finalizeCurrentBundleProposal(ctx, msg.PoolId, result.VoteDistribution, result.FundersPayout, result.InflationPayout, result.BundleReward, nextUploader) // Register the provided bundle as a new proposal for the next round k.registerBundleProposalFromUploader(ctx, msg, nextUploader) return &types.MsgSubmitBundleProposalResponse{}, nil - case types.BUNDLE_STATUS_INVALID: - // If the bundles is invalid, everybody who voted incorrectly gets slashed. - // The bundle provided by the message-sender is of no mean, because the previous bundle - // turned out to be incorrect. - // There this round needs to start again and the message-sender stays uploader. - - // slash stakers who voted incorrectly - uploader receives upload slash - for _, voter := range bundleProposal.VotersValid { - if voter == bundleProposal.Uploader { - k.slashDelegatorsAndRemoveStaker(ctx, msg.PoolId, voter, delegationTypes.SLASH_TYPE_UPLOAD) - } else { - k.slashDelegatorsAndRemoveStaker(ctx, msg.PoolId, voter, delegationTypes.SLASH_TYPE_VOTE) - } - } - + case types.TallyResultInvalid: // Drop current bundle. Can't register the provided bundle because the previous bundles // needs to be resubmitted first. - k.dropCurrentBundleProposal(ctx, msg.PoolId, voteDistribution, bundleProposal.NextUploader) + k.dropCurrentBundleProposal(ctx, msg.PoolId, result.VoteDistribution, bundleProposal.NextUploader) return &types.MsgSubmitBundleProposalResponse{}, nil - default: - // If the bundle is neither valid nor invalid the quorum has not been reached yet. return nil, types.ErrQuorumNotReached } } diff --git a/x/bundles/types/types.go b/x/bundles/types/types.go index 64ed1d6b..2d2cdce7 100644 --- a/x/bundles/types/types.go +++ b/x/bundles/types/types.go @@ -35,3 +35,19 @@ func (bundleVersionMap BundleVersionMap) GetMap() (versionMap map[int32]uint64) } return } + +type TallyResultStatus uint32 + +const ( + TallyResultValid TallyResultStatus = iota + TallyResultInvalid + TallyResultNoQuorum +) + +type TallyResult struct { + Status TallyResultStatus + VoteDistribution VoteDistribution + FundersPayout uint64 + InflationPayout uint64 + BundleReward BundleReward +}