diff --git a/sei-cosmos/proto/cosmos/gov/v1beta1/genesis.proto b/sei-cosmos/proto/cosmos/gov/v1beta1/genesis.proto index 8229d3c0b4..5403109227 100644 --- a/sei-cosmos/proto/cosmos/gov/v1beta1/genesis.proto +++ b/sei-cosmos/proto/cosmos/gov/v1beta1/genesis.proto @@ -41,4 +41,22 @@ message GenesisState { (gogoproto.nullable) = false, (gogoproto.moretags) = "yaml:\"tally_params\"" ]; + // vote_delegation_snapshots defines the delegation shares captured with each vote. + repeated VoteDelegationSnapshot vote_delegation_snapshots = 8 [(gogoproto.nullable) = false]; +} + +// VoteDelegationSnapshot defines the per-validator delegation shares captured with a vote. +message VoteDelegationSnapshot { + uint64 proposal_id = 1 [(gogoproto.moretags) = "yaml:\"proposal_id\""]; + string voter = 2; + repeated VoteDelegation delegations = 3 [(gogoproto.nullable) = false]; +} + +// VoteDelegation defines a voter's shares in one validator. +message VoteDelegation { + string validator = 1; + string shares = 2 [ + (gogoproto.customtype) = "github.com/sei-protocol/sei-chain/sei-cosmos/types.Dec", + (gogoproto.nullable) = false + ]; } diff --git a/sei-cosmos/x/gov/abci.go b/sei-cosmos/x/gov/abci.go index 528c7ed707..1bf98457cb 100644 --- a/sei-cosmos/x/gov/abci.go +++ b/sei-cosmos/x/gov/abci.go @@ -13,7 +13,13 @@ import ( var logger = seilog.NewLogger("cosmos", "x", "gov") -// EndBlocker called every block, process inflation, update validator set. +// MaxVotesProcessedPerBlock is the governance vote-record budget shared by tallying and cleanup. +const MaxVotesProcessedPerBlock = 1000 + +// minTallyCleanupVotesPerBlock reserves part of the budget for completed tally archives. +const minTallyCleanupVotesPerBlock = 100 + +// EndBlocker expires governance proposals and advances bounded vote tally work. func EndBlocker(ctx sdk.Context, keeper keeper.Keeper) { endBlockerStart := time.Now() defer func() { @@ -50,11 +56,18 @@ func EndBlocker(ctx sdk.Context, keeper keeper.Keeper) { return false }) + remainingVotes := MaxVotesProcessedPerBlock + remainingVotes -= keeper.CleanupTallyVotes(ctx, minTallyCleanupVotesPerBlock) + // fetch active proposals whose voting periods have ended (are passed the block time) keeper.IterateActiveProposalsQueue(ctx, ctx.BlockHeader().Time, func(proposal types.Proposal) bool { var tagValue, logMsg string - passes, burnDeposits, tallyResults := keeper.Tally(ctx, proposal) + complete, processed, passes, burnDeposits, tallyResults := keeper.TallyIncremental(ctx, proposal, remainingVotes) + remainingVotes -= processed + if !complete { + return true + } // If an expedited proposal fails, we do not want to update // the deposit at this point since the proposal is converted to regular. @@ -141,6 +154,8 @@ func EndBlocker(ctx sdk.Context, keeper keeper.Keeper) { sdk.NewAttribute(types.AttributeKeyProposalResult, tagValue), ), ) - return false + return remainingVotes == 0 }) + + keeper.CleanupTallyVotes(ctx, remainingVotes) } diff --git a/sei-cosmos/x/gov/abci_test.go b/sei-cosmos/x/gov/abci_test.go index 4dd8f55935..aee5c85748 100644 --- a/sei-cosmos/x/gov/abci_test.go +++ b/sei-cosmos/x/gov/abci_test.go @@ -2,6 +2,7 @@ package gov_test import ( "context" + "encoding/binary" "testing" "time" @@ -606,6 +607,129 @@ func TestEndBlockerProposalHandlerFailed(t *testing.T) { gov.EndBlocker(ctx, app.GovKeeper) } +func TestEndBlockerBoundsVoteTallyAndCleanupWork(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + cleanupProposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + app.GovKeeper.ActivateVotingPeriod(ctx, cleanupProposal) + cleanupProposal, found := app.GovKeeper.GetProposal(ctx, cleanupProposal.ProposalId) + require.True(t, found) + for i := 0; i < 101; i++ { + addr := make(sdk.AccAddress, 20) + binary.BigEndian.PutUint64(addr[12:], uint64(i+1)) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + cleanupProposal.ProposalId, + addr, + types.NewNonSplitVoteOption(types.OptionYes), + )) + } + complete, processed, _, _, _ := app.GovKeeper.TallyIncremental(ctx, cleanupProposal, 101) + require.True(t, complete) + require.Equal(t, 101, processed) + app.GovKeeper.RemoveFromActiveProposalQueue(ctx, cleanupProposal.ProposalId, cleanupProposal.VotingEndTime) + cleanupProposal.Status = types.StatusRejected + app.GovKeeper.SetProposal(ctx, cleanupProposal) + + proposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + app.GovKeeper.ActivateVotingPeriod(ctx, proposal) + proposal, found = app.GovKeeper.GetProposal(ctx, proposal.ProposalId) + require.True(t, found) + + for i := 0; i < gov.MaxVotesProcessedPerBlock+1; i++ { + addr := make(sdk.AccAddress, 20) + binary.BigEndian.PutUint64(addr[12:], uint64(i+1)) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + addr, + types.NewNonSplitVoteOption(types.OptionYes), + )) + } + + ctx = ctx.WithBlockTime(proposal.VotingEndTime) + gov.EndBlocker(ctx, app.GovKeeper) + + proposal, found = app.GovKeeper.GetProposal(ctx, proposal.ProposalId) + require.True(t, found) + require.Equal(t, types.StatusVotingPeriod, proposal.Status) + require.True(t, app.GovKeeper.IsTallying(ctx, proposal.ProposalId)) + require.Len(t, app.GovKeeper.GetVotes(ctx, proposal.ProposalId), gov.MaxVotesProcessedPerBlock+1) + require.Len(t, app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, false), 900) + require.Len(t, app.GovKeeper.GetArchivedTallyVotes(ctx, cleanupProposal.ProposalId, false), 1) + + newVoter := make(sdk.AccAddress, 20) + binary.BigEndian.PutUint64(newVoter[12:], uint64(gov.MaxVotesProcessedPerBlock+2)) + err = app.GovKeeper.AddVote(ctx, proposal.ProposalId, newVoter, types.NewNonSplitVoteOption(types.OptionNo)) + require.ErrorIs(t, err, types.ErrInactiveProposal) + + gov.EndBlocker(ctx, app.GovKeeper) + + proposal, found = app.GovKeeper.GetProposal(ctx, proposal.ProposalId) + require.True(t, found) + require.Equal(t, types.StatusRejected, proposal.Status) + require.False(t, app.GovKeeper.IsTallying(ctx, proposal.ProposalId)) + require.Empty(t, app.GovKeeper.GetVotes(ctx, proposal.ProposalId)) + require.Len(t, app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, false), 103) + + gov.EndBlocker(ctx, app.GovKeeper) + require.Empty(t, app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, false)) +} + +func TestEndBlockerKeepsExpeditedAndRegularTallyArchivesSeparate(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + proposal, err := app.GovKeeper.SubmitProposalWithExpedite(ctx, TestExpeditedProposal, true) + require.NoError(t, err) + app.GovKeeper.ActivateVotingPeriod(ctx, proposal) + proposal, found := app.GovKeeper.GetProposal(ctx, proposal.ProposalId) + require.True(t, found) + + for i := 0; i < 2*gov.MaxVotesProcessedPerBlock+1; i++ { + addr := make(sdk.AccAddress, 20) + binary.BigEndian.PutUint64(addr[12:], uint64(i+1)) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + addr, + types.NewNonSplitVoteOption(types.OptionYes), + )) + } + + ctx = ctx.WithBlockTime(proposal.VotingEndTime) + gov.EndBlocker(ctx, app.GovKeeper) + gov.EndBlocker(ctx, app.GovKeeper) + gov.EndBlocker(ctx, app.GovKeeper) + + proposal, found = app.GovKeeper.GetProposal(ctx, proposal.ProposalId) + require.True(t, found) + require.Equal(t, types.StatusVotingPeriod, proposal.Status) + require.False(t, proposal.IsExpedited) + require.Len(t, app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, true), 1002) + + regularVoter := make(sdk.AccAddress, 20) + binary.BigEndian.PutUint64(regularVoter[12:], uint64(2*gov.MaxVotesProcessedPerBlock+2)) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + regularVoter, + types.NewNonSplitVoteOption(types.OptionNo), + )) + + ctx = ctx.WithBlockTime(proposal.VotingEndTime) + gov.EndBlocker(ctx, app.GovKeeper) + + proposal, found = app.GovKeeper.GetProposal(ctx, proposal.ProposalId) + require.True(t, found) + require.Equal(t, types.StatusRejected, proposal.Status) + require.Len(t, app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, true), 3) + require.Len(t, app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, false), 1) +} + // With expedited proposal's minimum deposit set higher than the default deposit, we must // initialize and deposit an amount depositMultiplier times larger // than the regular min deposit amount. diff --git a/sei-cosmos/x/gov/genesis.go b/sei-cosmos/x/gov/genesis.go index 609f8abc96..81d5adff6e 100644 --- a/sei-cosmos/x/gov/genesis.go +++ b/sei-cosmos/x/gov/genesis.go @@ -30,6 +30,9 @@ func InitGenesis(ctx sdk.Context, ak types.AccountKeeper, bk types.BankKeeper, k for _, vote := range data.Votes { k.SetVote(ctx, vote) } + for _, snapshot := range data.VoteDelegationSnapshots { + k.SetVoteDelegationSnapshot(ctx, snapshot) + } for _, proposal := range data.Proposals { switch proposal.Status { @@ -37,6 +40,9 @@ func InitGenesis(ctx sdk.Context, ak types.AccountKeeper, bk types.BankKeeper, k k.InsertInactiveProposalQueue(ctx, proposal.ProposalId, proposal.DepositEndTime) case types.StatusVotingPeriod: k.InsertActiveProposalQueue(ctx, proposal.ProposalId, proposal.VotingEndTime) + if !proposal.VotingEndTime.After(ctx.BlockTime()) { + k.InitializeTally(ctx, proposal) + } } k.SetProposal(ctx, proposal) } @@ -63,21 +69,24 @@ func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState { var proposalsDeposits types.Deposits var proposalsVotes types.Votes + voteDelegationSnapshots := make([]types.VoteDelegationSnapshot, 0, len(proposals)) for _, proposal := range proposals { deposits := k.GetDeposits(ctx, proposal.ProposalId) proposalsDeposits = append(proposalsDeposits, deposits...) votes := k.GetVotes(ctx, proposal.ProposalId) proposalsVotes = append(proposalsVotes, votes...) + voteDelegationSnapshots = append(voteDelegationSnapshots, k.GetVoteDelegationSnapshots(ctx, proposal)...) } return &types.GenesisState{ - StartingProposalId: startingProposalID, - Deposits: proposalsDeposits, - Votes: proposalsVotes, - Proposals: proposals, - DepositParams: depositParams, - VotingParams: votingParams, - TallyParams: tallyParams, + StartingProposalId: startingProposalID, + Deposits: proposalsDeposits, + Votes: proposalsVotes, + Proposals: proposals, + DepositParams: depositParams, + VotingParams: votingParams, + TallyParams: tallyParams, + VoteDelegationSnapshots: voteDelegationSnapshots, } } diff --git a/sei-cosmos/x/gov/genesis_test.go b/sei-cosmos/x/gov/genesis_test.go index 176735c39e..b4ad98f7cc 100644 --- a/sei-cosmos/x/gov/genesis_test.go +++ b/sei-cosmos/x/gov/genesis_test.go @@ -2,6 +2,7 @@ package gov_test import ( "context" + "encoding/binary" "encoding/json" "testing" @@ -168,3 +169,61 @@ func TestEqualProposals(t *testing.T) { require.Equal(t, state1, state2) require.True(t, state1.Equal(state2)) } + +func TestExportGenesisIncludesVotesFromUnfinishedTally(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + proposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + app.GovKeeper.ActivateVotingPeriod(ctx, proposal) + proposal, found := app.GovKeeper.GetProposal(ctx, proposal.ProposalId) + require.True(t, found) + + for i := 0; i < 3; i++ { + addr := make(sdk.AccAddress, 20) + binary.BigEndian.PutUint64(addr[12:], uint64(i+1)) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + addr, + types.NewNonSplitVoteOption(types.OptionYes), + )) + } + + complete, processed, _, _, _ := app.GovKeeper.TallyIncremental(ctx, proposal, 1) + require.False(t, complete) + require.Equal(t, 1, processed) + + genesis := gov.ExportGenesis(ctx, app.GovKeeper) + require.Len(t, genesis.Votes, 3) + require.Len(t, genesis.VoteDelegationSnapshots, 3) + genesisJSON := app.AppCodec().MustMarshalJSON(genesis) + var decodedGenesis types.GenesisState + app.AppCodec().MustUnmarshalJSON(genesisJSON, &decodedGenesis) + require.True(t, genesis.Equal(decodedGenesis)) + genesis = &decodedGenesis + + importedApp := seiapp.Setup(t, false, false, false) + importedCtx := importedApp.BaseApp.NewContext(false, tmproto.Header{}).WithBlockTime(proposal.VotingEndTime) + gov.InitGenesis( + importedCtx, + importedApp.AccountKeeper, + importedApp.BankKeeper, + importedApp.GovKeeper, + genesis, + ) + + require.True(t, importedApp.GovKeeper.IsTallying(importedCtx, proposal.ProposalId)) + require.Len(t, importedApp.GovKeeper.GetVotes(importedCtx, proposal.ProposalId), 3) + require.Len(t, importedApp.GovKeeper.GetVoteDelegationSnapshots(importedCtx, proposal), 3) + newVoter := make(sdk.AccAddress, 20) + binary.BigEndian.PutUint64(newVoter[12:], 4) + err = importedApp.GovKeeper.AddVote( + importedCtx, + proposal.ProposalId, + newVoter, + types.NewNonSplitVoteOption(types.OptionNo), + ) + require.ErrorIs(t, err, types.ErrInactiveProposal) +} diff --git a/sei-cosmos/x/gov/keeper/grpc_query.go b/sei-cosmos/x/gov/keeper/grpc_query.go index 4beb84fce9..cc18d4861b 100644 --- a/sei-cosmos/x/gov/keeper/grpc_query.go +++ b/sei-cosmos/x/gov/keeper/grpc_query.go @@ -134,8 +134,7 @@ func (q Keeper) Votes(c context.Context, req *types.QueryVotesRequest) (*types.Q var votes types.Votes ctx := sdk.UnwrapSDKContext(c) - store := ctx.KVStore(q.storeKey) - votesStore := prefix.NewStore(store, types.VotesKey(req.ProposalId)) + votesStore := q.visibleVotesStore(ctx, req.ProposalId) pageRes, err := query.Paginate(ctx, votesStore, req.Pagination, func(key []byte, value []byte) error { var vote types.Vote diff --git a/sei-cosmos/x/gov/keeper/grpc_query_test.go b/sei-cosmos/x/gov/keeper/grpc_query_test.go index 3f078d2ca2..efb3eaef32 100644 --- a/sei-cosmos/x/gov/keeper/grpc_query_test.go +++ b/sei-cosmos/x/gov/keeper/grpc_query_test.go @@ -434,6 +434,72 @@ func (suite *KeeperTestSuite) TestGRPCQueryVotes() { } } +func (suite *KeeperTestSuite) TestGRPCQueryVotesDuringIncrementalTally() { + app, ctx, queryClient, addrs := suite.app, suite.ctx, suite.queryClient, suite.addrs + + proposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + suite.Require().NoError(err) + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + for i, addr := range addrs { + option := types.OptionYes + if i == 1 { + option = types.OptionNo + } + suite.Require().NoError(app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + addr, + types.NewNonSplitVoteOption(option), + )) + } + + complete, processed, _, _, _ := app.GovKeeper.TallyIncremental(ctx, proposal, 1) + suite.Require().False(complete) + suite.Require().Equal(1, processed) + archivedVotes := app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, false) + suite.Require().Len(archivedVotes, 1) + + voteResponse, err := queryClient.Vote(gocontext.Background(), &types.QueryVoteRequest{ + ProposalId: proposal.ProposalId, + Voter: archivedVotes[0].Voter, + }) + suite.Require().NoError(err) + suite.Require().Equal(archivedVotes[0], voteResponse.Vote) + + firstPage, err := queryClient.Votes(gocontext.Background(), &types.QueryVotesRequest{ + ProposalId: proposal.ProposalId, + Pagination: &query.PageRequest{Limit: 1, CountTotal: true}, + }) + suite.Require().NoError(err) + suite.Require().Len(firstPage.Votes, 1) + suite.Require().Equal(uint64(2), firstPage.Pagination.Total) + suite.Require().NotEmpty(firstPage.Pagination.NextKey) + + secondPage, err := queryClient.Votes(gocontext.Background(), &types.QueryVotesRequest{ + ProposalId: proposal.ProposalId, + Pagination: &query.PageRequest{Key: firstPage.Pagination.NextKey, Limit: 1}, + }) + suite.Require().NoError(err) + suite.Require().Len(secondPage.Votes, 1) + suite.Require().ElementsMatch(app.GovKeeper.GetVotes(ctx, proposal.ProposalId), append(firstPage.Votes, secondPage.Votes...)) + suite.Require().Len(app.GovKeeper.GetAllVotes(ctx), 2) + + reversePage, err := queryClient.Votes(gocontext.Background(), &types.QueryVotesRequest{ + ProposalId: proposal.ProposalId, + Pagination: &query.PageRequest{Limit: 2, Reverse: true}, + }) + suite.Require().NoError(err) + suite.Require().ElementsMatch(app.GovKeeper.GetVotes(ctx, proposal.ProposalId), reversePage.Votes) + + _, err = queryClient.TallyResult(gocontext.Background(), &types.QueryTallyResultRequest{ProposalId: proposal.ProposalId}) + suite.Require().NoError(err) + suite.Require().True(app.GovKeeper.IsTallying(ctx, proposal.ProposalId)) + suite.Require().Len(app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, false), 1) + suite.Require().Len(app.GovKeeper.GetVotes(ctx, proposal.ProposalId), 2) +} + func (suite *KeeperTestSuite) TestGRPCQueryParams() { queryClient := suite.queryClient diff --git a/sei-cosmos/x/gov/keeper/tally.go b/sei-cosmos/x/gov/keeper/tally.go index e806446cab..92fd716294 100644 --- a/sei-cosmos/x/gov/keeper/tally.go +++ b/sei-cosmos/x/gov/keeper/tally.go @@ -1,125 +1,434 @@ package keeper import ( + "encoding/json" + "fmt" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" ) -// TODO: Break into several smaller functions for clarity +const cleanupCursorUnset byte = 0 -// Tally iterates over the votes and updates the tally of a proposal based on the voting power of the -// voters -func (keeper Keeper) Tally(ctx sdk.Context, proposal types.Proposal) (passes bool, burnDeposits bool, tallyResults types.TallyResult) { - results := make(map[types.VoteOption]sdk.Dec) - results[types.OptionYes] = sdk.ZeroDec() - results[types.OptionAbstain] = sdk.ZeroDec() - results[types.OptionNo] = sdk.ZeroDec() - results[types.OptionNoWithVeto] = sdk.ZeroDec() - - totalVotingPower := sdk.ZeroDec() - currValidators := make(map[string]types.ValidatorGovInfo) - - // fetch all the bonded validators, insert them into currValidators - keeper.sk.IterateBondedValidatorsByPower(ctx, func(index int64, validator stakingtypes.ValidatorI) (stop bool) { - currValidators[validator.GetOperator().String()] = types.NewValidatorGovInfo( - validator.GetOperator(), - validator.GetBondedTokens(), - validator.GetDelegatorShares(), - sdk.ZeroDec(), - types.WeightedVoteOptions{}, - ) +type tallyProgress struct { + Cursor []byte `json:"cursor,omitempty"` + Results tallyOptionResults `json:"results"` + TotalVotingPower sdk.Dec `json:"total_voting_power"` + TotalBondedTokens sdk.Int `json:"total_bonded_tokens"` + TallyParams types.TallyParams `json:"tally_params"` + Validators []tallyValidator `json:"validators"` + Expedited bool `json:"expedited"` +} +type tallyOptionResults struct { + Yes sdk.Dec `json:"yes"` + Abstain sdk.Dec `json:"abstain"` + No sdk.Dec `json:"no"` + NoWithVeto sdk.Dec `json:"no_with_veto"` +} + +type tallyValidator struct { + Address string `json:"address"` + BondedTokens sdk.Int `json:"bonded_tokens"` + DelegatorShares sdk.Dec `json:"delegator_shares"` + ObservedDelegatorShares sdk.Dec `json:"observed_delegator_shares"` + DelegatorResults tallyOptionResults `json:"delegator_results"` + Vote types.WeightedVoteOptions `json:"vote"` +} + +// Tally calculates a proposal's result without changing its tally state. +func (keeper Keeper) Tally(ctx sdk.Context, proposal types.Proposal) (passes bool, burnDeposits bool, tallyResults types.TallyResult) { + progress := keeper.initializeTally(ctx, proposal) + validators := progress.validatorMap() + keeper.IterateVotes(ctx, proposal.ProposalId, func(vote types.Vote) bool { + keeper.addVoteToTally(validators, vote, keeper.voteDelegations(ctx, proposal.ProposalId, progress.Expedited, vote)) return false }) + return keeper.finishTally(progress) +} - keeper.IterateVotes(ctx, proposal.ProposalId, func(vote types.Vote) bool { - // if validator, just record it in the map - voter := sdk.MustAccAddressFromBech32(vote.Voter) +// TallyIncremental processes at most maxVotes vote records and persists an unfinished tally. +func (keeper Keeper) TallyIncremental( + ctx sdk.Context, + proposal types.Proposal, + maxVotes int, +) (complete bool, processed int, passes bool, burnDeposits bool, tallyResults types.TallyResult) { + if maxVotes < 0 { + panic("maximum votes to tally cannot be negative") + } + + progress, found := keeper.getTallyProgress(ctx, proposal.ProposalId) + if !found { + progress = keeper.initializeTally(ctx, proposal) + } else if progress.Expedited != proposal.IsExpedited { + panic(fmt.Sprintf("tally round for proposal %d changed", proposal.ProposalId)) + } + + complete, processed = keeper.processTallyVotes(ctx, proposal.ProposalId, &progress, maxVotes) + if !complete { + keeper.setTallyProgress(ctx, proposal.ProposalId, progress) + return false, processed, false, false, types.EmptyTallyResult() + } + + passes, burnDeposits, tallyResults = keeper.finishTally(progress) + keeper.deleteTallyProgress(ctx, proposal.ProposalId) + keeper.markTallyVotesForCleanup(ctx, proposal.ProposalId, progress.Expedited) + return true, processed, passes, burnDeposits, tallyResults +} - valAddrStr := sdk.ValAddress(voter.Bytes()).String() - if val, ok := currValidators[valAddrStr]; ok { - val.Vote = vote.Options - currValidators[valAddrStr] = val +// IsTallying reports whether a proposal has an unfinished incremental tally. +func (keeper Keeper) IsTallying(ctx sdk.Context, proposalID uint64) bool { + store := ctx.KVStore(keeper.storeKey) + return store.Has(types.TallyProgressKey(proposalID)) +} + +// InitializeTally persists a proposal's tally accumulator when one does not exist. +func (keeper Keeper) InitializeTally(ctx sdk.Context, proposal types.Proposal) { + progress, found := keeper.getTallyProgress(ctx, proposal.ProposalId) + if found { + if progress.Expedited != proposal.IsExpedited { + panic(fmt.Sprintf("tally round for proposal %d changed", proposal.ProposalId)) } + return + } + keeper.setTallyProgress(ctx, proposal.ProposalId, keeper.initializeTally(ctx, proposal)) +} + +// CleanupTallyVotes deletes at most maxVotes vote records archived by completed tallies. +func (keeper Keeper) CleanupTallyVotes(ctx sdk.Context, maxVotes int) (deleted int) { + if maxVotes <= 0 { + return 0 + } - // iterate over all delegations from voter, deduct from any delegated-to validators - keeper.sk.IterateDelegations(ctx, voter, func(index int64, delegation stakingtypes.DelegationI) (stop bool) { - valAddrStr := delegation.GetValidatorAddr().String() + store := ctx.KVStore(keeper.storeKey) + iterator := sdk.KVStorePrefixIterator(store, types.TallyCleanupKeyPrefix) + defer func() { _ = iterator.Close() }() - if val, ok := currValidators[valAddrStr]; ok { - // There is no need to handle the special case that validator address equal to voter address. - // Because voter's voting power will tally again even if there will deduct voter's voting power from validator. - val.DelegatorDeductions = val.DelegatorDeductions.Add(delegation.GetShares()) - currValidators[valAddrStr] = val + for ; iterator.Valid() && deleted < maxVotes; iterator.Next() { + proposalID, expedited := splitTallyCleanupKey(iterator.Key()) + cursor := decodeCleanupCursor(iterator.Value()) + count, complete, nextCursor := keeper.cleanupProposalTallyVotes( + ctx, + proposalID, + expedited, + maxVotes-deleted, + cursor, + ) + deleted += count - // delegation shares * bonded / total shares - votingPower := delegation.GetShares().MulInt(val.BondedTokens).Quo(val.DelegatorShares) + cleanupKey := types.TallyCleanupKey(proposalID, expedited) + if complete { + store.Delete(cleanupKey) + } else { + store.Set(cleanupKey, nextCursor) + } + } - for _, option := range vote.Options { - subPower := votingPower.Mul(option.Weight) - results[option.Option] = results[option.Option].Add(subPower) - } - totalVotingPower = totalVotingPower.Add(votingPower) - } + return deleted +} - return false - }) +func (keeper Keeper) initializeTally(ctx sdk.Context, proposal types.Proposal) tallyProgress { + progress := tallyProgress{ + Results: newTallyOptionResults(), + TotalVotingPower: sdk.ZeroDec(), + TotalBondedTokens: keeper.sk.TotalBondedTokens(ctx), + TallyParams: keeper.GetTallyParams(ctx), + Expedited: proposal.IsExpedited, + } - keeper.deleteVote(ctx, vote.ProposalId, voter) + keeper.sk.IterateBondedValidatorsByPower(ctx, func(_ int64, validator stakingtypes.ValidatorI) bool { + progress.Validators = append(progress.Validators, tallyValidator{ + Address: validator.GetOperator().String(), + BondedTokens: validator.GetBondedTokens(), + DelegatorShares: validator.GetDelegatorShares(), + ObservedDelegatorShares: sdk.ZeroDec(), + DelegatorResults: newTallyOptionResults(), + }) return false }) - // iterate over the validators again to tally their voting power - for _, val := range currValidators { - if len(val.Vote) == 0 { - continue + return progress +} + +func (keeper Keeper) processTallyVotes( + ctx sdk.Context, + proposalID uint64, + progress *tallyProgress, + maxVotes int, +) (complete bool, processed int) { + validators := progress.validatorMap() + + store := ctx.KVStore(keeper.storeKey) + votesPrefix := types.VotesKey(proposalID) + start := votesPrefix + if len(progress.Cursor) != 0 { + start = sdk.PrefixEndBytes(progress.Cursor) + } + iterator := store.Iterator(start, sdk.PrefixEndBytes(votesPrefix)) + defer func() { _ = iterator.Close() }() + + for ; iterator.Valid() && processed < maxVotes; iterator.Next() { + key := append([]byte(nil), iterator.Key()...) + value := append([]byte(nil), iterator.Value()...) + + var vote types.Vote + keeper.cdc.MustUnmarshal(value, &vote) + populateLegacyOption(&vote) + + voter := sdk.MustAccAddressFromBech32(vote.Voter) + snapshotKey := types.VoteDelegationsKey(proposalID, voter) + snapshotValue := store.Get(snapshotKey) + var snapshot types.VoteDelegationSnapshot + if snapshotValue == nil { + snapshot = keeper.snapshotVoteDelegations(ctx, proposalID, voter) + snapshotValue = keeper.cdc.MustMarshal(&snapshot) + } else { + snapshot = keeper.unmarshalVoteDelegations(snapshotValue) } + keeper.addVoteToTally(validators, vote, snapshot) - sharesAfterDeductions := val.DelegatorShares.Sub(val.DelegatorDeductions) - votingPower := sharesAfterDeductions.MulInt(val.BondedTokens).Quo(val.DelegatorShares) + store.Set(types.TallyVoteKey(proposalID, progress.Expedited, voter), value) + store.Set(types.TallyVoteDelegationsKey(proposalID, progress.Expedited, voter), snapshotValue) + store.Delete(key) + store.Delete(snapshotKey) + progress.Cursor = key + processed++ + } - for _, option := range val.Vote { - subPower := votingPower.Mul(option.Weight) - results[option.Option] = results[option.Option].Add(subPower) + return !iterator.Valid(), processed +} + +func (keeper Keeper) addVoteToTally( + validators map[string]*tallyValidator, + vote types.Vote, + snapshot types.VoteDelegationSnapshot, +) { + voter := sdk.MustAccAddressFromBech32(vote.Voter) + if validator, ok := validators[sdk.ValAddress(voter.Bytes()).String()]; ok { + validator.Vote = vote.Options + } + + for _, delegation := range snapshot.Delegations { + validator, ok := validators[delegation.Validator] + if !ok || validator.DelegatorShares.IsZero() { + continue } - totalVotingPower = totalVotingPower.Add(votingPower) + + votingShares := delegation.Shares + validator.ObservedDelegatorShares = validator.ObservedDelegatorShares.Add(votingShares) + votingPower := votingShares.MulInt(validator.BondedTokens).Quo(validator.DelegatorShares) + validator.DelegatorResults.add(vote.Options, votingPower) } +} - tallyParams := keeper.GetTallyParams(ctx) - tallyResults = types.NewTallyResultFromMap(results) +func (keeper Keeper) voteDelegations( + ctx sdk.Context, + proposalID uint64, + expedited bool, + vote types.Vote, +) types.VoteDelegationSnapshot { + voter := sdk.MustAccAddressFromBech32(vote.Voter) + store := ctx.KVStore(keeper.storeKey) + if bz := store.Get(types.VoteDelegationsKey(proposalID, voter)); bz != nil { + return keeper.unmarshalVoteDelegations(bz) + } + if bz := store.Get(types.TallyVoteDelegationsKey(proposalID, expedited, voter)); bz != nil { + return keeper.unmarshalVoteDelegations(bz) + } + return keeper.snapshotVoteDelegations(ctx, proposalID, voter) +} + +func (progress *tallyProgress) validatorMap() map[string]*tallyValidator { + validators := make(map[string]*tallyValidator, len(progress.Validators)) + for i := range progress.Validators { + validator := &progress.Validators[i] + validators[validator.Address] = validator + } + return validators +} - // TODO: Upgrade the spec to cover all of these cases & remove pseudocode. - // If there is no staked coins, the proposal fails - if keeper.sk.TotalBondedTokens(ctx).IsZero() { +func (keeper Keeper) finishTally(progress tallyProgress) (passes bool, burnDeposits bool, tallyResults types.TallyResult) { + for _, validator := range progress.Validators { + progress.addValidatorResults(validator) + } + + tallyResults = progress.Results.tallyResult() + if progress.TotalBondedTokens.IsZero() { return false, false, tallyResults } - // If there is not enough quorum of votes, the proposal fails - percentVoting := totalVotingPower.Quo(keeper.sk.TotalBondedTokens(ctx).ToDec()) - // Get the quorum threshold based on if the proposal is expedited or not - quorumThreshold := tallyParams.GetQuorum(proposal.IsExpedited) - if percentVoting.LT(quorumThreshold) { + percentVoting := progress.TotalVotingPower.Quo(progress.TotalBondedTokens.ToDec()) + if percentVoting.LT(progress.TallyParams.GetQuorum(progress.Expedited)) { return false, true, tallyResults } - // If no one votes (everyone abstains), proposal fails - if totalVotingPower.Sub(results[types.OptionAbstain]).Equal(sdk.ZeroDec()) { + if progress.TotalVotingPower.Sub(progress.Results.Abstain).IsZero() { return false, false, tallyResults } - // If more than 1/3 of voters veto, proposal fails - if results[types.OptionNoWithVeto].Quo(totalVotingPower).GT(tallyParams.VetoThreshold) { + if progress.Results.NoWithVeto.Quo(progress.TotalVotingPower).GT(progress.TallyParams.VetoThreshold) { return false, true, tallyResults } - // If more than threshold of non-abstaining voters vote Yes, proposal passes - // default value for regular proposals is 1/2. For expedited 2/3 - voteYesThreshold := tallyParams.GetThreshold(proposal.IsExpedited) - if results[types.OptionYes].Quo(totalVotingPower.Sub(results[types.OptionAbstain])).GT(voteYesThreshold) { + nonAbstainingPower := progress.TotalVotingPower.Sub(progress.Results.Abstain) + if progress.Results.Yes.Quo(nonAbstainingPower).GT(progress.TallyParams.GetThreshold(progress.Expedited)) { return true, false, tallyResults } - // Otherwise proposal fails return false, false, tallyResults } + +func (progress *tallyProgress) addValidatorResults(validator tallyValidator) { + if validator.DelegatorShares.IsZero() { + return + } + + countedDelegatorShares := validator.ObservedDelegatorShares + delegatorScale := sdk.OneDec() + if countedDelegatorShares.GT(validator.DelegatorShares) { + delegatorScale = validator.DelegatorShares.Quo(countedDelegatorShares) + countedDelegatorShares = validator.DelegatorShares + } + + progress.Results.addScaled(validator.DelegatorResults, delegatorScale) + delegatorVotingPower := countedDelegatorShares.MulInt(validator.BondedTokens).Quo(validator.DelegatorShares) + progress.TotalVotingPower = progress.TotalVotingPower.Add(delegatorVotingPower) + + if len(validator.Vote) == 0 { + return + } + validatorShares := validator.DelegatorShares.Sub(countedDelegatorShares) + validatorVotingPower := validatorShares.MulInt(validator.BondedTokens).Quo(validator.DelegatorShares) + progress.Results.add(validator.Vote, validatorVotingPower) + progress.TotalVotingPower = progress.TotalVotingPower.Add(validatorVotingPower) +} + +func (results *tallyOptionResults) add(options types.WeightedVoteOptions, votingPower sdk.Dec) { + for _, option := range options { + subPower := votingPower.Mul(option.Weight) + switch option.Option { + case types.OptionYes: + results.Yes = results.Yes.Add(subPower) + case types.OptionAbstain: + results.Abstain = results.Abstain.Add(subPower) + case types.OptionNo: + results.No = results.No.Add(subPower) + case types.OptionNoWithVeto: + results.NoWithVeto = results.NoWithVeto.Add(subPower) + default: + panic(fmt.Sprintf("unsupported vote option %s", option.Option)) + } + } +} + +func (results *tallyOptionResults) addScaled(other tallyOptionResults, scale sdk.Dec) { + results.Yes = results.Yes.Add(other.Yes.Mul(scale)) + results.Abstain = results.Abstain.Add(other.Abstain.Mul(scale)) + results.No = results.No.Add(other.No.Mul(scale)) + results.NoWithVeto = results.NoWithVeto.Add(other.NoWithVeto.Mul(scale)) +} + +func newTallyOptionResults() tallyOptionResults { + return tallyOptionResults{ + Yes: sdk.ZeroDec(), + Abstain: sdk.ZeroDec(), + No: sdk.ZeroDec(), + NoWithVeto: sdk.ZeroDec(), + } +} + +func (results tallyOptionResults) tallyResult() types.TallyResult { + return types.NewTallyResult( + results.Yes.TruncateInt(), + results.Abstain.TruncateInt(), + results.No.TruncateInt(), + results.NoWithVeto.TruncateInt(), + ) +} + +func (keeper Keeper) getTallyProgress(ctx sdk.Context, proposalID uint64) (progress tallyProgress, found bool) { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.TallyProgressKey(proposalID)) + if bz == nil { + return tallyProgress{}, false + } + if err := json.Unmarshal(bz, &progress); err != nil { + panic(fmt.Errorf("unmarshal tally progress for proposal %d: %w", proposalID, err)) + } + return progress, true +} + +func (keeper Keeper) setTallyProgress(ctx sdk.Context, proposalID uint64, progress tallyProgress) { + bz, err := json.Marshal(progress) + if err != nil { + panic(fmt.Errorf("marshal tally progress for proposal %d: %w", proposalID, err)) + } + ctx.KVStore(keeper.storeKey).Set(types.TallyProgressKey(proposalID), bz) +} + +func (keeper Keeper) deleteTallyProgress(ctx sdk.Context, proposalID uint64) { + ctx.KVStore(keeper.storeKey).Delete(types.TallyProgressKey(proposalID)) +} + +func (keeper Keeper) markTallyVotesForCleanup(ctx sdk.Context, proposalID uint64, expedited bool) { + store := ctx.KVStore(keeper.storeKey) + iterator := sdk.KVStorePrefixIterator(store, types.TallyVotesKey(proposalID, expedited)) + defer func() { _ = iterator.Close() }() + if iterator.Valid() { + store.Set(types.TallyCleanupKey(proposalID, expedited), []byte{cleanupCursorUnset}) + } +} + +func (keeper Keeper) cleanupProposalTallyVotes( + ctx sdk.Context, + proposalID uint64, + expedited bool, + maxVotes int, + after []byte, +) (deleted int, complete bool, cursor []byte) { + store := ctx.KVStore(keeper.storeKey) + votesPrefix := types.TallyVotesKey(proposalID, expedited) + start := votesPrefix + if len(after) != 0 { + start = sdk.PrefixEndBytes(after) + } + iterator := store.Iterator(start, sdk.PrefixEndBytes(votesPrefix)) + defer func() { _ = iterator.Close() }() + + for ; iterator.Valid() && deleted < maxVotes; iterator.Next() { + cursor = append(cursor[:0], iterator.Key()...) + store.Delete(iterator.Key()) + snapshotKey := append([]byte{types.TallyVoteDelegationsKeyPrefix[0]}, iterator.Key()[1:]...) + store.Delete(snapshotKey) + deleted++ + } + + complete = !iterator.Valid() + if complete { + store.Delete(types.TallyCleanupKey(proposalID, expedited)) + } + return deleted, complete, cursor +} + +func decodeCleanupCursor(value []byte) []byte { + if len(value) == 1 && value[0] == cleanupCursorUnset { + return nil + } + return append([]byte(nil), value...) +} + +func splitTallyCleanupKey(key []byte) (proposalID uint64, expedited bool) { + if len(key) != 10 { + panic(fmt.Sprintf("invalid tally cleanup key length %d", len(key))) + } + proposalID = types.GetProposalIDFromBytes(key[1:9]) + switch key[9] { + case 0: + return proposalID, true + case 1: + return proposalID, false + default: + panic(fmt.Sprintf("invalid tally round %d", key[9])) + } +} diff --git a/sei-cosmos/x/gov/keeper/tally_test.go b/sei-cosmos/x/gov/keeper/tally_test.go index 7c535e504d..6154f473e3 100644 --- a/sei-cosmos/x/gov/keeper/tally_test.go +++ b/sei-cosmos/x/gov/keeper/tally_test.go @@ -499,3 +499,175 @@ func TestTallyValidatorMultipleDelegations(t *testing.T) { require.True(t, tallyResults.Equals(expectedTallyResult)) } + +func TestTallyIncrementalPersistsProgressAndCleansArchivedVotes(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + addrs, _ := createValidators(t, ctx, app, []int64{5, 5, 5}) + proposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + for _, addr := range addrs[:3] { + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + addr, + types.NewNonSplitVoteOption(types.OptionYes), + )) + } + + complete, processed, _, _, _ := app.GovKeeper.TallyIncremental(ctx, proposal, 1) + require.False(t, complete) + require.Equal(t, 1, processed) + require.True(t, app.GovKeeper.IsTallying(ctx, proposal.ProposalId)) + require.Len(t, app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, false), 1) + require.Len(t, app.GovKeeper.GetVotes(ctx, proposal.ProposalId), 3) + store := ctx.KVStore(app.GetKey(types.StoreKey)) + require.True(t, store.Has(types.TallyVoteDelegationsKey(proposal.ProposalId, false, addrs[0]))) + require.False(t, store.Has(types.VoteDelegationsKey(proposal.ProposalId, addrs[0]))) + + _, _, queryResult := app.GovKeeper.Tally(ctx, proposal) + require.False(t, queryResult.Equals(types.EmptyTallyResult())) + require.True(t, app.GovKeeper.IsTallying(ctx, proposal.ProposalId)) + require.Len(t, app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, false), 1) + require.Len(t, app.GovKeeper.GetVotes(ctx, proposal.ProposalId), 3) + + err = app.GovKeeper.AddVote(ctx, proposal.ProposalId, addrs[3], types.NewNonSplitVoteOption(types.OptionNo)) + require.ErrorIs(t, err, types.ErrInactiveProposal) + + complete, processed, _, _, _ = app.GovKeeper.TallyIncremental(ctx, proposal, 1) + require.False(t, complete) + require.Equal(t, 1, processed) + + complete, processed, passes, burnDeposits, tallyResult := app.GovKeeper.TallyIncremental(ctx, proposal, 1) + require.True(t, complete) + require.Equal(t, 1, processed) + require.True(t, passes) + require.False(t, burnDeposits) + require.False(t, tallyResult.Equals(types.EmptyTallyResult())) + require.False(t, app.GovKeeper.IsTallying(ctx, proposal.ProposalId)) + require.Empty(t, app.GovKeeper.GetVotes(ctx, proposal.ProposalId)) + require.Len(t, app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, false), 3) + + require.Equal(t, 2, app.GovKeeper.CleanupTallyVotes(ctx, 2)) + require.Len(t, app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, false), 1) + require.Equal(t, 1, app.GovKeeper.CleanupTallyVotes(ctx, 2)) + require.Empty(t, app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, false)) + for _, addr := range addrs[:3] { + require.False(t, store.Has(types.TallyVoteDelegationsKey(proposal.ProposalId, false, addr))) + } +} + +func TestTallyIncrementalIgnoresDelegationsAddedAfterVote(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + addrs, valAddrs := createValidators(t, ctx, app, []int64{5, 5, 5}) + + proposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + addrs[0], + types.NewNonSplitVoteOption(types.OptionYes), + )) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + addrs[3], + types.NewNonSplitVoteOption(types.OptionNo), + )) + + validator, found := app.StakingKeeper.GetValidator(ctx, valAddrs[0]) + require.True(t, found) + snapshotValidatorTokens := validator.GetBondedTokens() + snapshotTotalBonded := app.StakingKeeper.TotalBondedTokens(ctx) + app.GovKeeper.InitializeTally(ctx, proposal) + + delegatedTokens := app.StakingKeeper.TokensFromConsensusPower(ctx, 20) + _, err = app.StakingKeeper.Delegate(ctx, addrs[3], delegatedTokens, stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + + complete, processed, _, _, tallyResult := app.GovKeeper.TallyIncremental(ctx, proposal, 2) + require.True(t, complete) + require.Equal(t, 2, processed) + require.True(t, tallyResult.Yes.Equal(snapshotValidatorTokens)) + require.True(t, tallyResult.No.IsZero()) + totalVotingPower := tallyResult.Yes.Add(tallyResult.Abstain).Add(tallyResult.No).Add(tallyResult.NoWithVeto) + require.False(t, totalVotingPower.GT(snapshotTotalBonded)) +} + +func TestTallyIncrementalKeepsDelegationsRemovedAfterVote(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + addrs, valAddrs := createValidators(t, ctx, app, []int64{5, 5, 5}) + + validator, found := app.StakingKeeper.GetValidator(ctx, valAddrs[0]) + require.True(t, found) + delegatedTokens := app.StakingKeeper.TokensFromConsensusPower(ctx, 20) + _, err := app.StakingKeeper.Delegate(ctx, addrs[3], delegatedTokens, stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + + proposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + addrs[3], + types.NewNonSplitVoteOption(types.OptionNo), + )) + app.GovKeeper.InitializeTally(ctx, proposal) + + delegation, found := app.StakingKeeper.GetDelegation(ctx, addrs[3], valAddrs[0]) + require.True(t, found) + _, err = app.StakingKeeper.Undelegate(ctx, addrs[3], valAddrs[0], delegation.GetShares()) + require.NoError(t, err) + + complete, processed, _, burnDeposits, tallyResult := app.GovKeeper.TallyIncremental(ctx, proposal, 1) + require.True(t, complete) + require.Equal(t, 1, processed) + require.False(t, burnDeposits) + require.True(t, tallyResult.No.Equal(delegatedTokens)) +} + +func TestTallyArchivesExpeditedAndRegularRoundsSeparately(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + addrs, _ := createValidators(t, ctx, app, []int64{5, 5, 5}) + + proposal, err := app.GovKeeper.SubmitProposalWithExpedite(ctx, TestExpeditedProposal, true) + require.NoError(t, err) + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + addrs[0], + types.NewNonSplitVoteOption(types.OptionYes), + )) + + complete, _, _, _, _ := app.GovKeeper.TallyIncremental(ctx, proposal, 1) + require.True(t, complete) + + proposal.IsExpedited = false + app.GovKeeper.SetProposal(ctx, proposal) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + addrs[0], + types.NewNonSplitVoteOption(types.OptionNo), + )) + complete, _, _, _, _ = app.GovKeeper.TallyIncremental(ctx, proposal, 1) + require.True(t, complete) + + require.Len(t, app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, true), 1) + require.Len(t, app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, false), 1) +} diff --git a/sei-cosmos/x/gov/keeper/vote.go b/sei-cosmos/x/gov/keeper/vote.go index 1988281eb8..4ea363c2fc 100644 --- a/sei-cosmos/x/gov/keeper/vote.go +++ b/sei-cosmos/x/gov/keeper/vote.go @@ -3,9 +3,13 @@ package keeper import ( "fmt" + "github.com/sei-protocol/sei-chain/sei-cosmos/store/cachekv" + "github.com/sei-protocol/sei-chain/sei-cosmos/store/prefix" + storetypes "github.com/sei-protocol/sei-chain/sei-cosmos/store/types" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" sdkerrors "github.com/sei-protocol/sei-chain/sei-cosmos/types/errors" "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" + stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" ) // AddVote adds a vote on a specific proposal @@ -17,6 +21,9 @@ func (keeper Keeper) AddVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.A if proposal.Status != types.StatusVotingPeriod { return sdkerrors.Wrapf(types.ErrInactiveProposal, "%d", proposalID) } + if keeper.IsTallying(ctx, proposalID) { + return sdkerrors.Wrapf(types.ErrInactiveProposal, "%d", proposalID) + } for _, option := range options { if !types.ValidWeightedVoteOption(option) { @@ -61,10 +68,27 @@ func (keeper Keeper) GetVotes(ctx sdk.Context, proposalID uint64) (votes types.V return } +// GetArchivedTallyVotes returns votes already processed by an unfinished proposal tally. +func (keeper Keeper) GetArchivedTallyVotes(ctx sdk.Context, proposalID uint64, expedited bool) (votes types.Votes) { + store := ctx.KVStore(keeper.storeKey) + iterator := sdk.KVStorePrefixIterator(store, types.TallyVotesKey(proposalID, expedited)) + defer func() { _ = iterator.Close() }() + + for ; iterator.Valid(); iterator.Next() { + var vote types.Vote + keeper.cdc.MustUnmarshal(iterator.Value(), &vote) + populateLegacyOption(&vote) + votes = append(votes, vote) + } + return votes +} + // GetVote gets the vote from an address on a specific proposal func (keeper Keeper) GetVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress) (vote types.Vote, found bool) { - store := ctx.KVStore(keeper.storeKey) - bz := store.Get(types.VoteKey(proposalID, voterAddr)) + store := keeper.visibleVotesStore(ctx, proposalID) + votesPrefix := types.VotesKey(proposalID) + voteKey := types.VoteKey(proposalID, voterAddr) + bz := store.Get(voteKey[len(votesPrefix):]) if bz == nil { return vote, false } @@ -87,11 +111,81 @@ func (keeper Keeper) SetVote(ctx sdk.Context, vote types.Vote) { addr := sdk.MustAccAddressFromBech32(vote.Voter) store.Set(types.VoteKey(vote.ProposalId, addr), bz) + keeper.setVoteDelegations(ctx, vote.ProposalId, addr) +} + +func (keeper Keeper) setVoteDelegations(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) { + snapshot := keeper.snapshotVoteDelegations(ctx, proposalID, voter) + keeper.setVoteDelegationSnapshot(ctx, snapshot) +} + +func (keeper Keeper) snapshotVoteDelegations( + ctx sdk.Context, + proposalID uint64, + voter sdk.AccAddress, +) types.VoteDelegationSnapshot { + snapshot := types.VoteDelegationSnapshot{ + ProposalId: proposalID, + Voter: voter.String(), + Delegations: []types.VoteDelegation{}, + } + keeper.sk.IterateDelegations(ctx, voter, func(_ int64, delegation stakingtypes.DelegationI) bool { + snapshot.Delegations = append(snapshot.Delegations, types.VoteDelegation{ + Validator: delegation.GetValidatorAddr().String(), + Shares: delegation.GetShares(), + }) + return false + }) + return snapshot +} + +func (keeper Keeper) setVoteDelegationSnapshot(ctx sdk.Context, snapshot types.VoteDelegationSnapshot) { + voter := sdk.MustAccAddressFromBech32(snapshot.Voter) + bz := keeper.cdc.MustMarshal(&snapshot) + ctx.KVStore(keeper.storeKey).Set(types.VoteDelegationsKey(snapshot.ProposalId, voter), bz) +} + +// SetVoteDelegationSnapshot stores a vote's exported delegation snapshot. +func (keeper Keeper) SetVoteDelegationSnapshot(ctx sdk.Context, snapshot types.VoteDelegationSnapshot) { + keeper.setVoteDelegationSnapshot(ctx, snapshot) +} + +// GetVoteDelegationSnapshots returns the stored delegation snapshots for a proposal's visible votes. +func (keeper Keeper) GetVoteDelegationSnapshots( + ctx sdk.Context, + proposal types.Proposal, +) []types.VoteDelegationSnapshot { + snapshots := make([]types.VoteDelegationSnapshot, 0) + keeper.IterateVotes(ctx, proposal.ProposalId, func(vote types.Vote) bool { + snapshots = append(snapshots, keeper.voteDelegations(ctx, proposal.ProposalId, proposal.IsExpedited, vote)) + return false + }) + return snapshots +} + +func (keeper Keeper) unmarshalVoteDelegations(bz []byte) types.VoteDelegationSnapshot { + var snapshot types.VoteDelegationSnapshot + keeper.cdc.MustUnmarshal(bz, &snapshot) + return snapshot } // IterateAllVotes iterates over the all the stored votes and performs a callback function func (keeper Keeper) IterateAllVotes(ctx sdk.Context, cb func(vote types.Vote) (stop bool)) { store := ctx.KVStore(keeper.storeKey) + progressIterator := sdk.KVStorePrefixIterator(store, types.TallyProgressKeyPrefix) + for ; progressIterator.Valid(); progressIterator.Next() { + proposalID := types.GetProposalIDFromBytes(progressIterator.Key()[len(types.TallyProgressKeyPrefix):]) + progress, found := keeper.getTallyProgress(ctx, proposalID) + if !found { + continue + } + if keeper.iterateVoteStore(prefix.NewStore(store, types.TallyVotesKey(proposalID, progress.Expedited)), cb) { + _ = progressIterator.Close() + return + } + } + _ = progressIterator.Close() + iterator := sdk.KVStorePrefixIterator(store, types.VotesKeyPrefix) defer func() { _ = iterator.Close() }() @@ -108,8 +202,11 @@ func (keeper Keeper) IterateAllVotes(ctx sdk.Context, cb func(vote types.Vote) ( // IterateVotes iterates over the all the proposals votes and performs a callback function func (keeper Keeper) IterateVotes(ctx sdk.Context, proposalID uint64, cb func(vote types.Vote) (stop bool)) { - store := ctx.KVStore(keeper.storeKey) - iterator := sdk.KVStorePrefixIterator(store, types.VotesKey(proposalID)) + keeper.iterateVoteStore(keeper.visibleVotesStore(ctx, proposalID), cb) +} + +func (keeper Keeper) iterateVoteStore(store storetypes.KVStore, cb func(vote types.Vote) (stop bool)) bool { + iterator := store.Iterator(nil, nil) defer func() { _ = iterator.Close() }() for ; iterator.Valid(); iterator.Next() { @@ -118,15 +215,60 @@ func (keeper Keeper) IterateVotes(ctx sdk.Context, proposalID uint64, cb func(vo populateLegacyOption(&vote) if cb(vote) { - break + return true } } + return false } -// deleteVote deletes a vote from a given proposalID and voter from the store -func (keeper Keeper) deleteVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress) { +func (keeper Keeper) visibleVotesStore(ctx sdk.Context, proposalID uint64) storetypes.KVStore { store := ctx.KVStore(keeper.storeKey) - store.Delete(types.VoteKey(proposalID, voterAddr)) + pending := prefix.NewStore(store, types.VotesKey(proposalID)) + progress, found := keeper.getTallyProgress(ctx, proposalID) + if !found { + return pending + } + + return visibleVotesStore{ + KVStore: pending, + archived: prefix.NewStore(store, types.TallyVotesKey(proposalID, progress.Expedited)), + storeKey: keeper.storeKey, + } +} + +type visibleVotesStore struct { + storetypes.KVStore + archived storetypes.KVStore + storeKey sdk.StoreKey +} + +func (store visibleVotesStore) Get(key []byte) []byte { + if value := store.KVStore.Get(key); value != nil { + return value + } + return store.archived.Get(key) +} + +func (store visibleVotesStore) Has(key []byte) bool { + return store.KVStore.Has(key) || store.archived.Has(key) +} + +func (store visibleVotesStore) Iterator(start, end []byte) storetypes.Iterator { + return cachekv.NewCacheMergeIterator( + store.archived.Iterator(start, end), + store.KVStore.Iterator(start, end), + true, + store.storeKey, + ) +} + +func (store visibleVotesStore) ReverseIterator(start, end []byte) storetypes.Iterator { + return cachekv.NewCacheMergeIterator( + store.archived.ReverseIterator(start, end), + store.KVStore.ReverseIterator(start, end), + false, + store.storeKey, + ) } // populateLegacyOption adds graceful fallback of deprecated `Option` field, in case diff --git a/sei-cosmos/x/gov/simulation/decoder.go b/sei-cosmos/x/gov/simulation/decoder.go index dbfa8c8c84..94f1d37023 100644 --- a/sei-cosmos/x/gov/simulation/decoder.go +++ b/sei-cosmos/x/gov/simulation/decoder.go @@ -41,12 +41,19 @@ func NewDecodeStore(cdc codec.Codec) func(kvA, kvB kv.Pair) string { cdc.MustUnmarshal(kvB.Value, &depositB) return fmt.Sprintf("%v\n%v", depositA, depositB) - case bytes.Equal(kvA.Key[:1], types.VotesKeyPrefix): + case bytes.Equal(kvA.Key[:1], types.VotesKeyPrefix), + bytes.Equal(kvA.Key[:1], types.TallyVotesKeyPrefix): var voteA, voteB types.Vote cdc.MustUnmarshal(kvA.Value, &voteA) cdc.MustUnmarshal(kvB.Value, &voteB) return fmt.Sprintf("%v\n%v", voteA, voteB) + case bytes.Equal(kvA.Key[:1], types.TallyProgressKeyPrefix), + bytes.Equal(kvA.Key[:1], types.TallyCleanupKeyPrefix), + bytes.Equal(kvA.Key[:1], types.VoteDelegationsKeyPrefix), + bytes.Equal(kvA.Key[:1], types.TallyVoteDelegationsKeyPrefix): + return fmt.Sprintf("%X\n%X", kvA.Value, kvB.Value) + default: panic(fmt.Sprintf("invalid governance key prefix %X", kvA.Key[:1])) } diff --git a/sei-cosmos/x/gov/spec/02_state.md b/sei-cosmos/x/gov/spec/02_state.md index 269ff69272..295f43162c 100644 --- a/sei-cosmos/x/gov/spec/02_state.md +++ b/sei-cosmos/x/gov/spec/02_state.md @@ -119,12 +119,14 @@ We also mention a method to update the tally for a given proposal: _Stores are KVStores in the multi-store. The key to find the store is the first parameter in the list_` -We will use one KVStore `Governance` to store two mappings: +We will use one KVStore `Governance` to store three mappings: - A mapping from `proposalID|'proposal'` to `Proposal`. - A mapping from `proposalID|'addresses'|address` to `Vote`. This mapping allows us to query all addresses that voted on the proposal along with their vote by doing a range query on `proposalID:addresses`. +- A mapping from `proposalID|'delegations'|address` to the voter's per-validator + delegation shares when the vote was recorded. For pseudocode purposes, here are the two function we will use to read or write in stores: @@ -137,11 +139,13 @@ For pseudocode purposes, here are the two function we will use to read or write - `ProposalProcessingQueue`: A queue `queue[proposalID]` containing all the `ProposalIDs` of proposals that reached `MinDeposit`. During each `EndBlock`, - all the proposals that have reached the end of their voting period are processed. - To process a finished proposal, the application tallies the votes, computes the - votes of each validator and checks if every validator in the validator set has - voted. If the proposal is accepted, deposits are refunded. Finally, the proposal - content `Handler` is executed. + proposals that have reached the end of their voting period are advanced within + the block's vote-processing budget. + +To process a finished proposal, the application tallies the votes, computes the +votes of each validator and checks if every validator in the validator set has +voted. If the proposal is accepted, deposits are refunded. Finally, the proposal +content `Handler` is executed. And the pseudocode for the `ProposalProcessingQueue`: @@ -161,7 +165,7 @@ And the pseudocode for the `ProposalProcessingQueue`: // Tally voterIterator = rangeQuery(Governance, ) //return all the addresses that voted on the proposal for each (voterAddress, vote) in voterIterator - delegations = stakingKeeper.getDelegations(voterAddress) // get all delegations for current voter + delegations = getVoteDelegationSnapshot(voterAddress) for each delegation in delegations // make sure delegation.Shares does NOT include shares being unbonded @@ -203,3 +207,27 @@ And the pseudocode for the `ProposalProcessingQueue`: store(Governance, , proposal) ``` + +## Incremental tally state + +An expired proposal retains a tally accumulator, a cursor, and a snapshot of the +bonded validators and tally parameters until all of its vote records have been +processed. Processed votes move to a round-specific archive so an application-state +export can reconstruct every vote while a tally is unfinished. New votes are rejected +after the accumulator is created. Each vote retains the voter's per-validator +delegation shares from the time the vote was recorded, and that snapshot moves with +the vote into the tally archive. Delegator results are accumulated per validator from +those stored shares, so delegation changes after voting do not alter the result. If +the stored delegation shares exceed that validator's tally snapshot, every delegator +option is scaled by the same factor to fit the snapshotted voting-power budget. This +makes the result independent of vote-record order. Completed tally archives and their +delegation snapshots are removed incrementally under the same per-block vote-record +budget, with part of that budget reserved so cleanup cannot be starved by unfinished +tallies. + +Application-state export serializes all archived and pending votes together with their +delegation snapshots, but not the in-progress accumulator. On import, an expired voting +proposal starts a new tally from those votes, their original delegation snapshots, and +the imported validator and governance-parameter state. The accumulator is created +during genesis initialization, so the proposal does not reopen for votes before its +first `EndBlock`. diff --git a/sei-cosmos/x/gov/types/genesis.go b/sei-cosmos/x/gov/types/genesis.go index f0f2547927..52249e4cfb 100644 --- a/sei-cosmos/x/gov/types/genesis.go +++ b/sei-cosmos/x/gov/types/genesis.go @@ -3,7 +3,8 @@ package types import ( "fmt" - "github.com/sei-protocol/sei-chain/sei-cosmos/codec/types" + codecTypes "github.com/sei-protocol/sei-chain/sei-cosmos/codec/types" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" ) // NewGenesisState creates a new genesis state for the governance module @@ -33,7 +34,26 @@ func (data GenesisState) Equal(other GenesisState) bool { data.Proposals.Equal(other.Proposals) && data.DepositParams.Equal(other.DepositParams) && data.TallyParams.Equal(other.TallyParams) && - data.VotingParams.Equal(other.VotingParams) + data.VotingParams.Equal(other.VotingParams) && + voteDelegationSnapshotsEqual(data.VoteDelegationSnapshots, other.VoteDelegationSnapshots) +} + +func voteDelegationSnapshotsEqual(a, b []VoteDelegationSnapshot) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i].ProposalId != b[i].ProposalId || a[i].Voter != b[i].Voter || len(a[i].Delegations) != len(b[i].Delegations) { + return false + } + for j := range a[i].Delegations { + if a[i].Delegations[j].Validator != b[i].Delegations[j].Validator || + !a[i].Delegations[j].Shares.Equal(b[i].Delegations[j].Shares) { + return false + } + } + } + return true } // Empty returns true if a GenesisState is empty @@ -71,13 +91,54 @@ func ValidateGenesis(data *GenesisState) error { data.DepositParams.MinDeposit.String()) } + if err := validateVoteDelegationSnapshots(data.Votes, data.VoteDelegationSnapshots); err != nil { + return err + } + + return nil +} + +func validateVoteDelegationSnapshots(votes Votes, snapshots []VoteDelegationSnapshot) error { + voteKeys := make(map[string]struct{}, len(votes)) + for _, vote := range votes { + voteKeys[fmt.Sprintf("%d/%s", vote.ProposalId, vote.Voter)] = struct{}{} + } + + seenSnapshots := make(map[string]struct{}, len(snapshots)) + for _, snapshot := range snapshots { + key := fmt.Sprintf("%d/%s", snapshot.ProposalId, snapshot.Voter) + if _, found := voteKeys[key]; !found { + return fmt.Errorf("vote delegation snapshot %s has no matching vote", key) + } + if _, found := seenSnapshots[key]; found { + return fmt.Errorf("duplicate vote delegation snapshot %s", key) + } + seenSnapshots[key] = struct{}{} + + if _, err := sdk.AccAddressFromBech32(snapshot.Voter); err != nil { + return fmt.Errorf("invalid vote delegation snapshot voter %q: %w", snapshot.Voter, err) + } + seenValidators := make(map[string]struct{}, len(snapshot.Delegations)) + for _, delegation := range snapshot.Delegations { + if _, err := sdk.ValAddressFromBech32(delegation.Validator); err != nil { + return fmt.Errorf("invalid vote delegation snapshot validator %q: %w", delegation.Validator, err) + } + if !delegation.Shares.IsPositive() { + return fmt.Errorf("vote delegation snapshot shares must be positive: %s", delegation.Shares) + } + if _, found := seenValidators[delegation.Validator]; found { + return fmt.Errorf("duplicate validator %q in vote delegation snapshot %s", delegation.Validator, key) + } + seenValidators[delegation.Validator] = struct{}{} + } + } return nil } -var _ types.UnpackInterfacesMessage = GenesisState{} +var _ codecTypes.UnpackInterfacesMessage = GenesisState{} // UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces -func (data GenesisState) UnpackInterfaces(unpacker types.AnyUnpacker) error { +func (data GenesisState) UnpackInterfaces(unpacker codecTypes.AnyUnpacker) error { for _, p := range data.Proposals { err := p.UnpackInterfaces(unpacker) if err != nil { diff --git a/sei-cosmos/x/gov/types/genesis.pb.go b/sei-cosmos/x/gov/types/genesis.pb.go index 0986a5e939..098c6b0d9b 100644 --- a/sei-cosmos/x/gov/types/genesis.pb.go +++ b/sei-cosmos/x/gov/types/genesis.pb.go @@ -5,11 +5,13 @@ package types import ( fmt "fmt" - _ "github.com/gogo/protobuf/gogoproto" - proto "github.com/gogo/protobuf/proto" io "io" math "math" math_bits "math/bits" + + _ "github.com/gogo/protobuf/gogoproto" + proto "github.com/gogo/protobuf/proto" + github_com_sei_protocol_sei_chain_sei_cosmos_types "github.com/sei-protocol/sei-chain/sei-cosmos/types" ) // Reference imports to suppress errors if they are not otherwise used. @@ -39,6 +41,8 @@ type GenesisState struct { VotingParams VotingParams `protobuf:"bytes,6,opt,name=voting_params,json=votingParams,proto3" json:"voting_params" yaml:"voting_params"` // params defines all the paramaters of related to tally. TallyParams TallyParams `protobuf:"bytes,7,opt,name=tally_params,json=tallyParams,proto3" json:"tally_params" yaml:"tally_params"` + // vote_delegation_snapshots defines the delegation shares captured with each vote. + VoteDelegationSnapshots []VoteDelegationSnapshot `protobuf:"bytes,8,rep,name=vote_delegation_snapshots,json=voteDelegationSnapshots,proto3" json:"vote_delegation_snapshots"` } func (m *GenesisState) Reset() { *m = GenesisState{} } @@ -123,42 +127,167 @@ func (m *GenesisState) GetTallyParams() TallyParams { return TallyParams{} } +func (m *GenesisState) GetVoteDelegationSnapshots() []VoteDelegationSnapshot { + if m != nil { + return m.VoteDelegationSnapshots + } + return nil +} + +// VoteDelegationSnapshot defines the per-validator delegation shares captured with a vote. +type VoteDelegationSnapshot struct { + ProposalId uint64 `protobuf:"varint,1,opt,name=proposal_id,json=proposalId,proto3" json:"proposal_id,omitempty" yaml:"proposal_id"` + Voter string `protobuf:"bytes,2,opt,name=voter,proto3" json:"voter,omitempty"` + Delegations []VoteDelegation `protobuf:"bytes,3,rep,name=delegations,proto3" json:"delegations"` +} + +func (m *VoteDelegationSnapshot) Reset() { *m = VoteDelegationSnapshot{} } +func (m *VoteDelegationSnapshot) String() string { return proto.CompactTextString(m) } +func (*VoteDelegationSnapshot) ProtoMessage() {} +func (*VoteDelegationSnapshot) Descriptor() ([]byte, []int) { + return fileDescriptor_43cd825e0fa7a627, []int{1} +} +func (m *VoteDelegationSnapshot) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *VoteDelegationSnapshot) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_VoteDelegationSnapshot.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 *VoteDelegationSnapshot) XXX_Merge(src proto.Message) { + xxx_messageInfo_VoteDelegationSnapshot.Merge(m, src) +} +func (m *VoteDelegationSnapshot) XXX_Size() int { + return m.Size() +} +func (m *VoteDelegationSnapshot) XXX_DiscardUnknown() { + xxx_messageInfo_VoteDelegationSnapshot.DiscardUnknown(m) +} + +var xxx_messageInfo_VoteDelegationSnapshot proto.InternalMessageInfo + +func (m *VoteDelegationSnapshot) GetProposalId() uint64 { + if m != nil { + return m.ProposalId + } + return 0 +} + +func (m *VoteDelegationSnapshot) GetVoter() string { + if m != nil { + return m.Voter + } + return "" +} + +func (m *VoteDelegationSnapshot) GetDelegations() []VoteDelegation { + if m != nil { + return m.Delegations + } + return nil +} + +// VoteDelegation defines a voter's shares in one validator. +type VoteDelegation struct { + Validator string `protobuf:"bytes,1,opt,name=validator,proto3" json:"validator,omitempty"` + Shares github_com_sei_protocol_sei_chain_sei_cosmos_types.Dec `protobuf:"bytes,2,opt,name=shares,proto3,customtype=github.com/sei-protocol/sei-chain/sei-cosmos/types.Dec" json:"shares"` +} + +func (m *VoteDelegation) Reset() { *m = VoteDelegation{} } +func (m *VoteDelegation) String() string { return proto.CompactTextString(m) } +func (*VoteDelegation) ProtoMessage() {} +func (*VoteDelegation) Descriptor() ([]byte, []int) { + return fileDescriptor_43cd825e0fa7a627, []int{2} +} +func (m *VoteDelegation) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *VoteDelegation) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_VoteDelegation.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 *VoteDelegation) XXX_Merge(src proto.Message) { + xxx_messageInfo_VoteDelegation.Merge(m, src) +} +func (m *VoteDelegation) XXX_Size() int { + return m.Size() +} +func (m *VoteDelegation) XXX_DiscardUnknown() { + xxx_messageInfo_VoteDelegation.DiscardUnknown(m) +} + +var xxx_messageInfo_VoteDelegation proto.InternalMessageInfo + +func (m *VoteDelegation) GetValidator() string { + if m != nil { + return m.Validator + } + return "" +} + func init() { proto.RegisterType((*GenesisState)(nil), "cosmos.gov.v1beta1.GenesisState") + proto.RegisterType((*VoteDelegationSnapshot)(nil), "cosmos.gov.v1beta1.VoteDelegationSnapshot") + proto.RegisterType((*VoteDelegation)(nil), "cosmos.gov.v1beta1.VoteDelegation") } func init() { proto.RegisterFile("cosmos/gov/v1beta1/genesis.proto", fileDescriptor_43cd825e0fa7a627) } var fileDescriptor_43cd825e0fa7a627 = []byte{ - // 438 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x92, 0x41, 0x6f, 0xd3, 0x30, - 0x14, 0xc7, 0x1b, 0xd6, 0x8e, 0xcd, 0x6d, 0x11, 0x98, 0x22, 0x45, 0x6b, 0x49, 0x42, 0x4e, 0xbd, - 0x90, 0x68, 0xe3, 0x82, 0x90, 0xb8, 0x44, 0x48, 0x68, 0x07, 0xa4, 0x61, 0x10, 0x07, 0x2e, 0x95, - 0x9b, 0x5a, 0x5e, 0xa4, 0xb4, 0x2f, 0xea, 0x33, 0x11, 0xfd, 0x16, 0x7c, 0x0e, 0x3e, 0xc9, 0x8e, - 0x3b, 0x72, 0x2a, 0xa8, 0x3d, 0x71, 0xdd, 0x27, 0x40, 0xb1, 0x1d, 0xc8, 0x44, 0xe0, 0x66, 0x3f, - 0xfd, 0xdf, 0xef, 0xf7, 0x6c, 0x3d, 0x12, 0xa4, 0x80, 0x4b, 0xc0, 0x58, 0x42, 0x19, 0x97, 0xa7, - 0x73, 0xa1, 0xf8, 0x69, 0x2c, 0xc5, 0x4a, 0x60, 0x86, 0x51, 0xb1, 0x06, 0x05, 0x94, 0x9a, 0x44, - 0x24, 0xa1, 0x8c, 0x6c, 0xe2, 0x64, 0xd2, 0xd6, 0x05, 0xa5, 0xe9, 0x38, 0x19, 0x49, 0x90, 0xa0, - 0x8f, 0x71, 0x75, 0x32, 0xd5, 0xf0, 0x67, 0x97, 0x0c, 0x5e, 0x1b, 0xf2, 0x3b, 0xc5, 0x95, 0xa0, - 0x6f, 0xc9, 0x08, 0x15, 0x5f, 0xab, 0x6c, 0x25, 0x67, 0xc5, 0x1a, 0x0a, 0x40, 0x9e, 0xcf, 0xb2, - 0x85, 0xeb, 0x04, 0xce, 0xb4, 0x9b, 0xf8, 0x37, 0x5b, 0x7f, 0xbc, 0xe1, 0xcb, 0xfc, 0x45, 0xd8, - 0x96, 0x0a, 0x19, 0xad, 0xcb, 0x17, 0xb6, 0x7a, 0xbe, 0xa0, 0xe7, 0xe4, 0x68, 0x21, 0x0a, 0xc0, - 0x4c, 0xa1, 0x7b, 0x27, 0x38, 0x98, 0xf6, 0xcf, 0xc6, 0xd1, 0xdf, 0xe3, 0x47, 0xaf, 0x4c, 0x26, - 0xb9, 0x7f, 0xb5, 0xf5, 0x3b, 0x5f, 0xbf, 0xfb, 0x47, 0xb6, 0x80, 0xec, 0x77, 0x3b, 0x7d, 0x49, - 0x7a, 0x25, 0x28, 0x81, 0xee, 0x81, 0xe6, 0xb8, 0x6d, 0x9c, 0x0f, 0xa0, 0x44, 0x32, 0xb4, 0x90, - 0x5e, 0x75, 0x43, 0x66, 0xba, 0xe8, 0x1b, 0x72, 0x5c, 0x4f, 0x8b, 0x6e, 0x57, 0x23, 0x26, 0x6d, - 0x88, 0x7a, 0xf8, 0xe4, 0x81, 0xc5, 0x1c, 0xd7, 0x15, 0x64, 0x7f, 0x08, 0x54, 0x92, 0x7b, 0x76, - 0xb2, 0x59, 0xc1, 0xd7, 0x7c, 0x89, 0x6e, 0x2f, 0x70, 0xa6, 0xfd, 0xb3, 0x27, 0xff, 0x79, 0xde, - 0x85, 0x0e, 0x26, 0x8f, 0x2b, 0xf0, 0xcd, 0xd6, 0x7f, 0x64, 0x3e, 0xf3, 0x36, 0x26, 0x64, 0xc3, - 0x45, 0x33, 0x4d, 0x53, 0x32, 0x2c, 0xc1, 0x7c, 0xb6, 0xf1, 0x1c, 0x6a, 0x4f, 0xf0, 0x8f, 0xe7, - 0x57, 0xdf, 0x6f, 0x34, 0x13, 0xab, 0x19, 0x19, 0xcd, 0x2d, 0x48, 0xc8, 0x06, 0x65, 0x23, 0x4b, - 0x67, 0x64, 0xa0, 0x78, 0x9e, 0x6f, 0x6a, 0xc7, 0x5d, 0xed, 0xf0, 0xdb, 0x1c, 0xef, 0xab, 0x9c, - 0x55, 0x8c, 0xad, 0xe2, 0xa1, 0x51, 0x34, 0x11, 0x21, 0xeb, 0xab, 0x46, 0x92, 0x5d, 0xed, 0x3c, - 0xe7, 0x7a, 0xe7, 0x39, 0x3f, 0x76, 0x9e, 0xf3, 0x65, 0xef, 0x75, 0xae, 0xf7, 0x5e, 0xe7, 0xdb, - 0xde, 0xeb, 0x7c, 0x7c, 0x2e, 0x33, 0x75, 0xf9, 0x69, 0x1e, 0xa5, 0xb0, 0x8c, 0x51, 0x64, 0x4f, - 0xf5, 0x6e, 0xa6, 0x90, 0xeb, 0x4b, 0x7a, 0xc9, 0xb3, 0x95, 0x39, 0x99, 0xfd, 0xfe, 0xac, 0x37, - 0x5c, 0x6d, 0x0a, 0x81, 0xf3, 0x43, 0x1d, 0x7d, 0xf6, 0x2b, 0x00, 0x00, 0xff, 0xff, 0x00, 0x82, - 0x10, 0xf3, 0x32, 0x03, 0x00, 0x00, + // 590 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x94, 0x41, 0x6f, 0xd3, 0x3c, + 0x18, 0xc7, 0x9b, 0x6d, 0xed, 0xdb, 0xba, 0xed, 0xf4, 0x62, 0xca, 0x08, 0x6b, 0x49, 0x42, 0x4e, + 0x15, 0x12, 0xa9, 0x36, 0x24, 0x40, 0x48, 0x70, 0x88, 0x2a, 0xa1, 0x21, 0x21, 0x0d, 0x0f, 0xed, + 0xc0, 0x25, 0x72, 0x13, 0x2b, 0x8d, 0x94, 0xd6, 0x51, 0x6c, 0x22, 0xfa, 0x05, 0x38, 0xf3, 0x39, + 0xb8, 0xf2, 0x25, 0x76, 0xdc, 0x0d, 0xc4, 0xa1, 0xa0, 0xf6, 0x1b, 0xf4, 0x13, 0xa0, 0xd8, 0xce, + 0xda, 0x6a, 0x19, 0x88, 0x9b, 0xfd, 0xe4, 0xff, 0xfc, 0xfe, 0xcf, 0x63, 0x3f, 0x31, 0xb0, 0x7c, + 0xca, 0x26, 0x94, 0x0d, 0x42, 0x9a, 0x0d, 0xb2, 0xa3, 0x11, 0xe1, 0xf8, 0x68, 0x10, 0x92, 0x29, + 0x61, 0x11, 0x73, 0x92, 0x94, 0x72, 0x0a, 0xa1, 0x54, 0x38, 0x21, 0xcd, 0x1c, 0xa5, 0x38, 0xec, + 0x95, 0x65, 0xd1, 0x4c, 0x66, 0x1c, 0x76, 0x42, 0x1a, 0x52, 0xb1, 0x1c, 0xe4, 0x2b, 0x19, 0xb5, + 0xbf, 0x55, 0x41, 0xeb, 0x95, 0x24, 0x9f, 0x71, 0xcc, 0x09, 0x7c, 0x0b, 0x3a, 0x8c, 0xe3, 0x94, + 0x47, 0xd3, 0xd0, 0x4b, 0x52, 0x9a, 0x50, 0x86, 0x63, 0x2f, 0x0a, 0x74, 0xcd, 0xd2, 0xfa, 0x7b, + 0xae, 0xb9, 0x9a, 0x9b, 0xdd, 0x19, 0x9e, 0xc4, 0xcf, 0xed, 0x32, 0x95, 0x8d, 0x60, 0x11, 0x3e, + 0x55, 0xd1, 0x93, 0x00, 0x9e, 0x80, 0x7a, 0x40, 0x12, 0xca, 0x22, 0xce, 0xf4, 0x1d, 0x6b, 0xb7, + 0xdf, 0x3c, 0xee, 0x3a, 0xd7, 0xcb, 0x77, 0x86, 0x52, 0xe3, 0xfe, 0x7f, 0x31, 0x37, 0x2b, 0x5f, + 0x7e, 0x9a, 0x75, 0x15, 0x60, 0xe8, 0x2a, 0x1d, 0xbe, 0x00, 0xd5, 0x8c, 0x72, 0xc2, 0xf4, 0x5d, + 0xc1, 0xd1, 0xcb, 0x38, 0xe7, 0x94, 0x13, 0xb7, 0xad, 0x20, 0xd5, 0x7c, 0xc7, 0x90, 0xcc, 0x82, + 0x6f, 0x40, 0xa3, 0xa8, 0x96, 0xe9, 0x7b, 0x02, 0xd1, 0x2b, 0x43, 0x14, 0xc5, 0xbb, 0xb7, 0x14, + 0xa6, 0x51, 0x44, 0x18, 0x5a, 0x13, 0x60, 0x08, 0xf6, 0x55, 0x65, 0x5e, 0x82, 0x53, 0x3c, 0x61, + 0x7a, 0xd5, 0xd2, 0xfa, 0xcd, 0xe3, 0x07, 0x7f, 0x68, 0xef, 0x54, 0x08, 0xdd, 0xfb, 0x39, 0x78, + 0x35, 0x37, 0xef, 0xc8, 0xc3, 0xdc, 0xc6, 0xd8, 0xa8, 0x1d, 0x6c, 0xaa, 0xa1, 0x0f, 0xda, 0x19, + 0x95, 0x87, 0x2d, 0x7d, 0x6a, 0xc2, 0xc7, 0xba, 0xa1, 0xfd, 0xfc, 0xf8, 0xa5, 0x4d, 0x4f, 0xd9, + 0x74, 0xa4, 0xcd, 0x16, 0xc4, 0x46, 0xad, 0x6c, 0x43, 0x0b, 0x3d, 0xd0, 0xe2, 0x38, 0x8e, 0x67, + 0x85, 0xc7, 0x7f, 0xc2, 0xc3, 0x2c, 0xf3, 0x78, 0x97, 0xeb, 0x94, 0x45, 0x57, 0x59, 0xdc, 0x96, + 0x16, 0x9b, 0x08, 0x1b, 0x35, 0xf9, 0x5a, 0x09, 0x63, 0x70, 0x2f, 0xbf, 0x06, 0x2f, 0x20, 0x31, + 0x09, 0x31, 0x8f, 0xe8, 0xd4, 0x63, 0x53, 0x9c, 0xb0, 0x31, 0xe5, 0x4c, 0xaf, 0x8b, 0xdb, 0x78, + 0x78, 0xd3, 0x85, 0x0e, 0xaf, 0x72, 0xce, 0x54, 0x8a, 0xbb, 0x97, 0x1b, 0xa3, 0xbb, 0x59, 0xe9, + 0x57, 0x66, 0x7f, 0xd5, 0xc0, 0x41, 0x79, 0x26, 0x7c, 0x0a, 0x9a, 0xd7, 0x47, 0xfb, 0x60, 0x35, + 0x37, 0xa1, 0xec, 0x61, 0x6b, 0xa2, 0x41, 0xb2, 0x9e, 0xe4, 0x8e, 0x1c, 0xbf, 0x54, 0xdf, 0xb1, + 0xb4, 0x7e, 0x43, 0x4e, 0x55, 0x0a, 0x5f, 0x83, 0xe6, 0xba, 0xa5, 0x62, 0x34, 0xed, 0xbf, 0x77, + 0xa2, 0x3a, 0xd8, 0x4c, 0xb6, 0x3f, 0x69, 0x60, 0x7f, 0x5b, 0x05, 0x7b, 0xa0, 0x91, 0xe1, 0x38, + 0x0a, 0x30, 0xa7, 0xa9, 0xa8, 0xb5, 0x81, 0xd6, 0x01, 0x78, 0x0e, 0x6a, 0x6c, 0x8c, 0x53, 0xc2, + 0x64, 0x4d, 0xee, 0xcb, 0x9c, 0xf9, 0x63, 0x6e, 0x3e, 0x09, 0x23, 0x3e, 0xfe, 0x30, 0x72, 0x7c, + 0x3a, 0x19, 0x30, 0x12, 0x3d, 0x12, 0xbf, 0xbb, 0x4f, 0x63, 0xb1, 0xf1, 0xc7, 0x38, 0x9a, 0xca, + 0x95, 0x7c, 0x32, 0xf8, 0x2c, 0x21, 0xcc, 0x19, 0x12, 0x1f, 0x29, 0x9a, 0x8b, 0x2e, 0x16, 0x86, + 0x76, 0xb9, 0x30, 0xb4, 0x5f, 0x0b, 0x43, 0xfb, 0xbc, 0x34, 0x2a, 0x97, 0x4b, 0xa3, 0xf2, 0x7d, + 0x69, 0x54, 0xde, 0x3f, 0xfb, 0x27, 0xf2, 0x47, 0xf1, 0x1c, 0x09, 0xfe, 0xa8, 0x26, 0xa4, 0x8f, + 0x7f, 0x07, 0x00, 0x00, 0xff, 0xff, 0x7b, 0x26, 0x3d, 0x35, 0xdf, 0x04, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -181,6 +310,20 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.VoteDelegationSnapshots) > 0 { + for iNdEx := len(m.VoteDelegationSnapshots) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.VoteDelegationSnapshots[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x42 + } + } { size, err := m.TallyParams.MarshalToSizedBuffer(dAtA[:i]) if err != nil { @@ -261,6 +404,95 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *VoteDelegationSnapshot) 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 *VoteDelegationSnapshot) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *VoteDelegationSnapshot) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Delegations) > 0 { + for iNdEx := len(m.Delegations) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Delegations[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } + if len(m.Voter) > 0 { + i -= len(m.Voter) + copy(dAtA[i:], m.Voter) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.Voter))) + i-- + dAtA[i] = 0x12 + } + if m.ProposalId != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.ProposalId)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *VoteDelegation) 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 *VoteDelegation) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *VoteDelegation) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size := m.Shares.Size() + i -= size + if _, err := m.Shares.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + if len(m.Validator) > 0 { + i -= len(m.Validator) + copy(dAtA[i:], m.Validator) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.Validator))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func encodeVarintGenesis(dAtA []byte, offset int, v uint64) int { offset -= sovGenesis(v) base := offset @@ -305,6 +537,49 @@ func (m *GenesisState) Size() (n int) { n += 1 + l + sovGenesis(uint64(l)) l = m.TallyParams.Size() n += 1 + l + sovGenesis(uint64(l)) + if len(m.VoteDelegationSnapshots) > 0 { + for _, e := range m.VoteDelegationSnapshots { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + return n +} + +func (m *VoteDelegationSnapshot) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.ProposalId != 0 { + n += 1 + sovGenesis(uint64(m.ProposalId)) + } + l = len(m.Voter) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + if len(m.Delegations) > 0 { + for _, e := range m.Delegations { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + return n +} + +func (m *VoteDelegation) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Validator) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + l = m.Shares.Size() + n += 1 + l + sovGenesis(uint64(l)) return n } @@ -563,6 +838,291 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 8: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field VoteDelegationSnapshots", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.VoteDelegationSnapshots = append(m.VoteDelegationSnapshots, VoteDelegationSnapshot{}) + if err := m.VoteDelegationSnapshots[len(m.VoteDelegationSnapshots)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenesis(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenesis + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *VoteDelegationSnapshot) 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 ErrIntOverflowGenesis + } + 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: VoteDelegationSnapshot: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: VoteDelegationSnapshot: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ProposalId", wireType) + } + m.ProposalId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ProposalId |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Voter", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Voter = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Delegations", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Delegations = append(m.Delegations, VoteDelegation{}) + if err := m.Delegations[len(m.Delegations)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenesis(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenesis + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *VoteDelegation) 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 ErrIntOverflowGenesis + } + 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: VoteDelegation: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: VoteDelegation: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Validator", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Validator = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Shares", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Shares.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/sei-cosmos/x/gov/types/genesis_test.go b/sei-cosmos/x/gov/types/genesis_test.go index a0fbebde22..cb8d4b8a7a 100644 --- a/sei-cosmos/x/gov/types/genesis_test.go +++ b/sei-cosmos/x/gov/types/genesis_test.go @@ -3,6 +3,7 @@ package types import ( "testing" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/stretchr/testify/require" ) @@ -21,6 +22,22 @@ func TestEqualProposalID(t *testing.T) { require.True(t, state1.Equal(state2)) } +func TestGenesisStateEqualIncludesVoteDelegationSnapshots(t *testing.T) { + state1 := GenesisState{VoteDelegationSnapshots: []VoteDelegationSnapshot{{ + ProposalId: 1, + Voter: "voter", + Delegations: []VoteDelegation{{ + Validator: "validator", + Shares: sdk.OneDec(), + }}, + }}} + state2 := state1 + require.True(t, state1.Equal(state2)) + + state2.VoteDelegationSnapshots = nil + require.False(t, state1.Equal(state2)) +} + func TestValidateGenesis(t *testing.T) { require.Nil(t, ValidateGenesis(DefaultGenesisState())) require.Error(t, ValidateGenesis(&GenesisState{})) diff --git a/sei-cosmos/x/gov/types/keys.go b/sei-cosmos/x/gov/types/keys.go index 9f590db78b..4d3822bc6f 100644 --- a/sei-cosmos/x/gov/types/keys.go +++ b/sei-cosmos/x/gov/types/keys.go @@ -37,6 +37,16 @@ const ( // - 0x10: Deposit // // - 0x20: Voter +// +// - 0x30: Tally progress +// +// - 0x31: Archived voter +// +// - 0x32: Tally archive cleanup cursor +// +// - 0x33: Voter delegation snapshot +// +// - 0x34: Archived voter delegation snapshot var ( ProposalsKeyPrefix = []byte{0x00} ActiveProposalQueuePrefix = []byte{0x01} @@ -46,6 +56,12 @@ var ( DepositsKeyPrefix = []byte{0x10} VotesKeyPrefix = []byte{0x20} + + TallyProgressKeyPrefix = []byte{0x30} + TallyVotesKeyPrefix = []byte{0x31} + TallyCleanupKeyPrefix = []byte{0x32} + VoteDelegationsKeyPrefix = []byte{0x33} + TallyVoteDelegationsKeyPrefix = []byte{0x34} ) var lenTime = len(sdk.FormatTimeBytes(time.Now())) @@ -107,6 +123,43 @@ func VoteKey(proposalID uint64, voterAddr sdk.AccAddress) []byte { return append(VotesKey(proposalID), address.MustLengthPrefix(voterAddr.Bytes())...) } +// VoteDelegationsKey returns the key for the delegation snapshot captured with a vote. +func VoteDelegationsKey(proposalID uint64, voterAddr sdk.AccAddress) []byte { + return append(append(VoteDelegationsKeyPrefix, GetProposalIDBytes(proposalID)...), address.MustLengthPrefix(voterAddr.Bytes())...) +} + +// TallyProgressKey returns the key for a proposal's incremental tally state. +func TallyProgressKey(proposalID uint64) []byte { + return append(TallyProgressKeyPrefix, GetProposalIDBytes(proposalID)...) +} + +// TallyVotesKey returns the prefix for votes archived during a proposal tally round. +func TallyVotesKey(proposalID uint64, expedited bool) []byte { + return append(append(TallyVotesKeyPrefix, GetProposalIDBytes(proposalID)...), tallyRound(expedited)) +} + +// TallyVoteKey returns the key for a vote archived during a proposal tally. +func TallyVoteKey(proposalID uint64, expedited bool, voterAddr sdk.AccAddress) []byte { + return append(TallyVotesKey(proposalID, expedited), address.MustLengthPrefix(voterAddr.Bytes())...) +} + +// TallyVoteDelegationsKey returns the key for an archived vote's delegation snapshot. +func TallyVoteDelegationsKey(proposalID uint64, expedited bool, voterAddr sdk.AccAddress) []byte { + return append(append(append(TallyVoteDelegationsKeyPrefix, GetProposalIDBytes(proposalID)...), tallyRound(expedited)), address.MustLengthPrefix(voterAddr.Bytes())...) +} + +// TallyCleanupKey returns the key for a proposal tally round's archived-vote cleanup cursor. +func TallyCleanupKey(proposalID uint64, expedited bool) []byte { + return append(append(TallyCleanupKeyPrefix, GetProposalIDBytes(proposalID)...), tallyRound(expedited)) +} + +func tallyRound(expedited bool) byte { + if expedited { + return 0 + } + return 1 +} + // Split keys function; used for iterators // SplitProposalKey split the proposal key and returns the proposal id diff --git a/sei-cosmos/x/gov/types/keys_test.go b/sei-cosmos/x/gov/types/keys_test.go index b98b450620..459cba0402 100644 --- a/sei-cosmos/x/gov/types/keys_test.go +++ b/sei-cosmos/x/gov/types/keys_test.go @@ -8,6 +8,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/keys/ed25519" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/types/address" ) var addr = sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) @@ -59,3 +60,12 @@ func TestVoteKeys(t *testing.T) { require.Equal(t, int(proposalID), 2) require.Equal(t, addr, voterAddr) } + +func TestTallyKeys(t *testing.T) { + require.Equal(t, append(TallyProgressKeyPrefix, GetProposalIDBytes(2)...), TallyProgressKey(2)) + require.NotEqual(t, TallyVotesKey(2, true), TallyVotesKey(2, false)) + require.NotEqual(t, TallyVoteKey(2, true, addr), TallyVoteKey(2, false, addr)) + require.NotEqual(t, TallyCleanupKey(2, true), TallyCleanupKey(2, false)) + require.Equal(t, append(append(VoteDelegationsKeyPrefix, GetProposalIDBytes(2)...), address.MustLengthPrefix(addr.Bytes())...), VoteDelegationsKey(2, addr)) + require.Equal(t, append(append(append(TallyVoteDelegationsKeyPrefix, GetProposalIDBytes(2)...), byte(1)), address.MustLengthPrefix(addr.Bytes())...), TallyVoteDelegationsKey(2, false, addr)) +}