diff --git a/app/app.go b/app/app.go index a310a2fc8f..6abc6b18c0 100644 --- a/app/app.go +++ b/app/app.go @@ -581,9 +581,8 @@ func New( // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks - app.StakingKeeper = *stakingKeeper.SetHooks( - stakingtypes.NewMultiStakingHooks(app.DistrKeeper.Hooks(), app.SlashingKeeper.Hooks()), - ) + stakingHooks := stakingtypes.NewMultiStakingHooks(app.DistrKeeper.Hooks(), app.SlashingKeeper.Hooks()) + app.StakingKeeper = *stakingKeeper.SetHooks(&stakingHooks) // ... other modules keepers @@ -801,6 +800,7 @@ func New( appCodec, keys[govtypes.StoreKey], app.GetSubspace(govtypes.ModuleName), app.AccountKeeper, app.BankKeeper, &stakingKeeper, app.ParamsKeeper, govRouter, ) + stakingHooks.AddHooks(app.GovKeeper.StakingHooks()) // this line is used by starport scaffolding # stargate/app/keeperDefinition @@ -843,6 +843,7 @@ func New( DistrKeeper: &app.DistrKeeper, SlashingKeeper: &app.SlashingKeeper, EvidenceKeeper: &app.EvidenceKeeper, + GovKeeper: &app.GovKeeper, StakingKeeper: &app.StakingKeeper, EvmKeeper: &app.EvmKeeper, } diff --git a/app/legacyabci/begin_block.go b/app/legacyabci/begin_block.go index bb24b490b3..03fedc5f99 100644 --- a/app/legacyabci/begin_block.go +++ b/app/legacyabci/begin_block.go @@ -12,6 +12,8 @@ import ( "github.com/sei-protocol/sei-chain/sei-cosmos/x/evidence" evidencekeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/evidence/keeper" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov" + govkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/keeper" "github.com/sei-protocol/sei-chain/sei-cosmos/x/slashing" slashingkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/slashing/keeper" @@ -30,6 +32,7 @@ type BeginBlockKeepers struct { DistrKeeper *distrkeeper.Keeper SlashingKeeper *slashingkeeper.Keeper EvidenceKeeper *evidencekeeper.Keeper + GovKeeper *govkeeper.Keeper StakingKeeper *stakingkeeper.Keeper EvmKeeper *evmkeeper.Keeper } @@ -48,6 +51,9 @@ func BeginBlock( telemetry.MeasureSince(start, "module", "total_begin_block") }() + if keepers.GovKeeper != nil { + gov.BeginBlocker(ctx, *keepers.GovKeeper) + } keepers.EpochKeeper.BeginBlock(ctx) upgrade.BeginBlocker(*keepers.UpgradeKeeper, ctx) distribution.BeginBlocker(ctx, votes, *keepers.DistrKeeper) diff --git a/evmrpc/export_test.go b/evmrpc/export_test.go index f5285e3808..53714dd11f 100644 --- a/evmrpc/export_test.go +++ b/evmrpc/export_test.go @@ -2,6 +2,7 @@ package evmrpc import ( "context" + "errors" "math/big" "sync" @@ -18,6 +19,21 @@ import ( "github.com/sei-protocol/sei-chain/x/evm/keeper" ) +// HoldSimulationSlotsForTest occupies simulation request slots until the returned function is called. +func (s *SimulationAPI) HoldSimulationSlotsForTest(ctx context.Context, slots int64) (func(), error) { + if s.requestLimiter == nil { + return nil, errors.New("simulation request limiter is disabled") + } + if slots <= 0 { + return nil, errors.New("simulation slot count must be positive") + } + if err := s.requestLimiter.Acquire(ctx, slots); err != nil { + return nil, err + } + + return func() { s.requestLimiter.Release(slots) }, nil +} + // RangeQueryWindowBlocksForTest exposes rangeQueryWindowBlocks so integration // tests can assert tryFilterLogsRange's window boundaries without hardcoding // the constant. diff --git a/evmrpc/simulate.go b/evmrpc/simulate.go index 475a8c6cb2..87034ace49 100644 --- a/evmrpc/simulate.go +++ b/evmrpc/simulate.go @@ -33,6 +33,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-cosmos/baseapp" "github.com/sei-protocol/sei-chain/sei-cosmos/client" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + govkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/keeper" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" @@ -731,6 +732,10 @@ func (b *Backend) initializeBlock(ctx context.Context, block *ethtypes.Block, ct // iteration can pass it into the SS MVCC skip loops. sdkCtx = sdkCtx.WithContext(ctx) } + if err := b.activateIncrementalTallyForTrace(sdkCtx, blockNumber); err != nil { + release() + return sdk.Context{}, nil, emptyRelease, fmt.Errorf("activate incremental governance tally: %w", err) + } runTraceBeginBlock(sdkCtx, blockNumber, reqBeginBlock.LastCommitInfo.Votes, tmBlock.Block.Evidence.ToABCI(), b.beginBlockKeepers) var nextCtx sdk.Context nextCtx, nextRelease = ctxProvider(sdkCtx.BlockHeight()) @@ -741,6 +746,19 @@ func (b *Backend) initializeBlock(ctx context.Context, block *ethtypes.Block, ct return sdkCtx, tmBlock, release, nil } +func (b *Backend) activateIncrementalTallyForTrace(ctx sdk.Context, height int64) error { + if b.keeper == nil || b.beginBlockKeepers.GovKeeper == nil { + return nil + } + govKeeper := *b.beginBlockKeepers.GovKeeper + if govKeeper.IncrementalTallyEnabled(ctx) || + !b.isV67ActiveAtHeight(height) || + b.isV67ActiveAtHeight(height-1) { + return nil + } + return govkeeper.NewMigrator(govKeeper).Migrate3to4(ctx) +} + // runTraceBeginBlock is the BeginBlock used when reconstructing historical // state for traces. var runTraceBeginBlock = legacyabci.BeginBlock diff --git a/evmrpc/simulate_test.go b/evmrpc/simulate_test.go index 0213f72baa..5328418665 100644 --- a/evmrpc/simulate_test.go +++ b/evmrpc/simulate_test.go @@ -14,8 +14,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/export" "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/trie" "github.com/sei-protocol/sei-chain/app" "github.com/sei-protocol/sei-chain/app/legacyabci" "github.com/sei-protocol/sei-chain/evmrpc" @@ -26,6 +28,7 @@ import ( sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" txtypes "github.com/sei-protocol/sei-chain/sei-cosmos/types/tx" banktypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/bank/types" + govtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" receipt "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/bytes" @@ -603,6 +606,7 @@ func TestSimulateBackendBlockResolutionCoverage(t *testing.T) { } func TestSimulationAPIRequestLimiter(t *testing.T) { + const maxConcurrentSimulationCalls = 2 type testEnv struct { simAPI *evmrpc.SimulationAPI @@ -629,7 +633,7 @@ func TestSimulationAPIRequestLimiter(t *testing.T) { config := &evmrpc.SimulateConfig{ GasCap: 1000000, EVMTimeout: 5 * time.Second, - MaxConcurrentSimulationCalls: 2, // Small limit to easily trigger rate limiting + MaxConcurrentSimulationCalls: maxConcurrentSimulationCalls, } // Use the existing test app from the global setup @@ -675,9 +679,21 @@ func TestSimulationAPIRequestLimiter(t *testing.T) { args: args, } } + assertRateLimited := func(t *testing.T, tEnv *testEnv, expected string, invoke func() error) { + t.Helper() + release, err := tEnv.simAPI.HoldSimulationSlotsForTest(t.Context(), maxConcurrentSimulationCalls) + require.NoError(t, err) + defer release() + require.EqualError(t, invoke(), expected) + } t.Run("TestEthCallRateLimiting", func(t *testing.T) { tEnv := newTestEnv(t) + assertRateLimited(t, tEnv, "eth_call rejected due to rate limit: server busy", func() error { + _, err := tEnv.simAPI.Call(t.Context(), tEnv.args, nil, nil, nil) + return err + }) + // Test eth_call rate limiting with concurrent requests numRequests := 10 // Much more than the limit of 2 runBurst := func() []error { @@ -718,8 +734,9 @@ func TestSimulationAPIRequestLimiter(t *testing.T) { } } - // With only 2 concurrent slots and 10 requests, we should have rejections - require.Greater(t, rejectedCount, 0, "Should have rejected requests due to rate limiting") + // Calls can serialize before another goroutine holds a slot. The saturated + // call above covers rejection deterministically; this burst covers the + // concurrent response outcomes. require.Greater(t, successCount, 0, "Should have some successful requests") require.Equal(t, numRequests, successCount+rejectedCount, "All requests should be accounted for") @@ -890,122 +907,23 @@ func TestSimulationAPIRequestLimiter(t *testing.T) { }) t.Run("TestDifferentMethodsShareSameLimiter", func(t *testing.T) { - // Test that different simulation methods share the same rate limiter. - // A single burst can occasionally avoid contention on overloaded CI workers, - // so retry a synchronized burst a few times. - const ( - numCallRequests = 20 - numEstimateRequests = 20 - maxAttempts = 5 - ) - totalRequests := numCallRequests + numEstimateRequests - - runMixedBurst := func(tEnv *testEnv) (int, int) { - results := make(chan error, totalRequests) - start := make(chan struct{}) - var wg sync.WaitGroup - - // Start mixed requests and release them at once to maximize contention. - for range numCallRequests { - wg.Go(func() { - <-start - _, err := tEnv.simAPI.Call(t.Context(), tEnv.args, nil, nil, nil) - results <- err - }) - } - for range numEstimateRequests { - wg.Go(func() { - <-start - _, err := tEnv.simAPI.EstimateGas(t.Context(), tEnv.args, nil, nil) - results <- err - }) - } - - close(start) - wg.Wait() - close(results) - - successCount := 0 - rejectedCount := 0 - for err := range results { - if err == nil { - successCount++ - } else if strings.Contains(err.Error(), "rejected due to rate limit: server busy") { - rejectedCount++ - } - } - return successCount, rejectedCount - } - - var ( - lastSuccess int - lastRejected int - attemptsUsed int - observedRejection bool - ) - for attempt := 1; attempt <= maxAttempts; attempt++ { - attemptsUsed = attempt - lastSuccess, lastRejected = runMixedBurst(newTestEnv(t)) - require.Equalf(t, totalRequests, lastSuccess+lastRejected, "All mixed method requests should be accounted for (attempt %d)", attempt) - if lastRejected > 0 { - observedRejection = true - break - } - } - - require.Truef( - t, - observedRejection, - "Different methods should share the same rate limiter (last burst: %d successful, %d rejected)", - lastSuccess, - lastRejected, - ) - t.Logf( - "Mixed methods rate limiting (attempt %d/%d): %d successful, %d rejected out of %d total", - attemptsUsed, - maxAttempts, - lastSuccess, - lastRejected, - totalRequests, - ) + tEnv := newTestEnv(t) + assertRateLimited(t, tEnv, "eth_call rejected due to rate limit: server busy", func() error { + _, err := tEnv.simAPI.Call(t.Context(), tEnv.args, nil, nil, nil) + return err + }) + assertRateLimited(t, tEnv, "eth_estimateGas rejected due to rate limit: server busy", func() error { + _, err := tEnv.simAPI.EstimateGas(t.Context(), tEnv.args, nil, nil) + return err + }) }) t.Run("TestRateLimitErrorFormat", func(t *testing.T) { tEnv := newTestEnv(t) - // Test the error message format by overwhelming the rate limiter - const numRequests = 20 - results := make(chan error, numRequests) - start := make(chan struct{}) - var wg sync.WaitGroup - - // Release all requests at once to reliably saturate the limiter. - for range numRequests { - wg.Go(func() { - <-start - _, err := tEnv.simAPI.Call(t.Context(), tEnv.args, nil, nil, nil) - results <- err - }) - } - close(start) - wg.Wait() - close(results) - - var rateLimitErrors []error - for err := range results { - if err != nil && strings.Contains(err.Error(), "rejected due to rate limit") { - rateLimitErrors = append(rateLimitErrors, err) - } - } - - require.Greater(t, len(rateLimitErrors), 0, "Should have at least one rate limit error") - - // Verify error message format - for _, err := range rateLimitErrors { - require.Contains(t, err.Error(), "eth_call rejected due to rate limit: server busy") - require.Contains(t, err.Error(), "server busy") - } - - t.Logf("Found %d rate limit errors with correct format", len(rateLimitErrors)) + assertRateLimited(t, tEnv, "eth_createAccessList rejected due to rate limit: server busy", func() error { + _, err := tEnv.simAPI.CreateAccessList(t.Context(), tEnv.args, nil) + return err + }) }) } @@ -1058,6 +976,92 @@ func (c *fixedBlockClient) Status(_ context.Context) (*coretypes.ResultStatus, e }, nil } +func (c *fixedBlockClient) Validators(context.Context, *int64, *int, *int) (*coretypes.ResultValidators, error) { + return &coretypes.ResultValidators{}, nil +} + +func TestStateAtBlockReplaysIncrementalTallyActivationAndGapBoundary(t *testing.T) { + const activationHeight = int64(200) + + testApp := app.Setup(t, false, false, false) + activationTime := time.Now().UTC().Add(time.Minute) + nextBlockTime := activationTime.Add(10 * time.Second) + baseCtx := testApp.BaseApp.NewContext(false, tenderminttypes.Header{ + Height: activationHeight - 1, + Time: activationTime.Add(-time.Second), + }).WithIsTracing(true).WithClosestUpgradeName("v6.7") + govStore := baseCtx.KVStore(testApp.GetKey(govtypes.StoreKey)) + govStore.Delete(govtypes.IncrementalTallyEnabledKey) + govStore.Delete(govtypes.VoteDelegationBackfillCutoffKey) + govStore.Delete(govtypes.DeadlineBoundaryBlockTimeKey) + + latestCtx := baseCtx.WithIsTracing(false).WithBlockHeight(activationHeight + 1).WithBlockTime(nextBlockTime) + testApp.UpgradeKeeper.SetDone(latestCtx.WithBlockHeight(activationHeight), "v6.7") + primeReceiptStore(t, testApp.EvmKeeper.ReceiptStore(), activationHeight+1) + parentCtx := baseCtx + ctxProvider := func(height int64) sdk.Context { + if height == evmrpc.LatestCtxHeight { + return latestCtx + } + return parentCtx.WithBlockHeight(height) + } + + stateAtBlock := func(height int64, blockTime time.Time) *state.DBImpl { + tmClient := &fixedBlockClient{block: &coretypes.ResultBlock{ + Block: &tmtypes.Block{ + Header: tmtypes.Header{Height: height, Time: blockTime}, + LastCommit: &tmtypes.Commit{Height: height - 1}, + }, + }} + watermarks := evmrpc.NewWatermarkManager(tmClient, ctxProvider, nil, testApp.EvmKeeper.ReceiptStore()) + backend := evmrpc.NewBackend( + ctxProvider, + &testApp.EvmKeeper, + testApp.BeginBlockKeepers, + func(int64) client.TxConfig { return TxConfig }, + tmClient, + &SConfig, + testApp.BaseApp, + testApp.TracerAnteHandler, + evmrpc.NewBlockCache(3000), + &sync.Mutex{}, + watermarks, + ) + block := ethtypes.NewBlock( + ðtypes.Header{Number: big.NewInt(height), Time: uint64(blockTime.Unix()), Difficulty: big.NewInt(0)}, //nolint:gosec + ðtypes.Body{}, + nil, + trie.NewStackTrie(nil), + ) + stateDB, release, err := backend.StateAtBlock(t.Context(), block, 0, nil, true, false) + require.NoError(t, err) + t.Cleanup(release) + return stateDB.(*state.DBImpl) + } + + activationState := stateAtBlock(activationHeight, activationTime) + activationCtx := activationState.Ctx() + require.True(t, testApp.GovKeeper.IncrementalTallyEnabled(activationCtx)) + require.Equal(t, sdk.FormatTimeBytes(activationTime), activationCtx.KVStore(testApp.GetKey(govtypes.StoreKey)).Get(govtypes.DeadlineBoundaryBlockTimeKey)) + cutoff, found := testApp.GovKeeper.GetVoteDelegationBackfillCutoff(activationCtx) + require.True(t, found) + require.Equal(t, uint64(1), cutoff) + + proposal, err := testApp.GovKeeper.SubmitProposal(activationCtx, govtypes.NewTextProposal("trace", "gap", false)) + require.NoError(t, err) + testApp.GovKeeper.RemoveFromInactiveProposalQueue(activationCtx, proposal.ProposalId, proposal.DepositEndTime) + proposal.Status = govtypes.StatusVotingPeriod + proposal.VotingStartTime = activationTime + proposal.VotingEndTime = activationTime.Add(5 * time.Second) + testApp.GovKeeper.SetProposal(activationCtx, proposal) + testApp.GovKeeper.InsertActiveProposalQueue(activationCtx, proposal.ProposalId, proposal.VotingEndTime) + parentCtx = activationCtx + + nextState := stateAtBlock(activationHeight+1, nextBlockTime) + nextStore := nextState.Ctx().KVStore(testApp.GetKey(govtypes.StoreKey)) + require.True(t, nextStore.Has(govtypes.GapTallyBoundaryKey(nextBlockTime))) +} + func TestTraceBlockByNumberUsesCompatDecoderForHistoricalCosmosTx(t *testing.T) { const ( blockHeight = int64(42) diff --git a/evmrpc/tests/regression_test.go b/evmrpc/tests/regression_test.go index 19088e80e7..edfea92439 100644 --- a/evmrpc/tests/regression_test.go +++ b/evmrpc/tests/regression_test.go @@ -5,11 +5,14 @@ import ( "strings" "testing" + "golang.org/x/mod/semver" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/sei-protocol/sei-chain/app" "github.com/sei-protocol/sei-chain/evmrpc" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + govtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" "github.com/stretchr/testify/require" ) @@ -384,6 +387,7 @@ func testTx(t *testing.T, txHash string, version string, expectedGasUsed string, blockHeight := mockStatesFromTxJson(ctx, txHash, a, mc) ctx = setLegacySstoreIfNeeded(ctx, a, version) ctx = withCapturedConsensusParams(ctx, mc, blockHeight) + removeFutureGovernanceActivation(ctx, a, version) return ctx.WithBlockHeight(blockHeight) }) s.Run( @@ -424,6 +428,7 @@ func testBlock( ) ctx = setLegacySstoreIfNeeded(ctx, a, version) ctx = withCapturedConsensusParams(ctx, mc, blockHeight) + removeFutureGovernanceActivation(ctx, a, version) return ctx.WithBlockHeight(blockHeight) }, ) @@ -456,6 +461,12 @@ func setLegacySstoreIfNeeded(ctx sdk.Context, a *app.App, version string) sdk.Co return ctx } +func removeFutureGovernanceActivation(ctx sdk.Context, a *app.App, version string) { + if semver.Compare(version, "v6.7") < 0 { + ctx.KVStore(a.GetKey(govtypes.StoreKey)).Delete(govtypes.IncrementalTallyEnabledKey) + } +} + func isVersionLessOrEqual(version, target string) bool { // Remove 'v' prefix if present if len(version) > 0 && version[0] == 'v' { diff --git a/sei-cosmos/proto/cosmos/gov/v1beta1/genesis.proto b/sei-cosmos/proto/cosmos/gov/v1beta1/genesis.proto index 8229d3c0b4..797172c5d8 100644 --- a/sei-cosmos/proto/cosmos/gov/v1beta1/genesis.proto +++ b/sei-cosmos/proto/cosmos/gov/v1beta1/genesis.proto @@ -41,4 +41,53 @@ message GenesisState { (gogoproto.nullable) = false, (gogoproto.moretags) = "yaml:\"tally_params\"" ]; + // vote_delegation_snapshots defines the delegation shares maintained for each vote. + repeated VoteDelegationSnapshot vote_delegation_snapshots = 8 [(gogoproto.nullable) = false]; + // tally_electorates defines the frozen electorate for unresolved proposals. + repeated TallyElectorate tally_electorates = 9 [(gogoproto.nullable) = false]; + // vote_delegation_backfill_cutoff defines the first proposal ID created after vote delegation tracking began. + uint64 vote_delegation_backfill_cutoff = 10 [(gogoproto.moretags) = "yaml:\"vote_delegation_backfill_cutoff\""]; + + // modern_tally_round_proposal_ids defines legacy proposals whose converted regular round uses deadline tallying. + repeated uint64 modern_tally_round_proposal_ids = 11 [(gogoproto.moretags) = "yaml:\"modern_tally_round_proposal_ids\""]; +} + +// VoteDelegationSnapshot defines the per-validator delegation shares maintained for 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 + ]; +} + +// TallyElectorate defines the validator and parameter state used to tally one proposal. +message TallyElectorate { + uint64 proposal_id = 1 [(gogoproto.moretags) = "yaml:\"proposal_id\""]; + string total_bonded_tokens = 2 [ + (gogoproto.customtype) = "github.com/sei-protocol/sei-chain/sei-cosmos/types.Int", + (gogoproto.nullable) = false + ]; + TallyParams tally_params = 3 [(gogoproto.nullable) = false]; + repeated TallyValidator tally_validators = 4 [(gogoproto.nullable) = false]; +} + +// TallyValidator defines a validator's frozen state in a proposal electorate. +message TallyValidator { + string address = 1; + string bonded_tokens = 2 [ + (gogoproto.customtype) = "github.com/sei-protocol/sei-chain/sei-cosmos/types.Int", + (gogoproto.nullable) = false + ]; + string delegator_shares = 3 [ + (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..497f60bc93 100644 --- a/sei-cosmos/x/gov/abci.go +++ b/sei-cosmos/x/gov/abci.go @@ -13,7 +13,18 @@ import ( var logger = seilog.NewLogger("cosmos", "x", "gov") -// EndBlocker called every block, process inflation, update validator set. +// MaxVotesProcessedPerBlock is the governance record-work budget shared by delegation updates, backfill, tallying, and cleanup. +const MaxVotesProcessedPerBlock = 1000 + +// minTallyCleanupVotesPerBlock reserves part of the budget for completed tally archives. +const minTallyCleanupVotesPerBlock = 100 + +// BeginBlocker freezes electorates for proposal deadlines strictly between consecutive block times. +func BeginBlocker(ctx sdk.Context, keeper keeper.Keeper) { + keeper.CaptureGapTallyBoundary(ctx) +} + +// EndBlocker expires governance proposals and advances their tally work. func EndBlocker(ctx sdk.Context, keeper keeper.Keeper) { endBlockerStart := time.Now() defer func() { @@ -21,6 +32,11 @@ func EndBlocker(ctx sdk.Context, keeper keeper.Keeper) { // TODO(PLT-414): remove once gov_end_blocker_duration verified telemetry.ModuleMeasureSince(types.ModuleName, endBlockerStart, telemetry.MetricKeyEndBlocker) }() + if !keeper.IncrementalTallyEnabled(ctx) { + legacyEndBlocker(ctx, keeper) + return + } + keeper.CaptureExactTallyBoundary(ctx) // delete inactive proposal from store and its deposits keeper.IterateInactiveProposalsQueue(ctx, ctx.BlockHeader().Time, func(proposal types.Proposal) bool { @@ -50,11 +66,22 @@ func EndBlocker(ctx sdk.Context, keeper keeper.Keeper) { return false }) + remainingRecords := MaxVotesProcessedPerBlock + remainingRecords -= keeper.CleanupTallyVotes(ctx, minTallyCleanupVotesPerBlock) + if remainingRecords == 0 { + return + } + // 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, remainingRecords) + remainingRecords -= processed + if !complete { + // Preserve queue order without initializing validator snapshots for unbounded later proposals. + 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. @@ -100,14 +127,13 @@ func EndBlocker(ctx sdk.Context, keeper keeper.Keeper) { // The proposal didn't pass after voting period ends if proposal.IsExpedited { // When expedited proposal fails, it is converted to a regular proposal. - // As a result, the voting period is extended. - // Once the regular voting period expires again, the tally is repeated - // according to the regular proposal rules. + // Resume the regular round after its expedited tally completes so that + // bounded tally work does not consume the regular voting window. proposal.IsExpedited = false votingParams := keeper.GetVotingParams(ctx) - proposal.VotingEndTime = proposal.VotingStartTime.Add(votingParams.VotingPeriod) + proposal.VotingEndTime = ctx.BlockTime().Add(votingParams.VotingPeriod - votingParams.ExpeditedVotingPeriod) - keeper.InsertActiveProposalQueue(ctx, proposal.ProposalId, proposal.VotingEndTime) + keeper.InsertActiveProposalQueueForModernTallyRound(ctx, proposal.ProposalId, proposal.VotingEndTime) tagValue = types.AttributeValueExpeditedConverted logMsg = "expedited proposal converted to regular" } else { @@ -134,6 +160,97 @@ func EndBlocker(ctx sdk.Context, keeper keeper.Keeper) { "result", logMsg, ) + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeActiveProposal, + sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.ProposalId)), + sdk.NewAttribute(types.AttributeKeyProposalResult, tagValue), + ), + ) + return remainingRecords == 0 + }) + + keeper.CleanupTallyVotes(ctx, remainingRecords) +} + +func legacyEndBlocker(ctx sdk.Context, keeper keeper.Keeper) { + keeper.IterateInactiveProposalsQueue(ctx, ctx.BlockHeader().Time, func(proposal types.Proposal) bool { + keeper.DeleteProposal(ctx, proposal.ProposalId) + keeper.DeleteDeposits(ctx, proposal.ProposalId) + keeper.AfterProposalFailedMinDeposit(ctx, proposal.ProposalId) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeInactiveProposal, + sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.ProposalId)), + sdk.NewAttribute(types.AttributeKeyProposalResult, types.AttributeValueProposalDropped), + ), + ) + + logger.Info( + "proposal did not meet minimum deposit; deleted", + "proposal", proposal.ProposalId, + "title", proposal.GetTitle(), + "min_deposit", keeper.GetDepositParams(ctx).MinDeposit.String(), + "min_expedited_deposit", keeper.GetDepositParams(ctx).MinExpeditedDeposit.String(), + "total_deposit", proposal.TotalDeposit.String(), + ) + + return false + }) + + keeper.IterateActiveProposalsQueue(ctx, ctx.BlockHeader().Time, func(proposal types.Proposal) bool { + var tagValue, logMsg string + passes, burnDeposits, tallyResults := keeper.TallyLegacy(ctx, proposal) + + if !proposal.IsExpedited || passes { + if burnDeposits { + keeper.DeleteDeposits(ctx, proposal.ProposalId) + } else { + keeper.RefundDeposits(ctx, proposal.ProposalId) + } + } + + keeper.RemoveFromActiveProposalQueue(ctx, proposal.ProposalId, proposal.VotingEndTime) + + if passes { + handler := keeper.Router().GetRoute(proposal.ProposalRoute()) + cacheCtx, writeCache := ctx.CacheContext() + err := handler(cacheCtx, proposal.GetContent()) + if err == nil { + proposal.Status = types.StatusPassed + tagValue = types.AttributeValueProposalPassed + logMsg = "passed" + ctx.EventManager().EmitEvents(cacheCtx.EventManager().Events()) + writeCache() + } else { + proposal.Status = types.StatusFailed + tagValue = types.AttributeValueProposalFailed + logMsg = fmt.Sprintf("passed, but failed on execution: %s", err) + } + } else if proposal.IsExpedited { + proposal.IsExpedited = false + proposal.VotingEndTime = proposal.VotingStartTime.Add(keeper.GetVotingParams(ctx).VotingPeriod) + keeper.InsertActiveProposalQueue(ctx, proposal.ProposalId, proposal.VotingEndTime) + tagValue = types.AttributeValueExpeditedConverted + logMsg = "expedited proposal converted to regular" + } else { + proposal.Status = types.StatusRejected + tagValue = types.AttributeValueProposalRejected + logMsg = "rejected" + } + + proposal.FinalTallyResult = tallyResults + keeper.SetProposal(ctx, proposal) + keeper.AfterProposalVotingPeriodEnded(ctx, proposal.ProposalId) + + logger.Info( + "proposal tallied", + "proposal", proposal.ProposalId, + "title", proposal.GetTitle(), + "result", logMsg, + ) + ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeActiveProposal, diff --git a/sei-cosmos/x/gov/abci_test.go b/sei-cosmos/x/gov/abci_test.go index 4dd8f55935..511eeb58d6 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" @@ -14,6 +15,7 @@ import ( "github.com/sei-protocol/sei-chain/app/legacyabci" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov" + govkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/keeper" "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking" ) @@ -432,10 +434,18 @@ func TestExpeditedProposalPassAndConvertToRegular(t *testing.T) { require.NoError(t, err) require.NotNil(t, res) + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + if tc.isExpeditedPasses { + // Validator votes YES before the expedited voting period expires. + err = app.GovKeeper.AddVote(ctx, proposal.ProposalId, addrs[0], types.NewNonSplitVoteOption(types.OptionYes)) + require.NoError(t, err) + } + votingParams := app.GovKeeper.GetVotingParams(ctx) newHeader = ctx.BlockHeader() - newHeader.Time = ctx.BlockHeader().Time.Add(app.GovKeeper.GetDepositParams(ctx).MaxDepositPeriod).Add(votingParams.ExpeditedVotingPeriod) + newHeader.Time = proposal.VotingEndTime ctx = ctx.WithBlockHeader(newHeader) inactiveQueue = app.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) @@ -446,18 +456,12 @@ func TestExpeditedProposalPassAndConvertToRegular(t *testing.T) { require.True(t, activeQueue.Valid()) activeProposalID := types.GetProposalIDFromBytes(activeQueue.Value()) - proposal, ok := app.GovKeeper.GetProposal(ctx, activeProposalID) + proposal, ok = app.GovKeeper.GetProposal(ctx, activeProposalID) require.True(t, ok) require.Equal(t, types.StatusVotingPeriod, proposal.Status) activeQueue.Close() - if tc.isExpeditedPasses { - // Validator votes YES, letting the expedited proposal pass. - err = app.GovKeeper.AddVote(ctx, proposal.ProposalId, addrs[0], types.NewNonSplitVoteOption(types.OptionYes)) - require.NoError(t, err) - } - // Here the expedited proposal is converted to regular after expiry. gov.EndBlocker(ctx, app.GovKeeper) @@ -465,6 +469,7 @@ func TestExpeditedProposalPassAndConvertToRegular(t *testing.T) { if tc.isExpeditedPasses { require.False(t, activeQueue.Valid()) + activeQueue.Close() proposal, ok = app.GovKeeper.GetProposal(ctx, activeProposalID) require.True(t, ok) @@ -485,9 +490,7 @@ func TestExpeditedProposalPassAndConvertToRegular(t *testing.T) { } // Expedited proposal should be converted to a regular proposal instead. - require.True(t, activeQueue.Valid()) - - activeProposalID = types.GetProposalIDFromBytes(activeQueue.Value()) + require.False(t, activeQueue.Valid()) activeQueue.Close() proposal, ok = app.GovKeeper.GetProposal(ctx, activeProposalID) @@ -506,8 +509,14 @@ func TestExpeditedProposalPassAndConvertToRegular(t *testing.T) { expectedIntermediateMofuleAccCoings := initialModuleAccCoins.Add(proposalCoins...).Add(proposalCoins...) require.Equal(t, expectedIntermediateMofuleAccCoings, intermediateModuleAccCoins) + if tc.isRegularEventuallyPassing { + // Validator votes YES before the converted regular voting period expires. + err = app.GovKeeper.AddVote(ctx, proposal.ProposalId, addrs[0], types.NewNonSplitVoteOption(types.OptionYes)) + require.NoError(t, err) + } + // block header time at the voting period - newHeader.Time = ctx.BlockHeader().Time.Add(app.GovKeeper.GetDepositParams(ctx).MaxDepositPeriod).Add(votingParams.VotingPeriod) + newHeader.Time = proposal.VotingEndTime ctx = ctx.WithBlockHeader(newHeader) inactiveQueue = app.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) @@ -517,12 +526,6 @@ func TestExpeditedProposalPassAndConvertToRegular(t *testing.T) { activeQueue = app.GovKeeper.ActiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) require.True(t, activeQueue.Valid()) - if tc.isRegularEventuallyPassing { - // Validator votes YES, letting the converted regular proposal pass. - err = app.GovKeeper.AddVote(ctx, proposal.ProposalId, addrs[0], types.NewNonSplitVoteOption(types.OptionYes)) - require.NoError(t, err) - } - // Here we validate the converted regular proposal gov.EndBlocker(ctx, app.GovKeeper) @@ -606,6 +609,322 @@ func TestEndBlockerProposalHandlerFailed(t *testing.T) { gov.EndBlocker(ctx, app.GovKeeper) } +func TestEndBlockerBoundsVoteBackfillTallyAndCleanupWork(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), + )) + } + store := ctx.KVStore(app.GetKey(types.StoreKey)) + for i := 0; i < gov.MaxVotesProcessedPerBlock+1; i++ { + addr := make(sdk.AccAddress, 20) + binary.BigEndian.PutUint64(addr[12:], uint64(i+1)) + store.Delete(types.VoteDelegationsKey(proposal.ProposalId, addr)) + store.Delete(types.VoterProposalsKey(addr, proposal.ProposalId)) + } + require.NoError(t, govkeeper.NewMigrator(app.GovKeeper).Migrate3to4(ctx)) + + 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.False(t, app.GovKeeper.IsTallying(ctx, proposal.ProposalId)) + require.True(t, app.GovKeeper.IsVoteDelegationBackfillInProgress(ctx, proposal.ProposalId)) + require.Len(t, app.GovKeeper.GetVotes(ctx, proposal.ProposalId), gov.MaxVotesProcessedPerBlock+1) + require.Empty(t, app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, false)) + require.Len(t, app.GovKeeper.GetArchivedTallyVotes(ctx, cleanupProposal.ProposalId, false), 1) + tracked := 0 + for i := 0; i < gov.MaxVotesProcessedPerBlock+1; i++ { + addr := make(sdk.AccAddress, 20) + binary.BigEndian.PutUint64(addr[12:], uint64(i+1)) + if store.Has(types.VoteDelegationsKey(proposal.ProposalId, addr)) { + tracked++ + } + } + require.Equal(t, 900, tracked) + + 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.StatusVotingPeriod, proposal.Status) + require.True(t, app.GovKeeper.IsTallying(ctx, proposal.ProposalId)) + require.False(t, app.GovKeeper.IsVoteDelegationBackfillInProgress(ctx, proposal.ProposalId)) + require.Len(t, app.GovKeeper.GetVotes(ctx, proposal.ProposalId), gov.MaxVotesProcessedPerBlock+1) + require.Empty(t, app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, false)) + require.Empty(t, app.GovKeeper.GetArchivedTallyVotes(ctx, cleanupProposal.ProposalId, false)) + + 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), gov.MaxVotesProcessedPerBlock) + + 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), 2) + + gov.EndBlocker(ctx, app.GovKeeper) + require.Empty(t, app.GovKeeper.GetArchivedTallyVotes(ctx, proposal.ProposalId, false)) +} + +func TestEndBlockerSharesVoteBudgetAcrossExpiredProposals(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + firstProposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + app.GovKeeper.ActivateVotingPeriod(ctx, firstProposal) + firstProposal, found := app.GovKeeper.GetProposal(ctx, firstProposal.ProposalId) + require.True(t, found) + + secondProposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + app.GovKeeper.ActivateVotingPeriod(ctx, secondProposal) + secondProposal, found = app.GovKeeper.GetProposal(ctx, secondProposal.ProposalId) + require.True(t, found) + require.Equal(t, firstProposal.VotingEndTime, secondProposal.VotingEndTime) + + addVotes := func(proposalID uint64, count int) { + for i := 0; i < count; i++ { + addr := make(sdk.AccAddress, 20) + voterID := proposalID*uint64(gov.MaxVotesProcessedPerBlock+1) + uint64(i+1) + binary.BigEndian.PutUint64(addr[12:], voterID) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposalID, + addr, + types.NewNonSplitVoteOption(types.OptionYes), + )) + } + } + addVotes(firstProposal.ProposalId, gov.MaxVotesProcessedPerBlock+1) + addVotes(secondProposal.ProposalId, gov.MaxVotesProcessedPerBlock) + + ctx = ctx.WithBlockTime(firstProposal.VotingEndTime) + gov.EndBlocker(ctx, app.GovKeeper) + + firstProposal, found = app.GovKeeper.GetProposal(ctx, firstProposal.ProposalId) + require.True(t, found) + require.Equal(t, types.StatusVotingPeriod, firstProposal.Status) + require.True(t, app.GovKeeper.IsTallying(ctx, firstProposal.ProposalId)) + require.Len(t, app.GovKeeper.GetArchivedTallyVotes(ctx, firstProposal.ProposalId, false), gov.MaxVotesProcessedPerBlock) + + secondProposal, found = app.GovKeeper.GetProposal(ctx, secondProposal.ProposalId) + require.True(t, found) + require.Equal(t, types.StatusVotingPeriod, secondProposal.Status) + require.False(t, app.GovKeeper.IsTallying(ctx, secondProposal.ProposalId)) + require.Empty(t, app.GovKeeper.GetArchivedTallyVotes(ctx, secondProposal.ProposalId, false)) + + gov.EndBlocker(ctx, app.GovKeeper) + + firstProposal, found = app.GovKeeper.GetProposal(ctx, firstProposal.ProposalId) + require.True(t, found) + require.Equal(t, types.StatusRejected, firstProposal.Status) + require.False(t, app.GovKeeper.IsTallying(ctx, firstProposal.ProposalId)) + + secondProposal, found = app.GovKeeper.GetProposal(ctx, secondProposal.ProposalId) + require.True(t, found) + require.Equal(t, types.StatusVotingPeriod, secondProposal.Status) + require.True(t, app.GovKeeper.IsTallying(ctx, secondProposal.ProposalId)) + require.Len(t, app.GovKeeper.GetArchivedTallyVotes(ctx, secondProposal.ProposalId, false), gov.MaxVotesProcessedPerBlock-1) + + gov.EndBlocker(ctx, app.GovKeeper) + + secondProposal, found = app.GovKeeper.GetProposal(ctx, secondProposal.ProposalId) + require.True(t, found) + require.Equal(t, types.StatusRejected, secondProposal.Status) + require.False(t, app.GovKeeper.IsTallying(ctx, secondProposal.ProposalId)) +} + +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) + votingParams := app.GovKeeper.GetVotingParams(ctx) + conversionTime := proposal.VotingStartTime.Add(votingParams.VotingPeriod).Add(time.Second) + ctx = ctx.WithBlockTime(conversionTime) + 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.Equal(t, conversionTime.Add(votingParams.VotingPeriod-votingParams.ExpeditedVotingPeriod), proposal.VotingEndTime) + require.True(t, proposal.VotingEndTime.After(conversionTime)) + 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) +} + +func TestConvertedLegacyExpeditedProposalUsesDeadlineTally(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + addrs := seiapp.AddTestAddrs(app, ctx, 2, valTokens) + + store := ctx.KVStore(app.GetKey(types.StoreKey)) + store.Delete(types.IncrementalTallyEnabledKey) + store.Delete(types.VoteDelegationBackfillCutoffKey) + store.Delete(types.DeadlineBoundaryBlockTimeKey) + + 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) + + require.NoError(t, govkeeper.NewMigrator(app.GovKeeper).Migrate3to4(ctx)) + ctx = ctx.WithBlockTime(proposal.VotingEndTime) + gov.EndBlocker(ctx, app.GovKeeper) + gov.EndBlocker(ctx, app.GovKeeper) + + proposal, found = app.GovKeeper.GetProposal(ctx, proposal.ProposalId) + require.True(t, found) + require.False(t, proposal.IsExpedited) + require.True(t, app.GovKeeper.IsModernTallyRound(ctx, proposal.ProposalId)) + require.True(t, store.Has(types.ProposalDeadlineKey(proposal.ProposalId, proposal.VotingEndTime))) + + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + addrs[0], + types.NewNonSplitVoteOption(types.OptionYes), + )) + + deadlineCtx := ctx.WithBlockTime(proposal.VotingEndTime) + app.GovKeeper.CaptureExactTallyBoundary(deadlineCtx) + require.ErrorIs(t, app.GovKeeper.AddVote( + deadlineCtx, + proposal.ProposalId, + addrs[1], + types.NewNonSplitVoteOption(types.OptionNo), + ), types.ErrInactiveProposal) +} + +func TestEndBlockerPreservesLegacyTallyBeforeActivation(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + store := ctx.KVStore(app.GetKey(types.StoreKey)) + store.Delete(types.IncrementalTallyEnabledKey) + + proposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + proposal.Status = types.StatusVotingPeriod + proposal.VotingEndTime = ctx.BlockTime() + app.GovKeeper.SetProposal(ctx, proposal) + app.GovKeeper.InsertActiveProposalQueue(ctx, proposal.ProposalId, proposal.VotingEndTime) + + for i := 0; i < gov.MaxVotesProcessedPerBlock+1; i++ { + voter := make(sdk.AccAddress, 20) + binary.BigEndian.PutUint64(voter[12:], uint64(i+1)) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + voter, + types.NewNonSplitVoteOption(types.OptionYes), + )) + } + + require.NotPanics(t, func() { + 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.Empty(t, app.GovKeeper.GetVotes(ctx, proposal.ProposalId)) + require.False(t, store.Has(types.TallyProgressKey(proposal.ProposalId))) + archivedVotes := sdk.KVStorePrefixIterator(store, types.TallyVotesKey(proposal.ProposalId, false)) + require.False(t, archivedVotes.Valid()) + require.NoError(t, archivedVotes.Close()) +} + // 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..a80f02a495 100644 --- a/sei-cosmos/x/gov/genesis.go +++ b/sei-cosmos/x/gov/genesis.go @@ -10,10 +10,15 @@ import ( // InitGenesis - store genesis parameters func InitGenesis(ctx sdk.Context, ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper, data *types.GenesisState) { + k.EnableIncrementalTally(ctx) k.SetProposalID(ctx, data.StartingProposalId) k.SetDepositParams(ctx, data.DepositParams) k.SetVotingParams(ctx, data.VotingParams) k.SetTallyParams(ctx, data.TallyParams) + if data.VoteDelegationBackfillCutoff != 0 { + k.SetVoteDelegationBackfillCutoff(ctx, data.VoteDelegationBackfillCutoff) + } + k.InitializeDeadlineBoundaryClock(ctx) // check if the deposits pool account exists moduleAcc := k.GetGovernanceAccount(ctx) @@ -30,13 +35,41 @@ 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) + } + proposalsByID := make(map[uint64]types.Proposal, len(data.Proposals)) + for _, proposal := range data.Proposals { + proposalsByID[proposal.ProposalId] = proposal + } + for _, proposalID := range data.ModernTallyRoundProposalIds { + if _, found := proposalsByID[proposalID]; !found { + panic(fmt.Sprintf("modern tally round for proposal %d does not exist", proposalID)) + } + k.SetModernTallyRound(ctx, proposalID) + } + for _, electorate := range data.TallyElectorates { + proposal, found := proposalsByID[electorate.ProposalId] + if !found || proposal.Status != types.StatusVotingPeriod || proposal.VotingEndTime.After(ctx.BlockTime()) { + panic(fmt.Sprintf("tally electorate for proposal %d precedes its voting end time", electorate.ProposalId)) + } + k.SetTallyElectorate(ctx, electorate) + k.CompleteVoteDelegationBackfill(ctx, electorate.ProposalId) + } for _, proposal := range data.Proposals { switch proposal.Status { case types.StatusDepositPeriod: k.InsertInactiveProposalQueue(ctx, proposal.ProposalId, proposal.DepositEndTime) case types.StatusVotingPeriod: - k.InsertActiveProposalQueue(ctx, proposal.ProposalId, proposal.VotingEndTime) + if k.IsModernTallyRound(ctx, proposal.ProposalId) { + k.InsertActiveProposalQueueForModernTallyRound(ctx, proposal.ProposalId, proposal.VotingEndTime) + } else { + k.InsertActiveProposalQueue(ctx, proposal.ProposalId, proposal.VotingEndTime) + } + if !proposal.VotingEndTime.After(ctx.BlockTime()) && !k.VoteDelegationBackfillRequired(ctx, proposal.ProposalId) { + k.InitializeTally(ctx, proposal) + } } k.SetProposal(ctx, proposal) } @@ -56,6 +89,7 @@ func InitGenesis(ctx sdk.Context, ak types.AccountKeeper, bk types.BankKeeper, k // ExportGenesis - output genesis parameters func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState { startingProposalID, _ := k.GetProposalID(ctx) + voteDelegationBackfillCutoff, _ := k.GetVoteDelegationBackfillCutoff(ctx) depositParams := k.GetDepositParams(ctx) votingParams := k.GetVotingParams(ctx) tallyParams := k.GetTallyParams(ctx) @@ -63,21 +97,35 @@ 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)) + tallyElectorates := make([]types.TallyElectorate, 0, len(proposals)) + modernTallyRoundProposalIDs := make([]uint64, 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)...) + if electorate, found := k.ExportTallyElectorate(ctx, proposal); found { + tallyElectorates = append(tallyElectorates, electorate) + } + if k.IsModernTallyRound(ctx, proposal.ProposalId) { + modernTallyRoundProposalIDs = append(modernTallyRoundProposalIDs, proposal.ProposalId) + } } 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, + TallyElectorates: tallyElectorates, + VoteDelegationBackfillCutoff: voteDelegationBackfillCutoff, + ModernTallyRoundProposalIds: modernTallyRoundProposalIDs, } } diff --git a/sei-cosmos/x/gov/genesis_test.go b/sei-cosmos/x/gov/genesis_test.go index 176735c39e..f7cd676a97 100644 --- a/sei-cosmos/x/gov/genesis_test.go +++ b/sei-cosmos/x/gov/genesis_test.go @@ -2,8 +2,10 @@ package gov_test import ( "context" + "encoding/binary" "encoding/json" "testing" + "time" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" @@ -16,7 +18,10 @@ import ( authtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/types" banktypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/bank/types" "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov" + govkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/keeper" "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking" + stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" ) func TestImportExportQueues(t *testing.T) { @@ -168,3 +173,338 @@ 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{}) + addrs := seiapp.AddTestAddrs(app, ctx, 3, valTokens) + SortAddresses(addrs) + stakingParams := app.StakingKeeper.GetParams(ctx) + stakingParams.MinCommissionRate = sdk.ZeroDec() + app.StakingKeeper.SetParams(ctx, stakingParams) + createValidators( + t, + staking.NewHandler(app.StakingKeeper), + ctx, + seiapp.ConvertAddrsToValAddrs(addrs), + []int64{6, 3, 1}, + ) + staking.EndBlocker(ctx, app.StakingKeeper) + + 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, option := range []types.VoteOption{types.OptionYes, types.OptionNo, types.OptionAbstain} { + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + addrs[i], + types.NewNonSplitVoteOption(option), + )) + } + + ctx = ctx.WithBlockTime(proposal.VotingEndTime) + app.GovKeeper.CaptureExactTallyBoundary(ctx) + complete, processed, _, _, _ := app.GovKeeper.TallyIncremental(ctx, proposal, 1) + require.False(t, complete) + require.Equal(t, 1, processed) + + validator, found := app.StakingKeeper.GetValidator(ctx, seiapp.ConvertAddrsToValAddrs(addrs)[0]) + require.True(t, found) + _, err = app.StakingKeeper.Delegate( + ctx, + addrs[0], + app.StakingKeeper.TokensFromConsensusPower(ctx, 20), + stakingtypes.Unbonded, + validator, + true, + ) + require.NoError(t, err) + mutatedTallyParams := app.GovKeeper.GetTallyParams(ctx) + mutatedTallyParams.Threshold = sdk.MustNewDecFromStr("0.90") + mutatedTallyParams.ExpeditedThreshold = sdk.MustNewDecFromStr("0.95") + app.GovKeeper.SetTallyParams(ctx, mutatedTallyParams) + + genesis := gov.ExportGenesis(ctx, app.GovKeeper) + require.Len(t, genesis.Votes, 3) + require.Len(t, genesis.VoteDelegationSnapshots, 3) + require.Len(t, genesis.TallyElectorates, 1) + genesisJSON := app.AppCodec().MustMarshalJSON(genesis) + var decodedGenesis types.GenesisState + app.AppCodec().MustUnmarshalJSON(genesisJSON, &decodedGenesis) + require.True(t, genesis.Equal(decodedGenesis)) + genesis = &decodedGenesis + + authGenesis := auth.ExportGenesis(ctx, app.AccountKeeper) + bankGenesis := app.BankKeeper.ExportGenesis(ctx) + stakingGenesis := staking.ExportGenesis(ctx, app.StakingKeeper) + appGenesis := seiapp.NewDefaultGenesisState(app.AppCodec()) + appGenesis[authtypes.ModuleName] = app.AppCodec().MustMarshalJSON(authGenesis) + appGenesis[banktypes.ModuleName] = app.AppCodec().MustMarshalJSON(bankGenesis) + appGenesis[stakingtypes.ModuleName] = app.AppCodec().MustMarshalJSON(stakingGenesis) + appGenesis[types.ModuleName] = app.AppCodec().MustMarshalJSON(genesis) + stateBytes, err := json.MarshalIndent(appGenesis, "", " ") + require.NoError(t, err) + + complete, processed, sourcePasses, sourceBurnDeposits, sourceTallyResult := app.GovKeeper.TallyIncremental(ctx, proposal, 2) + require.True(t, complete) + require.Equal(t, 2, processed) + require.True(t, sourcePasses) + require.False(t, sourceBurnDeposits) + require.False(t, sourceTallyResult.Equals(types.EmptyTallyResult())) + + db := dbm.NewMemDB() + importedApp := seiapp.SetupWithDB(t, db, true, false, false) + _, err = importedApp.InitChain(&abci.RequestInitChain{ + ConsensusParams: seiapp.DefaultConsensusParams, + AppStateBytes: stateBytes, + Time: proposal.VotingEndTime, + }) + require.NoError(t, err) + importedApp.Commit(context.Background()) + importedCtx := importedApp.BaseApp.NewUncachedContext(false, tmproto.Header{Time: proposal.VotingEndTime}) + + 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) + + complete, processed, importedPasses, importedBurnDeposits, importedTallyResult := importedApp.GovKeeper.TallyIncremental( + importedCtx, + proposal, + 3, + ) + require.True(t, complete) + require.Equal(t, 3, processed) + require.Equal(t, sourcePasses, importedPasses) + require.Equal(t, sourceBurnDeposits, importedBurnDeposits) + require.True(t, sourceTallyResult.Equals(importedTallyResult)) +} + +func TestImportExportPreservesUnresolvedLegacyTally(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + voter := seiapp.AddTestAddrs(app, ctx, 1, valTokens)[0] + + 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) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + voter, + types.NewNonSplitVoteOption(types.OptionYes), + )) + + store := ctx.KVStore(app.GetKey(types.StoreKey)) + store.Delete(types.VoteDelegationsKey(proposal.ProposalId, voter)) + store.Delete(types.VoterProposalsKey(voter, proposal.ProposalId)) + require.NoError(t, govkeeper.NewMigrator(app.GovKeeper).Migrate3to4(ctx)) + + ctx = ctx.WithBlockTime(proposal.VotingEndTime.Add(time.Second)) + genesis := gov.ExportGenesis(ctx, app.GovKeeper) + require.Equal(t, uint64(2), genesis.VoteDelegationBackfillCutoff) + require.Len(t, genesis.TallyElectorates, 1) + genesisJSON := app.AppCodec().MustMarshalJSON(genesis) + var decodedGenesis types.GenesisState + app.AppCodec().MustUnmarshalJSON(genesisJSON, &decodedGenesis) + require.Equal(t, genesis.VoteDelegationBackfillCutoff, decodedGenesis.VoteDelegationBackfillCutoff) + + authGenesis := auth.ExportGenesis(ctx, app.AccountKeeper) + bankGenesis := app.BankKeeper.ExportGenesis(ctx) + stakingGenesis := staking.ExportGenesis(ctx, app.StakingKeeper) + appGenesis := seiapp.NewDefaultGenesisState(app.AppCodec()) + appGenesis[authtypes.ModuleName] = app.AppCodec().MustMarshalJSON(authGenesis) + appGenesis[banktypes.ModuleName] = app.AppCodec().MustMarshalJSON(bankGenesis) + appGenesis[stakingtypes.ModuleName] = app.AppCodec().MustMarshalJSON(stakingGenesis) + appGenesis[types.ModuleName] = genesisJSON + stateBytes, err := json.Marshal(appGenesis) + require.NoError(t, err) + + importedApp := seiapp.SetupWithDB(t, dbm.NewMemDB(), false, false, false) + _, err = importedApp.InitChain(&abci.RequestInitChain{ + ConsensusParams: seiapp.DefaultConsensusParams, + AppStateBytes: stateBytes, + Time: ctx.BlockTime(), + }) + require.NoError(t, err) + importedApp.Commit(context.Background()) + importedCtx := importedApp.BaseApp.NewUncachedContext(false, tmproto.Header{Time: ctx.BlockTime()}) + + cutoff, found := importedApp.GovKeeper.GetVoteDelegationBackfillCutoff(importedCtx) + require.True(t, found) + require.Equal(t, uint64(2), cutoff) + require.False(t, importedApp.GovKeeper.VoteDelegationBackfillRequired(importedCtx, proposal.ProposalId)) + require.True(t, importedApp.GovKeeper.IsTallying(importedCtx, proposal.ProposalId)) + require.False(t, importedCtx.KVStore(importedApp.GetKey(types.StoreKey)).Has(types.ProposalDeadlineKey(proposal.ProposalId, proposal.VotingEndTime))) +} + +func TestImportExportPreservesModernTallyRound(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) + proposal.Status = types.StatusVotingPeriod + proposal.VotingEndTime = ctx.BlockTime().Add(time.Hour) + app.GovKeeper.SetProposal(ctx, proposal) + app.GovKeeper.SetVoteDelegationBackfillCutoff(ctx, proposal.ProposalId+1) + app.GovKeeper.InsertActiveProposalQueueForModernTallyRound(ctx, proposal.ProposalId, proposal.VotingEndTime) + + genesis := gov.ExportGenesis(ctx, app.GovKeeper) + require.Equal(t, []uint64{proposal.ProposalId}, genesis.ModernTallyRoundProposalIds) + + importedApp := seiapp.Setup(t, false, false, false) + importedCtx := importedApp.BaseApp.NewContext(false, tmproto.Header{Time: ctx.BlockTime()}) + gov.InitGenesis(importedCtx, importedApp.AccountKeeper, importedApp.BankKeeper, importedApp.GovKeeper, genesis) + + store := importedCtx.KVStore(importedApp.GetKey(types.StoreKey)) + require.True(t, importedApp.GovKeeper.IsModernTallyRound(importedCtx, proposal.ProposalId)) + require.True(t, store.Has(types.ProposalDeadlineKey(proposal.ProposalId, proposal.VotingEndTime))) + require.False(t, importedApp.GovKeeper.VoteDelegationBackfillRequired(importedCtx, proposal.ProposalId)) +} + +func TestImportExportCanonicalizesPartialLegacyVoteDelegationBackfill(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + t.Cleanup(func() { + require.NoError(t, app.Close()) + }) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + addrs := seiapp.AddTestAddrs(app, ctx, 2, valTokens) + SortAddresses(addrs) + stakingParams := app.StakingKeeper.GetParams(ctx) + stakingParams.MinCommissionRate = sdk.ZeroDec() + app.StakingKeeper.SetParams(ctx, stakingParams) + valAddrs := seiapp.ConvertAddrsToValAddrs(addrs) + createValidators(t, staking.NewHandler(app.StakingKeeper), ctx, valAddrs[:1], []int64{5}) + staking.EndBlocker(ctx, app.StakingKeeper) + + voter := addrs[1] + validator, found := app.StakingKeeper.GetValidator(ctx, valAddrs[0]) + require.True(t, found) + delegatedTokens := app.StakingKeeper.TokensFromConsensusPower(ctx, 10) + _, err := app.StakingKeeper.Delegate(ctx, voter, delegatedTokens, stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + + 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) + + voters := make([]sdk.AccAddress, gov.MaxVotesProcessedPerBlock+1) + voters[0] = voter + for i := 1; i < len(voters); i++ { + voters[i] = make(sdk.AccAddress, 20) + binary.BigEndian.PutUint64(voters[i][12:], uint64(i)) + } + for i, voteVoter := range voters { + option := types.OptionNo + if i == 0 { + option = types.OptionYes + } + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + voteVoter, + types.NewNonSplitVoteOption(option), + )) + } + + store := ctx.KVStore(app.GetKey(types.StoreKey)) + for _, voteVoter := range voters { + store.Delete(types.VoteDelegationsKey(proposal.ProposalId, voteVoter)) + store.Delete(types.VoterProposalsKey(voteVoter, proposal.ProposalId)) + store.Delete(types.VoteDelegationSnapshotRevisionKey(proposal.ProposalId, voteVoter)) + } + require.NoError(t, govkeeper.NewMigrator(app.GovKeeper).Migrate3to4(ctx)) + + ctx = ctx.WithBlockTime(proposal.VotingEndTime.Add(time.Second)) + complete, processed, _, _, _ := app.GovKeeper.TallyIncremental( + ctx, + proposal, + gov.MaxVotesProcessedPerBlock, + ) + require.False(t, complete) + require.Equal(t, gov.MaxVotesProcessedPerBlock, processed) + require.True(t, app.GovKeeper.IsVoteDelegationBackfillInProgress(ctx, proposal.ProposalId)) + + expectedPasses, expectedBurnDeposits, expectedTallyResult := app.GovKeeper.Tally(ctx, proposal) + genesis := gov.ExportGenesis(ctx, app.GovKeeper) + require.Len(t, genesis.Votes, gov.MaxVotesProcessedPerBlock+1) + require.Len(t, genesis.VoteDelegationSnapshots, gov.MaxVotesProcessedPerBlock+1) + require.Len(t, genesis.TallyElectorates, 1) + + authGenesis := auth.ExportGenesis(ctx, app.AccountKeeper) + bankGenesis := app.BankKeeper.ExportGenesis(ctx) + stakingGenesis := staking.ExportGenesis(ctx, app.StakingKeeper) + appGenesis := seiapp.NewDefaultGenesisState(app.AppCodec()) + appGenesis[authtypes.ModuleName] = app.AppCodec().MustMarshalJSON(authGenesis) + appGenesis[banktypes.ModuleName] = app.AppCodec().MustMarshalJSON(bankGenesis) + appGenesis[stakingtypes.ModuleName] = app.AppCodec().MustMarshalJSON(stakingGenesis) + appGenesis[types.ModuleName] = app.AppCodec().MustMarshalJSON(genesis) + stateBytes, err := json.Marshal(appGenesis) + require.NoError(t, err) + + importedApp := seiapp.SetupWithDB(t, dbm.NewMemDB(), false, false, false) + t.Cleanup(func() { + require.NoError(t, importedApp.Close()) + }) + _, err = importedApp.InitChain(&abci.RequestInitChain{ + ConsensusParams: seiapp.DefaultConsensusParams, + AppStateBytes: stateBytes, + Time: ctx.BlockTime(), + }) + require.NoError(t, err) + importedApp.Commit(context.Background()) + importedCtx := importedApp.BaseApp.NewUncachedContext(false, tmproto.Header{Time: ctx.BlockTime()}) + importedProposal, found := importedApp.GovKeeper.GetProposal(importedCtx, proposal.ProposalId) + require.True(t, found) + require.False(t, importedApp.GovKeeper.VoteDelegationBackfillRequired(importedCtx, proposal.ProposalId)) + require.True(t, importedApp.GovKeeper.IsTallying(importedCtx, proposal.ProposalId)) + + delegation, found := importedApp.StakingKeeper.GetDelegation(importedCtx, voter, valAddrs[0]) + require.True(t, found) + delegation.Shares = delegation.Shares.Add(delegatedTokens.ToDec()) + importedApp.StakingKeeper.SetDelegation(importedCtx, delegation) + importedApp.GovKeeper.StakingHooks().AfterDelegationModified(importedCtx, voter, valAddrs[0]) + importedPasses, importedBurnDeposits, importedTallyResult := importedApp.GovKeeper.Tally(importedCtx, importedProposal) + require.Equal(t, expectedPasses, importedPasses) + require.Equal(t, expectedBurnDeposits, importedBurnDeposits) + require.True(t, expectedTallyResult.Equals(importedTallyResult)) +} + +func TestInitGenesisRejectsFutureTallyElectorate(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) + + genesis := gov.ExportGenesis(ctx, app.GovKeeper) + genesis.TallyElectorates = []types.TallyElectorate{{ + ProposalId: proposal.ProposalId, + TotalBondedTokens: sdk.ZeroInt(), + TallyParams: types.DefaultTallyParams(), + TallyValidators: []types.TallyValidator{}, + }} + + importedApp := seiapp.Setup(t, false, false, false) + importedCtx := importedApp.BaseApp.NewContext(false, tmproto.Header{}) + require.Panics(t, func() { + gov.InitGenesis(importedCtx, importedApp.AccountKeeper, importedApp.BankKeeper, importedApp.GovKeeper, genesis) + }) +} diff --git a/sei-cosmos/x/gov/keeper/common_test.go b/sei-cosmos/x/gov/keeper/common_test.go index 3424b5b753..8ecce56efe 100644 --- a/sei-cosmos/x/gov/keeper/common_test.go +++ b/sei-cosmos/x/gov/keeper/common_test.go @@ -31,6 +31,8 @@ func createValidators(t *testing.T, ctx sdk.Context, app *seiapp.App, powers []i app.BankKeeper, app.GetSubspace(stakingtypes.ModuleName), ) + stakingHooks := stakingtypes.NewMultiStakingHooks(app.GovKeeper.StakingHooks()) + app.StakingKeeper.SetHooks(&stakingHooks) val1, err := stakingtypes.NewValidator(valAddrs[0], pks[0], stakingtypes.Description{}) require.NoError(t, err) diff --git a/sei-cosmos/x/gov/keeper/delegation_updates.go b/sei-cosmos/x/gov/keeper/delegation_updates.go new file mode 100644 index 0000000000..44204ca567 --- /dev/null +++ b/sei-cosmos/x/gov/keeper/delegation_updates.go @@ -0,0 +1,346 @@ +package keeper + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "math" + "time" + + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" +) + +type pendingVoteDelegationUpdate struct { + Voter string `json:"voter"` + Validator string `json:"validator"` + Shares sdk.Dec `json:"shares"` + BlockTime time.Time `json:"block_time"` + Cursor []byte `json:"cursor,omitempty"` +} + +// QueueVoteDelegationUpdate defers one slash-induced delegation change for bounded processing. +func (keeper Keeper) QueueVoteDelegationUpdate( + ctx sdk.Context, + voter sdk.AccAddress, + validator sdk.ValAddress, + shares sdk.Dec, +) { + if !keeper.IncrementalTallyEnabled(ctx) || !keeper.voterHasTrackedProposals(ctx, voter) { + return + } + + sequence := keeper.nextVoteDelegationUpdateSequence(ctx) + update := pendingVoteDelegationUpdate{ + Voter: voter.String(), + Validator: validator.String(), + Shares: shares, + BlockTime: ctx.BlockTime(), + } + store := ctx.KVStore(keeper.storeKey) + store.Set(types.VoteDelegationUpdateKey(sequence), marshalVoteDelegationUpdate(update)) + store.Set(types.VoterVoteDelegationUpdateKey(voter, sequence), []byte{1}) +} + +// ProcessVoteDelegationUpdates applies at most maxUpdates deferred snapshot updates. +func (keeper Keeper) ProcessVoteDelegationUpdates(ctx sdk.Context, maxUpdates int) (complete bool, processed int) { + return keeper.ProcessVoteDelegationUpdatesThrough(ctx, maxUpdates, math.MaxUint64) +} + +// ProcessVoteDelegationUpdatesThrough applies deferred snapshot updates through a sequence. +func (keeper Keeper) ProcessVoteDelegationUpdatesThrough( + ctx sdk.Context, + maxUpdates int, + throughSequence uint64, +) (complete bool, processed int) { + if maxUpdates < 0 { + panic("maximum vote delegation updates cannot be negative") + } + if !keeper.IncrementalTallyEnabled(ctx) { + return true, 0 + } + + store := ctx.KVStore(keeper.storeKey) + iterator := sdk.KVStorePrefixIterator(store, types.VoteDelegationUpdatesKeyPrefix) + defer func() { _ = iterator.Close() }() + + for ; iterator.Valid() && processed < maxUpdates; iterator.Next() { + key := append([]byte(nil), iterator.Key()...) + sequence := voteDelegationUpdateSequenceFromKey(key) + if sequence > throughSequence { + return true, processed + } + update := unmarshalVoteDelegationUpdate(iterator.Value()) + var updateComplete bool + updateComplete, processed = keeper.processVoteDelegationUpdate( + ctx, + sequence, + update, + maxUpdates, + processed, + ) + if !updateComplete { + return false, processed + } + store.Delete(key) + voter := sdk.MustAccAddressFromBech32(update.Voter) + store.Delete(types.VoterVoteDelegationUpdateKey(voter, sequence)) + } + + return !iterator.Valid() || voteDelegationUpdateSequenceFromKey(iterator.Key()) > throughSequence, processed +} + +// HasPendingVoteDelegationUpdates reports whether slash-induced snapshot work remains. +func (keeper Keeper) HasPendingVoteDelegationUpdates(ctx sdk.Context) bool { + if !keeper.IncrementalTallyEnabled(ctx) { + return false + } + iterator := sdk.KVStorePrefixIterator(ctx.KVStore(keeper.storeKey), types.VoteDelegationUpdatesKeyPrefix) + defer func() { _ = iterator.Close() }() + return iterator.Valid() +} + +func (keeper Keeper) voterHasTrackedProposals(ctx sdk.Context, voter sdk.AccAddress) bool { + iterator := sdk.KVStorePrefixIterator( + ctx.KVStore(keeper.storeKey), + types.VoterProposalsKeyPrefixForAddress(voter), + ) + defer func() { _ = iterator.Close() }() + return iterator.Valid() +} + +func (keeper Keeper) nextVoteDelegationUpdateSequence(ctx sdk.Context) uint64 { + store := ctx.KVStore(keeper.storeKey) + sequence := decodeVoteDelegationUpdateSequence(store.Get(types.VoteDelegationUpdateSequenceKey)) + if sequence == math.MaxUint64 { + panic("vote delegation update sequence overflow") + } + sequence++ + store.Set(types.VoteDelegationUpdateSequenceKey, types.GetProposalIDBytes(sequence)) + return sequence +} + +func (keeper Keeper) processVoteDelegationUpdate( + ctx sdk.Context, + sequence uint64, + update pendingVoteDelegationUpdate, + maxUpdates int, + processed int, +) (complete bool, newProcessed int) { + store := ctx.KVStore(keeper.storeKey) + voter := sdk.MustAccAddressFromBech32(update.Voter) + prefix := types.VoterProposalsKeyPrefixForAddress(voter) + start := prefix + if len(update.Cursor) != 0 { + start = sdk.PrefixEndBytes(append(append([]byte(nil), prefix...), update.Cursor...)) + } + iterator := store.Iterator(start, sdk.PrefixEndBytes(prefix)) + defer func() { _ = iterator.Close() }() + + newProcessed = processed + for ; iterator.Valid() && newProcessed < maxUpdates; iterator.Next() { + proposalIDBytes := iterator.Key()[len(prefix):] + if len(proposalIDBytes) != 8 { + panic(fmt.Sprintf("invalid voter proposal key length %d", len(iterator.Key()))) + } + proposalID := types.GetProposalIDFromBytes(proposalIDBytes) + keeper.applyVoteDelegationUpdate(ctx, proposalID, sequence, update) + update.Cursor = append(update.Cursor[:0], proposalIDBytes...) + newProcessed++ + } + + if iterator.Valid() { + store.Set(types.VoteDelegationUpdateKey(sequence), marshalVoteDelegationUpdate(update)) + return false, newProcessed + } + if newProcessed == processed { + newProcessed++ + } + return true, newProcessed +} + +func (keeper Keeper) applyVoteDelegationUpdate( + ctx sdk.Context, + proposalID uint64, + sequence uint64, + update pendingVoteDelegationUpdate, +) { + voter := sdk.MustAccAddressFromBech32(update.Voter) + if keeper.voteDelegationSnapshotRevision(ctx, proposalID, voter) >= sequence { + return + } + + proposal, found := keeper.GetProposal(ctx, proposalID) + if found && keeper.delegationUpdateBelongsToTallyBoundary(ctx, proposal, sequence, update.BlockTime) { + store := ctx.KVStore(keeper.storeKey) + snapshotKey := types.VoteDelegationsKey(proposalID, voter) + bz := store.Get(snapshotKey) + if bz == nil { + panic(fmt.Sprintf("missing delegation snapshot for proposal %d voter %s", proposalID, voter)) + } + snapshot := keeper.unmarshalVoteDelegations(bz) + index := newVoteDelegationSnapshotIndex(snapshot) + index.set(update.Validator, update.Shares) + keeper.storeVoteDelegationSnapshot(ctx, index.snapshot(), sequence) + return + } + keeper.setVoteDelegationSnapshotRevision(ctx, proposalID, voter, sequence) +} + +func (keeper Keeper) applyVoteDelegationSnapshotUpdates( + ctx sdk.Context, + proposalID uint64, + voter sdk.AccAddress, + snapshot types.VoteDelegationSnapshot, +) types.VoteDelegationSnapshot { + index := newVoteDelegationSnapshotIndex(snapshot) + + proposal, found := keeper.GetProposal(ctx, proposalID) + if !found { + return index.snapshot() + } + + revision := keeper.voteDelegationSnapshotRevision(ctx, proposalID, voter) + prefix := types.VoterVoteDelegationUpdatesKeyPrefixForAddress(voter) + start := prefix + if revision != 0 { + start = sdk.PrefixEndBytes(append(append([]byte(nil), prefix...), types.GetProposalIDBytes(revision)...)) + } + store := ctx.KVStore(keeper.storeKey) + iterator := store.Iterator(start, sdk.PrefixEndBytes(prefix)) + defer func() { _ = iterator.Close() }() + for ; iterator.Valid(); iterator.Next() { + sequence := voteDelegationUpdateSequenceFromVoterKey(iterator.Key(), len(prefix)) + bz := store.Get(types.VoteDelegationUpdateKey(sequence)) + if bz == nil { + panic(fmt.Sprintf("missing vote delegation update %d", sequence)) + } + update := unmarshalVoteDelegationUpdate(bz) + if keeper.delegationUpdateBelongsToTallyBoundary(ctx, proposal, sequence, update.BlockTime) { + index.set(update.Validator, update.Shares) + } + } + return index.snapshot() +} + +func (keeper Keeper) delegationUpdateBelongsToTallyBoundary( + ctx sdk.Context, + proposal types.Proposal, + sequence uint64, + updateTime time.Time, +) bool { + if boundarySequence, found := keeper.proposalTallyBoundarySequence(ctx, proposal); found { + return sequence <= boundarySequence + } + if keeper.usesLegacyTallySemantics(ctx, proposal) { + return !keeper.IsTallying(ctx, proposal.ProposalId) + } + return !proposal.VotingEndTime.Before(updateTime) && !keeper.IsTallying(ctx, proposal.ProposalId) +} + +func (keeper Keeper) deleteVoteDelegationSnapshotRevision(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) { + ctx.KVStore(keeper.storeKey).Delete(types.VoteDelegationSnapshotRevisionKey(proposalID, voter)) +} + +func (keeper Keeper) voteDelegationUpdateSequence(ctx sdk.Context) uint64 { + return decodeVoteDelegationUpdateSequence( + ctx.KVStore(keeper.storeKey).Get(types.VoteDelegationUpdateSequenceKey), + ) +} + +func (keeper Keeper) voteDelegationSnapshotRevision(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) uint64 { + return decodeVoteDelegationUpdateSequence( + ctx.KVStore(keeper.storeKey).Get(types.VoteDelegationSnapshotRevisionKey(proposalID, voter)), + ) +} + +func (keeper Keeper) setVoteDelegationSnapshotRevision( + ctx sdk.Context, + proposalID uint64, + voter sdk.AccAddress, + revision uint64, +) { + ctx.KVStore(keeper.storeKey).Set( + types.VoteDelegationSnapshotRevisionKey(proposalID, voter), + types.GetProposalIDBytes(revision), + ) +} + +func marshalVoteDelegationUpdate(update pendingVoteDelegationUpdate) []byte { + bz, err := json.Marshal(update) + if err != nil { + panic(fmt.Errorf("marshal vote delegation update: %w", err)) + } + return bz +} + +func unmarshalVoteDelegationUpdate(bz []byte) pendingVoteDelegationUpdate { + var update pendingVoteDelegationUpdate + if err := json.Unmarshal(bz, &update); err != nil { + panic(fmt.Errorf("unmarshal vote delegation update: %w", err)) + } + return update +} + +func decodeVoteDelegationUpdateSequence(bz []byte) uint64 { + if bz == nil { + return 0 + } + if len(bz) != 8 { + panic(fmt.Sprintf("invalid vote delegation update sequence length %d", len(bz))) + } + return binary.BigEndian.Uint64(bz) +} + +func voteDelegationUpdateSequenceFromKey(key []byte) uint64 { + if len(key) != len(types.VoteDelegationUpdatesKeyPrefix)+8 { + panic(fmt.Sprintf("invalid vote delegation update key length %d", len(key))) + } + return binary.BigEndian.Uint64(key[len(types.VoteDelegationUpdatesKeyPrefix):]) +} + +func voteDelegationUpdateSequenceFromVoterKey(key []byte, prefixLength int) uint64 { + if len(key) != prefixLength+8 { + panic(fmt.Sprintf("invalid voter vote delegation update key length %d", len(key))) + } + return binary.BigEndian.Uint64(key[prefixLength:]) +} + +type voteDelegationSnapshotIndex struct { + value types.VoteDelegationSnapshot + positions map[string]int +} + +func newVoteDelegationSnapshotIndex(snapshot types.VoteDelegationSnapshot) *voteDelegationSnapshotIndex { + positions := make(map[string]int, len(snapshot.Delegations)) + for i, delegation := range snapshot.Delegations { + positions[delegation.Validator] = i + } + return &voteDelegationSnapshotIndex{value: snapshot, positions: positions} +} + +func (index *voteDelegationSnapshotIndex) set(validator string, shares sdk.Dec) { + if position, found := index.positions[validator]; found { + index.value.Delegations[position].Shares = shares + return + } + if shares.IsZero() { + return + } + index.positions[validator] = len(index.value.Delegations) + index.value.Delegations = append(index.value.Delegations, types.VoteDelegation{ + Validator: validator, + Shares: shares, + }) +} + +func (index *voteDelegationSnapshotIndex) snapshot() types.VoteDelegationSnapshot { + delegations := index.value.Delegations[:0] + for _, delegation := range index.value.Delegations { + if !delegation.Shares.IsZero() { + delegations = append(delegations, delegation) + } + } + index.value.Delegations = delegations + return index.value +} diff --git a/sei-cosmos/x/gov/keeper/delegation_updates_test.go b/sei-cosmos/x/gov/keeper/delegation_updates_test.go new file mode 100644 index 0000000000..da176cf488 --- /dev/null +++ b/sei-cosmos/x/gov/keeper/delegation_updates_test.go @@ -0,0 +1,393 @@ +package keeper_test + +import ( + "testing" + "time" + + tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" + "github.com/stretchr/testify/require" + + seiapp "github.com/sei-protocol/sei-chain/app" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + gov "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov" + govtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" + stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" +) + +func TestSlashDelegationUpdatesAreDeferredAndBounded(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{Time: time.Unix(100, 0)}) + addrs, valAddrs := createValidators(t, ctx, app, []int64{5, 5, 5}) + + proposals := make([]govtypes.Proposal, 2) + for i := range proposals { + proposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + proposal.Status = govtypes.StatusVotingPeriod + proposal.VotingEndTime = ctx.BlockTime().Add(time.Hour) + app.GovKeeper.SetProposal(ctx, proposal) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + addrs[0], + govtypes.NewNonSplitVoteOption(govtypes.OptionYes), + )) + proposals[i] = proposal + } + + store := ctx.KVStore(app.GetKey(govtypes.StoreKey)) + firstSnapshotKey := govtypes.VoteDelegationsKey(proposals[0].ProposalId, addrs[0]) + secondSnapshotKey := govtypes.VoteDelegationsKey(proposals[1].ProposalId, addrs[0]) + firstSnapshotBefore := append([]byte(nil), store.Get(firstSnapshotKey)...) + secondSnapshotBefore := append([]byte(nil), store.Get(secondSnapshotKey)...) + + delegation, found := app.StakingKeeper.GetDelegation(ctx, addrs[0], valAddrs[0]) + require.True(t, found) + updatedShares := delegation.Shares.Sub(sdk.OneDec()) + delegation.Shares = updatedShares + app.StakingKeeper.SetDelegation(ctx, delegation) + + slashCtx := stakingtypes.WithSlashDelegationModification(ctx) + app.GovKeeper.StakingHooks().AfterDelegationModified(slashCtx, addrs[0], valAddrs[0]) + + require.True(t, app.GovKeeper.HasPendingVoteDelegationUpdates(ctx)) + require.Equal(t, firstSnapshotBefore, store.Get(firstSnapshotKey)) + require.Equal(t, secondSnapshotBefore, store.Get(secondSnapshotKey)) + requireVoteDelegationShares(t, app.GovKeeper.GetVoteDelegationSnapshots(ctx, proposals[0]), valAddrs[0], updatedShares) + requireVoteDelegationShares(t, app.GovKeeper.GetVoteDelegationSnapshots(ctx, proposals[1]), valAddrs[0], updatedShares) + + complete, processed := app.GovKeeper.ProcessVoteDelegationUpdates(ctx, 1) + require.False(t, complete) + require.Equal(t, 1, processed) + require.True(t, app.GovKeeper.HasPendingVoteDelegationUpdates(ctx)) + require.NotEqual(t, firstSnapshotBefore, store.Get(firstSnapshotKey)) + require.Equal(t, secondSnapshotBefore, store.Get(secondSnapshotKey)) + requireVoteDelegationShares( + t, + []govtypes.VoteDelegationSnapshot{decodeVoteDelegationSnapshot(t, store.Get(firstSnapshotKey))}, + valAddrs[0], + updatedShares, + ) + requireVoteDelegationShares(t, app.GovKeeper.GetVoteDelegationSnapshots(ctx, proposals[0]), valAddrs[0], updatedShares) + requireVoteDelegationShares(t, app.GovKeeper.GetVoteDelegationSnapshots(ctx, proposals[1]), valAddrs[0], updatedShares) + + complete, processed = app.GovKeeper.ProcessVoteDelegationUpdates(ctx, 1) + require.True(t, complete) + require.Equal(t, 1, processed) + require.False(t, app.GovKeeper.HasPendingVoteDelegationUpdates(ctx)) + require.NotEqual(t, secondSnapshotBefore, store.Get(secondSnapshotKey)) + requireVoteDelegationShares( + t, + []govtypes.VoteDelegationSnapshot{decodeVoteDelegationSnapshot(t, store.Get(secondSnapshotKey))}, + valAddrs[0], + updatedShares, + ) + + complete, processed, _, _, _ = app.GovKeeper.TallyIncremental(ctx, proposals[0], 1) + require.True(t, complete) + require.Equal(t, 1, processed) + archivedSnapshot := decodeVoteDelegationSnapshot( + t, + store.Get(govtypes.TallyVoteDelegationsKey(proposals[0].ProposalId, false, addrs[0])), + ) + requireVoteDelegationShares(t, []govtypes.VoteDelegationSnapshot{archivedSnapshot}, valAddrs[0], updatedShares) + require.False(t, store.Has(govtypes.VoteDelegationSnapshotRevisionKey(proposals[0].ProposalId, addrs[0]))) +} + +func TestTallySharesRecordBudgetWithCanonicalDelegationUpdates(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{Time: time.Unix(100, 0)}) + addrs, valAddrs := createValidators(t, ctx, app, []int64{5, 5, 5}) + + secondValidator, found := app.StakingKeeper.GetValidator(ctx, valAddrs[1]) + require.True(t, found) + _, err := app.StakingKeeper.Delegate( + ctx, + addrs[0], + app.StakingKeeper.TokensFromConsensusPower(ctx, 1), + stakingtypes.Unbonded, + secondValidator, + true, + ) + require.NoError(t, err) + + proposal := newVotingProposalWithVote(t, ctx, app, addrs[0], ctx.BlockTime().Add(time.Hour)) + store := ctx.KVStore(app.GetKey(govtypes.StoreKey)) + snapshotKey := govtypes.VoteDelegationsKey(proposal.ProposalId, addrs[0]) + originalSnapshot := decodeVoteDelegationSnapshot(t, store.Get(snapshotKey)) + + updatedShares := make(map[string]sdk.Dec, 2) + for _, validator := range valAddrs[:2] { + delegation, found := app.StakingKeeper.GetDelegation(ctx, addrs[0], validator) + require.True(t, found) + delegation.Shares = delegation.Shares.Sub(sdk.OneDec()) + updatedShares[validator.String()] = delegation.Shares + app.StakingKeeper.SetDelegation(ctx, delegation) + app.GovKeeper.StakingHooks().AfterDelegationModified( + stakingtypes.WithSlashDelegationModification(ctx), + addrs[0], + validator, + ) + } + + require.True(t, app.GovKeeper.HasPendingVoteDelegationUpdates(ctx)) + effectiveSnapshots := app.GovKeeper.GetVoteDelegationSnapshots(ctx, proposal) + genesisSnapshots := gov.ExportGenesis(ctx, app.GovKeeper).VoteDelegationSnapshots + for _, validator := range valAddrs[:2] { + requireVoteDelegationShares(t, effectiveSnapshots, validator, updatedShares[validator.String()]) + requireVoteDelegationShares(t, genesisSnapshots, validator, updatedShares[validator.String()]) + } + + complete, processed, _, _, _ := app.GovKeeper.TallyIncremental(ctx, proposal, 1) + require.False(t, complete) + require.Equal(t, 1, processed) + require.True(t, app.GovKeeper.HasPendingVoteDelegationUpdates(ctx)) + partiallyUpdatedSnapshot := decodeVoteDelegationSnapshot(t, store.Get(snapshotKey)) + requireVoteDelegationShares( + t, + []govtypes.VoteDelegationSnapshot{partiallyUpdatedSnapshot}, + valAddrs[0], + updatedShares[valAddrs[0].String()], + ) + requireVoteDelegationShares( + t, + []govtypes.VoteDelegationSnapshot{partiallyUpdatedSnapshot}, + valAddrs[1], + voteDelegationShares(t, originalSnapshot, valAddrs[1]), + ) + + complete, processed, _, _, _ = app.GovKeeper.TallyIncremental(ctx, proposal, 1) + require.False(t, complete) + require.Equal(t, 1, processed) + require.False(t, app.GovKeeper.HasPendingVoteDelegationUpdates(ctx)) + require.True(t, app.GovKeeper.IsTallying(ctx, proposal.ProposalId)) + canonicalSnapshot := decodeVoteDelegationSnapshot(t, store.Get(snapshotKey)) + for _, validator := range valAddrs[:2] { + requireVoteDelegationShares( + t, + []govtypes.VoteDelegationSnapshot{canonicalSnapshot}, + validator, + updatedShares[validator.String()], + ) + } + + complete, processed, _, _, _ = app.GovKeeper.TallyIncremental(ctx, proposal, 1) + require.True(t, complete) + require.Equal(t, 1, processed) + archivedSnapshot := decodeVoteDelegationSnapshot( + t, + store.Get(govtypes.TallyVoteDelegationsKey(proposal.ProposalId, false, addrs[0])), + ) + for _, validator := range valAddrs[:2] { + requireVoteDelegationShares( + t, + []govtypes.VoteDelegationSnapshot{archivedSnapshot}, + validator, + updatedShares[validator.String()], + ) + } + require.False(t, store.Has(govtypes.VoteDelegationSnapshotRevisionKey(proposal.ProposalId, addrs[0]))) +} + +func TestSlashRedelegationDefersVoteSnapshotRefresh(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{Time: time.Unix(100, 0)}) + addrs, valAddrs := createValidators(t, ctx, app, []int64{5, 5, 5}) + proposal := newVotingProposalWithVote(t, ctx, app, addrs[0], ctx.BlockTime().Add(time.Hour)) + + store := ctx.KVStore(app.GetKey(govtypes.StoreKey)) + snapshotKey := govtypes.VoteDelegationsKey(proposal.ProposalId, addrs[0]) + snapshotBefore := append([]byte(nil), store.Get(snapshotKey)...) + delegationBefore, found := app.StakingKeeper.GetDelegation(ctx, addrs[0], valAddrs[0]) + require.True(t, found) + sourceValidator, found := app.StakingKeeper.GetValidator(ctx, valAddrs[1]) + require.True(t, found) + redelegation := stakingtypes.NewRedelegation( + addrs[0], + valAddrs[1], + valAddrs[0], + ctx.BlockHeight(), + ctx.BlockTime().Add(time.Hour), + app.StakingKeeper.TokensFromConsensusPower(ctx, 5), + delegationBefore.Shares, + ) + + app.StakingKeeper.SlashRedelegation(ctx, sourceValidator, redelegation, ctx.BlockHeight(), sdk.NewDecWithPrec(5, 1)) + delegationAfter, found := app.StakingKeeper.GetDelegation(ctx, addrs[0], valAddrs[0]) + require.True(t, found) + require.True(t, delegationAfter.Shares.LT(delegationBefore.Shares)) + require.Equal(t, snapshotBefore, store.Get(snapshotKey)) + require.True(t, app.GovKeeper.HasPendingVoteDelegationUpdates(ctx)) + + complete, processed := app.GovKeeper.ProcessVoteDelegationUpdates(ctx, 1) + require.True(t, complete) + require.Equal(t, 1, processed) + requireVoteDelegationShares( + t, + app.GovKeeper.GetVoteDelegationSnapshots(ctx, proposal), + valAddrs[0], + delegationAfter.Shares, + ) +} + +func TestSlashDelegationRemovalFoldsIntoCanonicalSnapshot(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{Time: time.Unix(100, 0)}) + addrs, valAddrs := createValidators(t, ctx, app, []int64{5, 5, 5}) + proposal := newVotingProposalWithVote(t, ctx, app, addrs[0], ctx.BlockTime().Add(time.Hour)) + + app.GovKeeper.StakingHooks().BeforeDelegationRemoved( + stakingtypes.WithSlashDelegationModification(ctx), + addrs[0], + valAddrs[0], + ) + complete, processed := app.GovKeeper.ProcessVoteDelegationUpdates(ctx, 1) + require.True(t, complete) + require.Equal(t, 1, processed) + + store := ctx.KVStore(app.GetKey(govtypes.StoreKey)) + snapshot := decodeVoteDelegationSnapshot( + t, + store.Get(govtypes.VoteDelegationsKey(proposal.ProposalId, addrs[0])), + ) + requireNoVoteDelegation(t, snapshot, valAddrs[0]) +} + +func TestSynchronousDelegationRefreshSupersedesDeferredSlashUpdate(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{Time: time.Unix(100, 0)}) + addrs, valAddrs := createValidators(t, ctx, app, []int64{5, 5, 5}) + proposal := newVotingProposalWithVote(t, ctx, app, addrs[0], ctx.BlockTime().Add(time.Hour)) + + delegation, found := app.StakingKeeper.GetDelegation(ctx, addrs[0], valAddrs[0]) + require.True(t, found) + delegation.Shares = delegation.Shares.Sub(sdk.OneDec()) + app.StakingKeeper.SetDelegation(ctx, delegation) + app.GovKeeper.StakingHooks().AfterDelegationModified( + stakingtypes.WithSlashDelegationModification(ctx), + addrs[0], + valAddrs[0], + ) + + latestShares := delegation.Shares.Sub(sdk.OneDec()) + delegation.Shares = latestShares + app.StakingKeeper.SetDelegation(ctx, delegation) + app.GovKeeper.StakingHooks().AfterDelegationModified(ctx, addrs[0], valAddrs[0]) + + complete, processed := app.GovKeeper.ProcessVoteDelegationUpdates(ctx, 1) + require.True(t, complete) + require.Equal(t, 1, processed) + requireVoteDelegationShares(t, app.GovKeeper.GetVoteDelegationSnapshots(ctx, proposal), valAddrs[0], latestShares) + store := ctx.KVStore(app.GetKey(govtypes.StoreKey)) + snapshot := decodeVoteDelegationSnapshot( + t, + store.Get(govtypes.VoteDelegationsKey(proposal.ProposalId, addrs[0])), + ) + requireVoteDelegationShares(t, []govtypes.VoteDelegationSnapshot{snapshot}, valAddrs[0], latestShares) +} + +func TestSlashDelegationUpdateHonorsVotingEndTime(t *testing.T) { + for _, tc := range []struct { + name string + offset time.Duration + applyUpdate bool + }{ + {name: "at voting end", offset: 0, applyUpdate: true}, + {name: "after voting end", offset: time.Second, applyUpdate: false}, + } { + t.Run(tc.name, func(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{Time: time.Unix(100, 0)}) + addrs, valAddrs := createValidators(t, ctx, app, []int64{5, 5, 5}) + votingEnd := ctx.BlockTime().Add(time.Minute) + proposal := newVotingProposalWithVote(t, ctx, app, addrs[0], votingEnd) + + delegation, found := app.StakingKeeper.GetDelegation(ctx, addrs[0], valAddrs[0]) + require.True(t, found) + originalShares := delegation.Shares + updatedShares := originalShares.Sub(sdk.OneDec()) + delegation.Shares = updatedShares + app.StakingKeeper.SetDelegation(ctx, delegation) + slashCtx := stakingtypes.WithSlashDelegationModification(ctx.WithBlockTime(votingEnd.Add(tc.offset))) + app.GovKeeper.StakingHooks().AfterDelegationModified(slashCtx, addrs[0], valAddrs[0]) + + complete, processed := app.GovKeeper.ProcessVoteDelegationUpdates(ctx, 1) + require.True(t, complete) + require.Equal(t, 1, processed) + expectedShares := originalShares + if tc.applyUpdate { + expectedShares = updatedShares + } + requireVoteDelegationShares( + t, + app.GovKeeper.GetVoteDelegationSnapshots(ctx, proposal), + valAddrs[0], + expectedShares, + ) + }) + } +} + +func newVotingProposalWithVote( + t *testing.T, + ctx sdk.Context, + app *seiapp.App, + voter sdk.AccAddress, + votingEnd time.Time, +) govtypes.Proposal { + t.Helper() + proposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + proposal.Status = govtypes.StatusVotingPeriod + proposal.VotingEndTime = votingEnd + app.GovKeeper.SetProposal(ctx, proposal) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + voter, + govtypes.NewNonSplitVoteOption(govtypes.OptionYes), + )) + return proposal +} + +func decodeVoteDelegationSnapshot(t *testing.T, bz []byte) govtypes.VoteDelegationSnapshot { + t.Helper() + var snapshot govtypes.VoteDelegationSnapshot + seiapp.MakeEncodingConfig().Marshaler.MustUnmarshal(bz, &snapshot) + return snapshot +} + +func voteDelegationShares(t *testing.T, snapshot govtypes.VoteDelegationSnapshot, validator sdk.ValAddress) sdk.Dec { + t.Helper() + for _, delegation := range snapshot.Delegations { + if delegation.Validator == validator.String() { + return delegation.Shares + } + } + require.FailNow(t, "validator delegation not found", validator.String()) + return sdk.ZeroDec() +} + +func requireNoVoteDelegation(t *testing.T, snapshot govtypes.VoteDelegationSnapshot, validator sdk.ValAddress) { + t.Helper() + for _, delegation := range snapshot.Delegations { + require.NotEqual(t, validator.String(), delegation.Validator) + } +} + +func requireVoteDelegationShares( + t *testing.T, + snapshots []govtypes.VoteDelegationSnapshot, + validator sdk.ValAddress, + expected sdk.Dec, +) { + t.Helper() + require.Len(t, snapshots, 1) + for _, delegation := range snapshots[0].Delegations { + if delegation.Validator == validator.String() { + require.True(t, delegation.Shares.Equal(expected), "%s != %s", delegation.Shares, expected) + return + } + } + require.Fail(t, "validator delegation not found", validator.String()) +} diff --git a/sei-cosmos/x/gov/keeper/deposit.go b/sei-cosmos/x/gov/keeper/deposit.go index 4705893148..04d40f379a 100644 --- a/sei-cosmos/x/gov/keeper/deposit.go +++ b/sei-cosmos/x/gov/keeper/deposit.go @@ -116,6 +116,11 @@ func (keeper Keeper) AddDeposit(ctx sdk.Context, proposalID uint64, depositorAdd if (proposal.Status != types.StatusDepositPeriod) && (proposal.Status != types.StatusVotingPeriod) { return false, sdkerrors.Wrapf(types.ErrInactiveProposal, "%d", proposalID) } + if keeper.IncrementalTallyEnabled(ctx) && proposal.Status == types.StatusVotingPeriod { + if proposal.VotingEndTime.Before(ctx.BlockTime()) || keeper.voteDelegationSnapshotFrozen(ctx, proposal) { + return false, sdkerrors.Wrapf(types.ErrInactiveProposal, "%d", proposalID) + } + } // update the governance module's account coins pool err := keeper.bankKeeper.SendCoinsFromAccountToModule(ctx, depositorAddr, types.ModuleName, depositAmount) diff --git a/sei-cosmos/x/gov/keeper/deposit_test.go b/sei-cosmos/x/gov/keeper/deposit_test.go index dc48b344eb..b146a4112a 100644 --- a/sei-cosmos/x/gov/keeper/deposit_test.go +++ b/sei-cosmos/x/gov/keeper/deposit_test.go @@ -135,6 +135,48 @@ func TestDeposits(t *testing.T) { } } +func TestAddDepositRejectsBlocksAfterVotingEnd(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}).WithBlockTime(time.Unix(100, 0)) + depositor := seiapp.AddTestAddrsIncremental(app, ctx, 1, sdk.NewInt(100))[0] + depositAmount := sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1)) + + proposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + proposal.Status = types.StatusVotingPeriod + proposal.VotingEndTime = ctx.BlockTime().Add(time.Second) + app.GovKeeper.SetProposal(ctx, proposal) + app.GovKeeper.InsertActiveProposalQueue(ctx, proposal.ProposalId, proposal.VotingEndTime) + + atVotingEnd := ctx.WithBlockTime(proposal.VotingEndTime) + _, err = app.GovKeeper.AddDeposit(atVotingEnd, proposal.ProposalId, depositor, depositAmount) + require.NoError(t, err) + + proposalAtVotingEnd, found := app.GovKeeper.GetProposal(ctx, proposal.ProposalId) + require.True(t, found) + depositorBalanceAtVotingEnd := app.BankKeeper.GetAllBalances(ctx, depositor) + moduleBalanceAtVotingEnd := app.BankKeeper.GetAllBalances(ctx, app.AccountKeeper.GetModuleAddress(types.ModuleName)) + app.GovKeeper.CaptureExactTallyBoundary(atVotingEnd) + _, err = app.GovKeeper.AddDeposit(atVotingEnd, proposal.ProposalId, depositor, depositAmount) + require.ErrorIs(t, err, types.ErrInactiveProposal) + + afterVotingEnd := ctx.WithBlockTime(proposal.VotingEndTime.Add(time.Second)) + _, err = app.GovKeeper.AddDeposit(afterVotingEnd, proposal.ProposalId, depositor, depositAmount) + require.ErrorIs(t, err, types.ErrInactiveProposal) + + proposalAfterVotingEnd, found := app.GovKeeper.GetProposal(ctx, proposal.ProposalId) + require.True(t, found) + require.Equal(t, proposalAtVotingEnd.TotalDeposit, proposalAfterVotingEnd.TotalDeposit) + require.Equal(t, depositorBalanceAtVotingEnd, app.BankKeeper.GetAllBalances(ctx, depositor)) + require.Equal(t, moduleBalanceAtVotingEnd, app.BankKeeper.GetAllBalances(ctx, app.AccountKeeper.GetModuleAddress(types.ModuleName))) + + store := afterVotingEnd.KVStore(app.GetKey(types.StoreKey)) + store.Delete(types.IncrementalTallyEnabledKey) + legacyCtx := afterVotingEnd.WithIsTracing(true).WithClosestUpgradeName("v6.7") + _, err = app.GovKeeper.AddDeposit(legacyCtx, proposal.ProposalId, depositor, depositAmount) + require.NoError(t, err) +} + func TestRefundDepositsLeavesInvalidRecipientPending(t *testing.T) { app := seiapp.Setup(t, false, false, false) ctx := app.BaseApp.NewContext(false, tmproto.Header{}) diff --git a/sei-cosmos/x/gov/keeper/electorate.go b/sei-cosmos/x/gov/keeper/electorate.go new file mode 100644 index 0000000000..75898c9d55 --- /dev/null +++ b/sei-cosmos/x/gov/keeper/electorate.go @@ -0,0 +1,371 @@ +package keeper + +import ( + "encoding/json" + "fmt" + "time" + + 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" +) + +const ( + gapTallyBoundary byte = 'g' + exactTallyBoundary byte = 'e' + proposalTallyBoundary byte = 'p' +) + +type tallyElectorate struct { + TotalBondedTokens sdk.Int `json:"total_bonded_tokens"` + TallyParams types.TallyParams `json:"tally_params"` + Validators []tallyValidator `json:"validators"` +} + +type tallyBoundary struct { + LowerTime time.Time `json:"lower_time"` + UpperTime time.Time `json:"upper_time"` + UpdateSequence uint64 `json:"update_sequence"` + Electorate tallyElectorate `json:"electorate"` +} + +// CaptureGapTallyBoundary freezes one electorate for proposal deadlines between consecutive block times. +func (keeper Keeper) CaptureGapTallyBoundary(ctx sdk.Context) { + if !keeper.IncrementalTallyEnabled(ctx) { + return + } + + store := ctx.KVStore(keeper.storeKey) + previousValue := store.Get(types.DeadlineBoundaryBlockTimeKey) + if previousValue == nil { + return + } + previous := parseBoundaryTime(previousValue) + current := ctx.BlockTime() + if !previous.Before(current) { + return + } + + if keeper.hasProposalDeadlineBetween(ctx, previous, current) { + boundaryID := gapTallyBoundaryID(current) + keeper.setTallyBoundary(ctx, boundaryID, tallyBoundary{ + LowerTime: previous, + UpperTime: current, + UpdateSequence: keeper.voteDelegationUpdateSequence(ctx), + Electorate: keeper.snapshotTallyElectorate(ctx), + }) + store.Set(types.GapTallyBoundaryKey(current), boundaryID) + } + store.Set(types.DeadlineBoundaryBlockTimeKey, sdk.FormatTimeBytes(current)) +} + +// CaptureExactTallyBoundary freezes one electorate for proposal deadlines equal to the current block time. +func (keeper Keeper) CaptureExactTallyBoundary(ctx sdk.Context) { + if !keeper.IncrementalTallyEnabled(ctx) || !keeper.hasProposalDeadlineAt(ctx, ctx.BlockTime()) { + return + } + + store := ctx.KVStore(keeper.storeKey) + indexKey := types.ExactTallyBoundaryKey(ctx.BlockTime()) + if store.Has(indexKey) { + return + } + boundaryID := exactTallyBoundaryID(ctx.BlockTime()) + keeper.setTallyBoundary(ctx, boundaryID, tallyBoundary{ + LowerTime: ctx.BlockTime(), + UpperTime: ctx.BlockTime(), + UpdateSequence: keeper.voteDelegationUpdateSequence(ctx), + Electorate: keeper.snapshotTallyElectorate(ctx), + }) + store.Set(indexKey, boundaryID) +} + +// InitializeDeadlineBoundaryClock records the block time preceding future deadline captures. +func (keeper Keeper) InitializeDeadlineBoundaryClock(ctx sdk.Context) { + if !keeper.IncrementalTallyEnabled(ctx) { + return + } + ctx.KVStore(keeper.storeKey).Set(types.DeadlineBoundaryBlockTimeKey, sdk.FormatTimeBytes(ctx.BlockTime())) +} + +func (keeper Keeper) snapshotTallyElectorate(ctx sdk.Context) tallyElectorate { + electorate := tallyElectorate{ + TotalBondedTokens: keeper.sk.TotalBondedTokens(ctx), + TallyParams: keeper.GetTallyParams(ctx), + Validators: []tallyValidator{}, + } + keeper.sk.IterateBondedValidatorsByPower(ctx, func(_ int64, validator stakingtypes.ValidatorI) bool { + electorate.Validators = append(electorate.Validators, tallyValidator{ + Address: validator.GetOperator().String(), + BondedTokens: validator.GetBondedTokens(), + DelegatorShares: validator.GetDelegatorShares(), + ObservedDelegatorShares: sdk.ZeroDec(), + DelegatorResults: newTallyOptionResults(), + }) + return false + }) + return electorate +} + +func (keeper Keeper) selectTallyBoundary(ctx sdk.Context, proposal types.Proposal) (tallyBoundary, []byte) { + if boundary, boundaryID, found := keeper.getSelectedTallyBoundary(ctx, proposal.ProposalId); found { + return boundary, boundaryID + } + + if !keeper.usesLegacyTallySemantics(ctx, proposal) { + if boundary, boundaryID, found := keeper.getDeadlineTallyBoundary(ctx, proposal.VotingEndTime); found { + keeper.setProposalTallyBoundary(ctx, proposal.ProposalId, boundaryID) + return boundary, boundaryID + } + } + + boundaryID := proposalSpecificTallyBoundaryID(proposal.ProposalId) + boundary := tallyBoundary{ + LowerTime: ctx.BlockTime(), + UpperTime: ctx.BlockTime(), + UpdateSequence: keeper.voteDelegationUpdateSequence(ctx), + Electorate: keeper.snapshotTallyElectorate(ctx), + } + keeper.setTallyBoundary(ctx, boundaryID, boundary) + keeper.setProposalTallyBoundary(ctx, proposal.ProposalId, boundaryID) + return boundary, boundaryID +} + +// ExportTallyElectorate returns the frozen electorate needed to restart a proposal tally. +func (keeper Keeper) ExportTallyElectorate( + ctx sdk.Context, + proposal types.Proposal, +) (types.TallyElectorate, bool) { + if progress, found := keeper.getTallyProgress(ctx, proposal.ProposalId); found { + return tallyElectorateToGenesis(proposal.ProposalId, tallyElectorate{ + TotalBondedTokens: progress.TotalBondedTokens, + TallyParams: progress.TallyParams, + Validators: progress.Validators, + }), true + } + if boundary, _, found := keeper.getSelectedTallyBoundary(ctx, proposal.ProposalId); found { + return tallyElectorateToGenesis(proposal.ProposalId, boundary.Electorate), true + } + if !keeper.usesLegacyTallySemantics(ctx, proposal) { + if boundary, _, found := keeper.getDeadlineTallyBoundary(ctx, proposal.VotingEndTime); found { + return tallyElectorateToGenesis(proposal.ProposalId, boundary.Electorate), true + } + } + if proposal.Status == types.StatusVotingPeriod && !proposal.VotingEndTime.After(ctx.BlockTime()) { + return tallyElectorateToGenesis(proposal.ProposalId, keeper.snapshotTallyElectorate(ctx)), true + } + return types.TallyElectorate{}, false +} + +// SetTallyElectorate stores an imported frozen electorate for a proposal tally. +func (keeper Keeper) SetTallyElectorate(ctx sdk.Context, electorate types.TallyElectorate) { + boundaryID := proposalSpecificTallyBoundaryID(electorate.ProposalId) + keeper.setTallyBoundary(ctx, boundaryID, tallyBoundary{ + LowerTime: ctx.BlockTime(), + UpperTime: ctx.BlockTime(), + UpdateSequence: keeper.voteDelegationUpdateSequence(ctx), + Electorate: tallyElectorateFromGenesis(electorate), + }) + keeper.setProposalTallyBoundary(ctx, electorate.ProposalId, boundaryID) +} + +func (keeper Keeper) getSelectedTallyBoundary( + ctx sdk.Context, + proposalID uint64, +) (tallyBoundary, []byte, bool) { + store := ctx.KVStore(keeper.storeKey) + boundaryID := store.Get(types.ProposalTallyBoundaryKey(proposalID)) + if boundaryID == nil { + return tallyBoundary{}, nil, false + } + boundary, found := keeper.getTallyBoundary(ctx, boundaryID) + if !found { + panic(fmt.Sprintf("missing tally boundary for proposal %d", proposalID)) + } + return boundary, boundaryID, true +} + +func (keeper Keeper) getDeadlineTallyBoundary( + ctx sdk.Context, + endTime time.Time, +) (tallyBoundary, []byte, bool) { + store := ctx.KVStore(keeper.storeKey) + if boundaryID := store.Get(types.ExactTallyBoundaryKey(endTime)); boundaryID != nil { + boundary, found := keeper.getTallyBoundary(ctx, boundaryID) + if !found { + panic(fmt.Sprintf("missing exact tally boundary at %s", endTime)) + } + return boundary, boundaryID, true + } + + start := sdk.PrefixEndBytes(types.GapTallyBoundaryKey(endTime)) + iterator := store.Iterator(start, sdk.PrefixEndBytes(types.GapTallyBoundaryKeyPrefix)) + defer func() { _ = iterator.Close() }() + if !iterator.Valid() { + return tallyBoundary{}, nil, false + } + boundaryID := append([]byte(nil), iterator.Value()...) + boundary, found := keeper.getTallyBoundary(ctx, boundaryID) + if !found { + panic(fmt.Sprintf("missing gap tally boundary for deadline %s", endTime)) + } + if !boundary.LowerTime.Before(endTime) || !endTime.Before(boundary.UpperTime) { + return tallyBoundary{}, nil, false + } + return boundary, boundaryID, true +} + +func (keeper Keeper) proposalTallyBoundarySequence(ctx sdk.Context, proposal types.Proposal) (uint64, bool) { + if boundary, _, found := keeper.getSelectedTallyBoundary(ctx, proposal.ProposalId); found { + return boundary.UpdateSequence, true + } + if keeper.usesLegacyTallySemantics(ctx, proposal) { + return 0, false + } + boundary, _, found := keeper.getDeadlineTallyBoundary(ctx, proposal.VotingEndTime) + if !found { + return 0, false + } + return boundary.UpdateSequence, true +} + +func (keeper Keeper) setProposalTallyBoundary(ctx sdk.Context, proposalID uint64, boundaryID []byte) { + ctx.KVStore(keeper.storeKey).Set(types.ProposalTallyBoundaryKey(proposalID), boundaryID) +} + +func (keeper Keeper) setTallyBoundary(ctx sdk.Context, boundaryID []byte, boundary tallyBoundary) { + bz, err := json.Marshal(boundary) + if err != nil { + panic(fmt.Errorf("marshal tally boundary: %w", err)) + } + ctx.KVStore(keeper.storeKey).Set(types.TallyBoundaryMetaKey(boundaryID), bz) +} + +func (keeper Keeper) getTallyBoundary(ctx sdk.Context, boundaryID []byte) (tallyBoundary, bool) { + bz := ctx.KVStore(keeper.storeKey).Get(types.TallyBoundaryMetaKey(boundaryID)) + if bz == nil { + return tallyBoundary{}, false + } + var boundary tallyBoundary + if err := json.Unmarshal(bz, &boundary); err != nil { + panic(fmt.Errorf("unmarshal tally boundary: %w", err)) + } + return boundary, true +} + +func (keeper Keeper) addProposalDeadline(ctx sdk.Context, proposalID uint64, endTime time.Time) { + if !keeper.IncrementalTallyEnabled(ctx) || keeper.isLegacyProposal(ctx, proposalID) { + return + } + ctx.KVStore(keeper.storeKey).Set(types.ProposalDeadlineKey(proposalID, endTime), []byte{1}) +} + +func (keeper Keeper) addModernProposalDeadline(ctx sdk.Context, proposalID uint64, endTime time.Time) { + if !keeper.IncrementalTallyEnabled(ctx) { + return + } + ctx.KVStore(keeper.storeKey).Set(types.ProposalDeadlineKey(proposalID, endTime), []byte{1}) +} + +func (keeper Keeper) removeProposalDeadline(ctx sdk.Context, proposalID uint64, endTime time.Time) { + if !keeper.IncrementalTallyEnabled(ctx) { + return + } + store := ctx.KVStore(keeper.storeKey) + boundary, boundaryID, found := keeper.getSelectedTallyBoundary(ctx, proposalID) + if !found { + boundary, boundaryID, found = keeper.getDeadlineTallyBoundary(ctx, endTime) + } + store.Delete(types.ProposalDeadlineKey(proposalID, endTime)) + store.Delete(types.ProposalTallyBoundaryKey(proposalID)) + if !found { + return + } + + switch boundaryID[0] { + case proposalTallyBoundary: + store.Delete(types.TallyBoundaryMetaKey(boundaryID)) + case exactTallyBoundary: + if !keeper.hasProposalDeadlineAt(ctx, boundary.UpperTime) { + store.Delete(types.ExactTallyBoundaryKey(boundary.UpperTime)) + store.Delete(types.TallyBoundaryMetaKey(boundaryID)) + } + case gapTallyBoundary: + if !keeper.hasProposalDeadlineBetween(ctx, boundary.LowerTime, boundary.UpperTime) { + store.Delete(types.GapTallyBoundaryKey(boundary.UpperTime)) + store.Delete(types.TallyBoundaryMetaKey(boundaryID)) + } + default: + panic(fmt.Sprintf("unknown tally boundary %q", boundaryID)) + } +} + +func (keeper Keeper) hasProposalDeadlineAt(ctx sdk.Context, endTime time.Time) bool { + prefix := types.ProposalDeadlineByTimeKey(endTime) + iterator := ctx.KVStore(keeper.storeKey).Iterator(prefix, sdk.PrefixEndBytes(prefix)) + defer func() { _ = iterator.Close() }() + return iterator.Valid() +} + +func (keeper Keeper) hasProposalDeadlineBetween(ctx sdk.Context, lowerTime, upperTime time.Time) bool { + start := sdk.PrefixEndBytes(types.ProposalDeadlineByTimeKey(lowerTime)) + end := types.ProposalDeadlineByTimeKey(upperTime) + iterator := ctx.KVStore(keeper.storeKey).Iterator(start, end) + defer func() { _ = iterator.Close() }() + return iterator.Valid() +} + +func gapTallyBoundaryID(upperTime time.Time) []byte { + return append([]byte{gapTallyBoundary}, sdk.FormatTimeBytes(upperTime)...) +} + +func exactTallyBoundaryID(endTime time.Time) []byte { + return append([]byte{exactTallyBoundary}, sdk.FormatTimeBytes(endTime)...) +} + +func proposalSpecificTallyBoundaryID(proposalID uint64) []byte { + return append([]byte{proposalTallyBoundary}, types.GetProposalIDBytes(proposalID)...) +} + +func parseBoundaryTime(value []byte) time.Time { + blockTime, err := sdk.ParseTimeBytes(value) + if err != nil { + panic(fmt.Errorf("parse tally boundary block time: %w", err)) + } + return blockTime +} + +func tallyElectorateToGenesis(proposalID uint64, electorate tallyElectorate) types.TallyElectorate { + validators := make([]types.TallyValidator, 0, len(electorate.Validators)) + for _, validator := range electorate.Validators { + validators = append(validators, types.TallyValidator{ + Address: validator.Address, + BondedTokens: validator.BondedTokens, + DelegatorShares: validator.DelegatorShares, + }) + } + return types.TallyElectorate{ + ProposalId: proposalID, + TotalBondedTokens: electorate.TotalBondedTokens, + TallyParams: electorate.TallyParams, + TallyValidators: validators, + } +} + +func tallyElectorateFromGenesis(electorate types.TallyElectorate) tallyElectorate { + validators := make([]tallyValidator, 0, len(electorate.TallyValidators)) + for _, validator := range electorate.TallyValidators { + validators = append(validators, tallyValidator{ + Address: validator.Address, + BondedTokens: validator.BondedTokens, + DelegatorShares: validator.DelegatorShares, + ObservedDelegatorShares: sdk.ZeroDec(), + DelegatorResults: newTallyOptionResults(), + }) + } + return tallyElectorate{ + TotalBondedTokens: electorate.TotalBondedTokens, + TallyParams: electorate.TallyParams, + Validators: validators, + } +} diff --git a/sei-cosmos/x/gov/keeper/electorate_test.go b/sei-cosmos/x/gov/keeper/electorate_test.go new file mode 100644 index 0000000000..c149fbfb66 --- /dev/null +++ b/sei-cosmos/x/gov/keeper/electorate_test.go @@ -0,0 +1,187 @@ +package keeper_test + +import ( + "testing" + "time" + + tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" + "github.com/stretchr/testify/require" + + seiapp "github.com/sei-protocol/sei-chain/app" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + govtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" + stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" +) + +func TestGapTallyBoundaryFreezesElectorateBeforeNextBlockMutations(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + initialTime := time.Unix(100, 0) + ctx := app.BaseApp.NewContext(false, tmproto.Header{Time: initialTime}) + addrs, valAddrs := createValidators(t, ctx, app, []int64{5, 5, 5}) + proposal := createVotingProposalEndingAt(t, ctx, app, initialTime.Add(5*time.Second)) + delegateAndVoteYes(t, ctx, app, proposal.ProposalId, addrs[3], valAddrs[0], 2) + _, _, expected := app.GovKeeper.Tally(ctx, proposal) + + app.GovKeeper.InitializeDeadlineBoundaryClock(ctx) + nextCtx := ctx.WithBlockTime(initialTime.Add(10 * time.Second)) + app.GovKeeper.CaptureGapTallyBoundary(nextCtx) + delegateToValidator(t, nextCtx, app, addrs[3], valAddrs[0], 20) + + complete, processed, _, _, result := app.GovKeeper.TallyIncremental(nextCtx, proposal, 1) + require.True(t, complete) + require.Equal(t, 1, processed) + require.True(t, expected.Equals(result)) +} + +func TestExactTallyBoundaryFreezesBeforeLaterEndBlockMutations(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + blockTime := time.Unix(100, 0) + ctx := app.BaseApp.NewContext(false, tmproto.Header{Time: blockTime}) + addrs, valAddrs := createValidators(t, ctx, app, []int64{5, 5, 5}) + + first := createVotingProposalEndingAt(t, ctx, app, blockTime) + second := createVotingProposalEndingAt(t, ctx, app, blockTime) + delegateAndVoteYes(t, ctx, app, first.ProposalId, addrs[3], valAddrs[0], 2) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + second.ProposalId, + addrs[3], + govtypes.NewNonSplitVoteOption(govtypes.OptionYes), + )) + _, _, expected := app.GovKeeper.Tally(ctx, second) + + app.GovKeeper.CaptureExactTallyBoundary(ctx) + require.Equal(t, 1, countStorePrefix(ctx, app, govtypes.TallyBoundaryMetaKeyPrefix)) + delegateToValidator(t, ctx, app, addrs[3], valAddrs[0], 20) + + complete, processed, _, _, result := app.GovKeeper.TallyIncremental(ctx, first, 1) + require.True(t, complete) + require.Equal(t, 1, processed) + require.True(t, expected.Equals(result)) + app.GovKeeper.RemoveFromActiveProposalQueue(ctx, first.ProposalId, first.VotingEndTime) + require.Equal(t, 1, countStorePrefix(ctx, app, govtypes.TallyBoundaryMetaKeyPrefix)) + + complete, processed, _, _, result = app.GovKeeper.TallyIncremental(ctx, second, 1) + require.True(t, complete) + require.Equal(t, 1, processed) + require.True(t, expected.Equals(result)) + app.GovKeeper.RemoveFromActiveProposalQueue(ctx, second.ProposalId, second.VotingEndTime) + require.Zero(t, countStorePrefix(ctx, app, govtypes.TallyBoundaryMetaKeyPrefix)) +} + +func TestTallyOnlyWaitsForDelegationUpdatesThroughItsBoundary(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + blockTime := time.Unix(100, 0) + ctx := app.BaseApp.NewContext(false, tmproto.Header{Time: blockTime}) + addrs, valAddrs := createValidators(t, ctx, app, []int64{5, 5, 5}) + proposal := createVotingProposalEndingAt(t, ctx, app, blockTime) + delegateAndVoteYes(t, ctx, app, proposal.ProposalId, addrs[3], valAddrs[0], 2) + + firstShares := queueDelegationShareUpdate(t, ctx, app, addrs[3], valAddrs[0], sdk.OneDec()) + app.GovKeeper.CaptureExactTallyBoundary(ctx) + queueDelegationShareUpdate(t, ctx, app, addrs[3], valAddrs[0], sdk.OneDec()) + + complete, processed, _, _, _ := app.GovKeeper.TallyIncremental(ctx, proposal, 2) + require.True(t, complete) + require.Equal(t, 2, processed) + require.True(t, app.GovKeeper.HasPendingVoteDelegationUpdates(ctx)) + archivedSnapshot := decodeVoteDelegationSnapshot( + t, + ctx.KVStore(app.GetKey(govtypes.StoreKey)).Get( + govtypes.TallyVoteDelegationsKey(proposal.ProposalId, false, addrs[3]), + ), + ) + requireVoteDelegationShares( + t, + []govtypes.VoteDelegationSnapshot{archivedSnapshot}, + valAddrs[0], + firstShares, + ) +} + +func createVotingProposalEndingAt( + t *testing.T, + ctx sdk.Context, + app *seiapp.App, + endTime time.Time, +) govtypes.Proposal { + 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) + app.GovKeeper.RemoveFromActiveProposalQueue(ctx, proposal.ProposalId, proposal.VotingEndTime) + proposal.VotingEndTime = endTime + app.GovKeeper.SetProposal(ctx, proposal) + app.GovKeeper.InsertActiveProposalQueue(ctx, proposal.ProposalId, endTime) + return proposal +} + +func delegateAndVoteYes( + t *testing.T, + ctx sdk.Context, + app *seiapp.App, + proposalID uint64, + voter sdk.AccAddress, + validator sdk.ValAddress, + power int64, +) { + delegateToValidator(t, ctx, app, voter, validator, power) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposalID, + voter, + govtypes.NewNonSplitVoteOption(govtypes.OptionYes), + )) +} + +func delegateToValidator( + t *testing.T, + ctx sdk.Context, + app *seiapp.App, + delegator sdk.AccAddress, + validatorAddress sdk.ValAddress, + power int64, +) { + validator, found := app.StakingKeeper.GetValidator(ctx, validatorAddress) + require.True(t, found) + _, err := app.StakingKeeper.Delegate( + ctx, + delegator, + app.StakingKeeper.TokensFromConsensusPower(ctx, power), + stakingtypes.Unbonded, + validator, + true, + ) + require.NoError(t, err) +} + +func queueDelegationShareUpdate( + t *testing.T, + ctx sdk.Context, + app *seiapp.App, + delegator sdk.AccAddress, + validator sdk.ValAddress, + delta sdk.Dec, +) sdk.Dec { + delegation, found := app.StakingKeeper.GetDelegation(ctx, delegator, validator) + require.True(t, found) + delegation.Shares = delegation.Shares.Sub(delta) + app.StakingKeeper.SetDelegation(ctx, delegation) + app.GovKeeper.StakingHooks().AfterDelegationModified( + stakingtypes.WithSlashDelegationModification(ctx), + delegator, + validator, + ) + return delegation.Shares +} + +func countStorePrefix(tctx sdk.Context, app *seiapp.App, prefix []byte) int { + iterator := sdk.KVStorePrefixIterator(tctx.KVStore(app.GetKey(govtypes.StoreKey)), prefix) + defer func() { _ = iterator.Close() }() + count := 0 + for ; iterator.Valid(); iterator.Next() { + count++ + } + return count +} 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/keeper.go b/sei-cosmos/x/gov/keeper/keeper.go index ab4753e4a2..cd587d2b69 100644 --- a/sei-cosmos/x/gov/keeper/keeper.go +++ b/sei-cosmos/x/gov/keeper/keeper.go @@ -95,15 +95,31 @@ func (keeper Keeper) GetGovernanceAccount(ctx sdk.Context) authtypes.ModuleAccou // InsertActiveProposalQueue inserts a ProposalID into the active proposal queue at endTime func (keeper Keeper) InsertActiveProposalQueue(ctx sdk.Context, proposalID uint64, endTime time.Time) { - store := ctx.KVStore(keeper.storeKey) - bz := types.GetProposalIDBytes(proposalID) - store.Set(types.ActiveProposalQueueKey(proposalID, endTime), bz) + keeper.insertActiveProposalQueue(ctx, proposalID, endTime) + keeper.addProposalDeadline(ctx, proposalID, endTime) +} + +// InsertActiveProposalQueueForModernTallyRound inserts a converted legacy proposal's regular voting round. +func (keeper Keeper) InsertActiveProposalQueueForModernTallyRound(ctx sdk.Context, proposalID uint64, endTime time.Time) { + if !keeper.isLegacyProposal(ctx, proposalID) { + keeper.InsertActiveProposalQueue(ctx, proposalID, endTime) + return + } + keeper.insertActiveProposalQueue(ctx, proposalID, endTime) + keeper.SetModernTallyRound(ctx, proposalID) + keeper.addModernProposalDeadline(ctx, proposalID, endTime) +} + +func (keeper Keeper) insertActiveProposalQueue(ctx sdk.Context, proposalID uint64, endTime time.Time) { + ctx.KVStore(keeper.storeKey).Set(types.ActiveProposalQueueKey(proposalID, endTime), types.GetProposalIDBytes(proposalID)) } // RemoveFromActiveProposalQueue removes a proposalID from the Active Proposal Queue func (keeper Keeper) RemoveFromActiveProposalQueue(ctx sdk.Context, proposalID uint64, endTime time.Time) { store := ctx.KVStore(keeper.storeKey) store.Delete(types.ActiveProposalQueueKey(proposalID, endTime)) + keeper.removeProposalDeadline(ctx, proposalID, endTime) + store.Delete(types.ModernTallyRoundKey(proposalID)) } // InsertInactiveProposalQueue Inserts a ProposalID into the inactive proposal queue at endTime diff --git a/sei-cosmos/x/gov/keeper/migrations.go b/sei-cosmos/x/gov/keeper/migrations.go index 6e67c4fee4..e45a8b07e7 100644 --- a/sei-cosmos/x/gov/keeper/migrations.go +++ b/sei-cosmos/x/gov/keeper/migrations.go @@ -2,8 +2,11 @@ package keeper import ( sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" ) +const voteDelegationBackfillComplete byte = 0 + // Migrator is a struct for handling in-place store migrations. type Migrator struct { keeper Keeper @@ -23,3 +26,136 @@ func (m Migrator) Migrate1to2(ctx sdk.Context) error { func (m Migrator) Migrate2to3(ctx sdk.Context) error { return nil } + +// Migrate3to4 schedules delegation-tracking backfill for existing votes. +func (m Migrator) Migrate3to4(ctx sdk.Context) error { + nextProposalID, err := m.keeper.GetProposalID(ctx) + if err != nil { + return err + } + + m.keeper.SetVoteDelegationBackfillCutoff(ctx, nextProposalID) + m.keeper.EnableIncrementalTally(ctx) + m.keeper.InitializeDeadlineBoundaryClock(ctx) + return nil +} + +// EnableIncrementalTally records that bounded governance tallying is active. +func (keeper Keeper) EnableIncrementalTally(ctx sdk.Context) { + ctx.KVStore(keeper.storeKey).Set(types.IncrementalTallyEnabledKey, []byte{1}) +} + +// SetVoteDelegationBackfillCutoff records the first proposal that does not require delegation-tracking backfill. +func (keeper Keeper) SetVoteDelegationBackfillCutoff(ctx sdk.Context, proposalID uint64) { + ctx.KVStore(keeper.storeKey).Set(types.VoteDelegationBackfillCutoffKey, types.GetProposalIDBytes(proposalID)) +} + +// GetVoteDelegationBackfillCutoff returns the first proposal that does not require delegation-tracking backfill. +func (keeper Keeper) GetVoteDelegationBackfillCutoff(ctx sdk.Context) (uint64, bool) { + cutoff := ctx.KVStore(keeper.storeKey).Get(types.VoteDelegationBackfillCutoffKey) + if cutoff == nil { + return 0, false + } + if len(cutoff) != 8 { + panic("invalid vote delegation backfill cutoff") + } + return types.GetProposalIDFromBytes(cutoff), true +} + +// BackfillVoteDelegationTracking initializes tracking for at most maxVotes of a proposal's votes. +func (keeper Keeper) BackfillVoteDelegationTracking( + ctx sdk.Context, + proposalID uint64, + maxVotes int, +) (complete bool, processed int) { + if maxVotes < 0 { + panic("maximum votes to backfill cannot be negative") + } + if !keeper.voteNeedsDelegationBackfill(ctx, proposalID) { + return true, 0 + } + + store := ctx.KVStore(keeper.storeKey) + progressKey := types.VoteDelegationBackfillProgressKey(proposalID) + cursor := store.Get(progressKey) + if cursor == nil { + cursor = types.VotesKey(proposalID) + store.Set(progressKey, cursor) + } + + votesPrefix := types.VotesKey(proposalID) + iterator := store.Iterator(cursor, sdk.PrefixEndBytes(votesPrefix)) + defer func() { _ = iterator.Close() }() + + for ; iterator.Valid() && processed < maxVotes; iterator.Next() { + _, voter := types.SplitKeyVote(iterator.Key()) + if !store.Has(types.VoteDelegationsKey(proposalID, voter)) || + !store.Has(types.VoterProposalsKey(voter, proposalID)) { + keeper.initializeVoteDelegationTracking(ctx, proposalID, voter) + } + processed++ + } + + if iterator.Valid() { + store.Set(progressKey, append([]byte(nil), iterator.Key()...)) + return false, processed + } + store.Set(progressKey, []byte{voteDelegationBackfillComplete}) + return true, processed +} + +// IsVoteDelegationBackfillInProgress reports whether a proposal's tracking backfill has started but not finished. +func (keeper Keeper) IsVoteDelegationBackfillInProgress(ctx sdk.Context, proposalID uint64) bool { + progress := ctx.KVStore(keeper.storeKey).Get(types.VoteDelegationBackfillProgressKey(proposalID)) + return progress != nil && !voteDelegationBackfillIsComplete(progress) +} + +// VoteDelegationBackfillRequired reports whether a proposal's votes require delegation-tracking backfill. +func (keeper Keeper) VoteDelegationBackfillRequired(ctx sdk.Context, proposalID uint64) bool { + return keeper.voteNeedsDelegationBackfill(ctx, proposalID) +} + +// CompleteVoteDelegationBackfill marks a legacy proposal's delegation tracking complete. +func (keeper Keeper) CompleteVoteDelegationBackfill(ctx sdk.Context, proposalID uint64) { + if !keeper.isLegacyProposal(ctx, proposalID) || keeper.IsModernTallyRound(ctx, proposalID) { + return + } + ctx.KVStore(keeper.storeKey).Set( + types.VoteDelegationBackfillProgressKey(proposalID), + []byte{voteDelegationBackfillComplete}, + ) +} + +func (keeper Keeper) voteNeedsDelegationBackfill(ctx sdk.Context, proposalID uint64) bool { + if !keeper.isLegacyProposal(ctx, proposalID) || keeper.IsModernTallyRound(ctx, proposalID) { + return false + } + progress := ctx.KVStore(keeper.storeKey).Get(types.VoteDelegationBackfillProgressKey(proposalID)) + return !voteDelegationBackfillIsComplete(progress) +} + +func (keeper Keeper) isLegacyProposal(ctx sdk.Context, proposalID uint64) bool { + cutoff, found := keeper.GetVoteDelegationBackfillCutoff(ctx) + if !found { + return false + } + return proposalID < cutoff +} + +func (keeper Keeper) usesLegacyTallySemantics(ctx sdk.Context, proposal types.Proposal) bool { + return keeper.isLegacyProposal(ctx, proposal.ProposalId) && !keeper.IsModernTallyRound(ctx, proposal.ProposalId) +} + +// SetModernTallyRound marks a legacy proposal's converted regular round for deadline-based tallying. +func (keeper Keeper) SetModernTallyRound(ctx sdk.Context, proposalID uint64) { + ctx.KVStore(keeper.storeKey).Set(types.ModernTallyRoundKey(proposalID), []byte{1}) +} + +// IsModernTallyRound reports whether a legacy proposal's current round uses deadline-based tallying. +func (keeper Keeper) IsModernTallyRound(ctx sdk.Context, proposalID uint64) bool { + return ctx.KVStore(keeper.storeKey).Has(types.ModernTallyRoundKey(proposalID)) +} + +func voteDelegationBackfillIsComplete(progress []byte) bool { + return len(progress) == 1 && progress[0] == voteDelegationBackfillComplete +} diff --git a/sei-cosmos/x/gov/keeper/migrations_test.go b/sei-cosmos/x/gov/keeper/migrations_test.go new file mode 100644 index 0000000000..2ce462b2df --- /dev/null +++ b/sei-cosmos/x/gov/keeper/migrations_test.go @@ -0,0 +1,143 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" + "github.com/stretchr/testify/require" + + seiapp "github.com/sei-protocol/sei-chain/app" + gov "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov" + govkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/keeper" + govtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" + stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" +) + +func TestMigrate3to4SchedulesBoundedVoteDelegationBackfill(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 = govtypes.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + addrs[0], + govtypes.NewNonSplitVoteOption(govtypes.OptionYes), + )) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + addrs[3], + govtypes.NewNonSplitVoteOption(govtypes.OptionNo), + )) + + store := ctx.KVStore(app.GetKey(govtypes.StoreKey)) + store.Delete(govtypes.IncrementalTallyEnabledKey) + store.Delete(govtypes.DeadlineBoundaryBlockTimeKey) + for _, voter := range []int{0, 3} { + store.Delete(govtypes.VoteDelegationsKey(proposal.ProposalId, addrs[voter])) + store.Delete(govtypes.VoterProposalsKey(addrs[voter], proposal.ProposalId)) + } + + migrator := govkeeper.NewMigrator(app.GovKeeper) + require.NoError(t, migrator.Migrate3to4(ctx)) + require.True(t, store.Has(govtypes.IncrementalTallyEnabledKey)) + require.Equal(t, sdk.FormatTimeBytes(ctx.BlockTime()), store.Get(govtypes.DeadlineBoundaryBlockTimeKey)) + cutoff, found := app.GovKeeper.GetVoteDelegationBackfillCutoff(ctx) + require.True(t, found) + require.Equal(t, uint64(2), cutoff) + require.False(t, app.GovKeeper.IsVoteDelegationBackfillInProgress(ctx, proposal.ProposalId)) + for _, voter := range []int{0, 3} { + require.False(t, store.Has(govtypes.VoteDelegationsKey(proposal.ProposalId, addrs[voter]))) + require.False(t, store.Has(govtypes.VoterProposalsKey(addrs[voter], proposal.ProposalId))) + } + + newProposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + newProposal.Status = govtypes.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, newProposal) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + newProposal.ProposalId, + addrs[1], + govtypes.NewNonSplitVoteOption(govtypes.OptionAbstain), + )) + backfillComplete, backfilled := app.GovKeeper.BackfillVoteDelegationTracking(ctx, newProposal.ProposalId, 1) + require.True(t, backfillComplete) + require.Zero(t, backfilled) + + backfillComplete, backfilled = app.GovKeeper.BackfillVoteDelegationTracking(ctx, proposal.ProposalId, 0) + require.False(t, backfillComplete) + require.Zero(t, backfilled) + require.True(t, app.GovKeeper.IsVoteDelegationBackfillInProgress(ctx, proposal.ProposalId)) + + complete, processed, _, _, _ := app.GovKeeper.TallyIncremental(ctx, proposal, 1) + require.False(t, complete) + require.Equal(t, 1, processed) + require.False(t, app.GovKeeper.IsTallying(ctx, proposal.ProposalId)) + require.True(t, app.GovKeeper.IsVoteDelegationBackfillInProgress(ctx, proposal.ProposalId)) + tracked := 0 + for _, voter := range []int{0, 3} { + if store.Has(govtypes.VoteDelegationsKey(proposal.ProposalId, addrs[voter])) { + tracked++ + require.True(t, store.Has(govtypes.VoterProposalsKey(addrs[voter], proposal.ProposalId))) + } + } + require.Equal(t, 1, tracked) + require.True(t, store.Has(govtypes.VoteDelegationsKey(proposal.ProposalId, addrs[0]))) + require.ErrorIs(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + addrs[2], + govtypes.NewNonSplitVoteOption(govtypes.OptionYes), + ), govtypes.ErrInactiveProposal) + + validator, found := app.StakingKeeper.GetValidator(ctx, valAddrs[0]) + require.True(t, found) + snapshotKey := govtypes.VoteDelegationsKey(proposal.ProposalId, addrs[0]) + snapshotBeforeDelegation := append([]byte(nil), store.Get(snapshotKey)...) + delegatedTokens := app.StakingKeeper.TokensFromConsensusPower(ctx, 20) + _, err = app.StakingKeeper.Delegate(ctx, addrs[0], delegatedTokens, stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + require.NotEqual(t, snapshotBeforeDelegation, store.Get(snapshotKey)) + + require.NotPanics(t, func() { + _, _, _ = app.GovKeeper.Tally(ctx, proposal) + }) + genesis := gov.ExportGenesis(ctx, app.GovKeeper) + require.Len(t, genesis.Votes, 3) + require.Len(t, genesis.VoteDelegationSnapshots, 3) + require.Equal(t, cutoff, genesis.VoteDelegationBackfillCutoff) + + lateDelegatedTokens := app.StakingKeeper.TokensFromConsensusPower(ctx, 10) + validator, found = app.StakingKeeper.GetValidator(ctx, valAddrs[0]) + require.True(t, found) + _, err = app.StakingKeeper.Delegate(ctx, addrs[3], lateDelegatedTokens, stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + + 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.False(t, app.GovKeeper.IsVoteDelegationBackfillInProgress(ctx, proposal.ProposalId)) + for _, voter := range []int{0, 3} { + require.True(t, store.Has(govtypes.VoteDelegationsKey(proposal.ProposalId, addrs[voter]))) + require.True(t, store.Has(govtypes.VoterProposalsKey(addrs[voter], proposal.ProposalId))) + } + + validator, found = app.StakingKeeper.GetValidator(ctx, valAddrs[0]) + require.True(t, found) + _, 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.Equal(t, app.StakingKeeper.TokensFromConsensusPower(ctx, 25).String(), tallyResult.Yes.String()) + require.Equal(t, lateDelegatedTokens.String(), tallyResult.No.String()) +} diff --git a/sei-cosmos/x/gov/keeper/staking_hooks.go b/sei-cosmos/x/gov/keeper/staking_hooks.go new file mode 100644 index 0000000000..b961dd0564 --- /dev/null +++ b/sei-cosmos/x/gov/keeper/staking_hooks.go @@ -0,0 +1,71 @@ +package keeper + +import ( + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" +) + +var _ stakingtypes.StakingHooks = StakingHooks{} + +// StakingHooks maintains governance vote delegation snapshots as staking state changes. +type StakingHooks struct { + keeper Keeper +} + +// StakingHooks returns the governance staking hooks. +func (keeper Keeper) StakingHooks() StakingHooks { + return StakingHooks{keeper: keeper} +} + +func (StakingHooks) AfterValidatorCreated(sdk.Context, sdk.ValAddress) {} + +func (StakingHooks) BeforeValidatorModified(sdk.Context, sdk.ValAddress) {} + +func (StakingHooks) AfterValidatorRemoved(sdk.Context, sdk.ConsAddress, sdk.ValAddress) {} + +func (StakingHooks) AfterValidatorBonded(sdk.Context, sdk.ConsAddress, sdk.ValAddress) {} + +func (StakingHooks) AfterValidatorBeginUnbonding(sdk.Context, sdk.ConsAddress, sdk.ValAddress) {} + +func (StakingHooks) BeforeDelegationCreated(sdk.Context, sdk.AccAddress, sdk.ValAddress) {} + +func (StakingHooks) BeforeDelegationSharesModified(sdk.Context, sdk.AccAddress, sdk.ValAddress) {} + +// BeforeDelegationRemoved removes the outgoing delegation from active vote snapshots. +func (hooks StakingHooks) BeforeDelegationRemoved( + ctx sdk.Context, + delegator sdk.AccAddress, + validator sdk.ValAddress, +) { + if !hooks.keeper.IncrementalTallyEnabled(ctx) { + return + } + if stakingtypes.IsSlashDelegationModification(ctx) { + hooks.keeper.QueueVoteDelegationUpdate(ctx, delegator, validator, sdk.ZeroDec()) + return + } + hooks.keeper.refreshVoteDelegationSnapshots(ctx, delegator, validator) +} + +// AfterDelegationModified refreshes active vote snapshots from the updated delegation state. +func (hooks StakingHooks) AfterDelegationModified( + ctx sdk.Context, + delegator sdk.AccAddress, + validator sdk.ValAddress, +) { + if !hooks.keeper.IncrementalTallyEnabled(ctx) { + return + } + if stakingtypes.IsSlashDelegationModification(ctx) { + delegation, found := hooks.keeper.sk.GetDelegation(ctx, delegator, validator) + if !found { + hooks.keeper.QueueVoteDelegationUpdate(ctx, delegator, validator, sdk.ZeroDec()) + return + } + hooks.keeper.QueueVoteDelegationUpdate(ctx, delegator, validator, delegation.Shares) + return + } + hooks.keeper.refreshVoteDelegationSnapshots(ctx, delegator, nil) +} + +func (StakingHooks) BeforeValidatorSlashed(sdk.Context, sdk.ValAddress, sdk.Dec) {} diff --git a/sei-cosmos/x/gov/keeper/tally.go b/sei-cosmos/x/gov/keeper/tally.go index e806446cab..8edf650760 100644 --- a/sei-cosmos/x/gov/keeper/tally.go +++ b/sei-cosmos/x/gov/keeper/tally.go @@ -1,125 +1,575 @@ package keeper import ( + "encoding/json" + "fmt" + + "github.com/sei-protocol/sei-chain/sei-cosmos/store/prefix" 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 + +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 iterates over the votes and updates the tally of a proposal based on the voting power of the -// voters +// 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) { - 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() + progress, found := tallyProgress{}, false + if keeper.IncrementalTallyEnabled(ctx) { + progress, found = keeper.getTallyProgress(ctx, proposal.ProposalId) + } + if !found { + progress = keeper.initializeTally(ctx, proposal) + } + + validators := progress.validatorMap() + store := ctx.KVStore(keeper.storeKey) + votes := prefix.NewStore(store, types.VotesKey(proposal.ProposalId)) + keeper.iterateVoteStore(votes, func(vote types.Vote) bool { + keeper.addVoteToTally(validators, vote, keeper.voteDelegations(ctx, proposal.ProposalId, progress.Expedited, vote)) + return false + }) + return keeper.finishTally(progress) +} +// TallyLegacy calculates a proposal's result and removes its votes using the legacy tally transition. +func (keeper Keeper) TallyLegacy(ctx sdk.Context, proposal types.Proposal) (passes bool, burnDeposits bool, tallyResults types.TallyResult) { + results := map[types.VoteOption]sdk.Dec{ + types.OptionYes: sdk.ZeroDec(), + types.OptionAbstain: sdk.ZeroDec(), + types.OptionNo: sdk.ZeroDec(), + types.OptionNoWithVeto: sdk.ZeroDec(), + } totalVotingPower := sdk.ZeroDec() - currValidators := make(map[string]types.ValidatorGovInfo) + validators := 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( + keeper.sk.IterateBondedValidatorsByPower(ctx, func(_ int64, validator stakingtypes.ValidatorI) bool { + validators[validator.GetOperator().String()] = types.NewValidatorGovInfo( validator.GetOperator(), validator.GetBondedTokens(), validator.GetDelegatorShares(), sdk.ZeroDec(), types.WeightedVoteOptions{}, ) - return false }) keeper.IterateVotes(ctx, proposal.ProposalId, func(vote types.Vote) bool { - // if validator, just record it in the map voter := sdk.MustAccAddressFromBech32(vote.Voter) - - valAddrStr := sdk.ValAddress(voter.Bytes()).String() - if val, ok := currValidators[valAddrStr]; ok { - val.Vote = vote.Options - currValidators[valAddrStr] = val + validatorAddress := sdk.ValAddress(voter.Bytes()).String() + if validator, found := validators[validatorAddress]; found { + validator.Vote = vote.Options + validators[validatorAddress] = validator } - // 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() - - 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 - - // delegation shares * bonded / total shares - votingPower := delegation.GetShares().MulInt(val.BondedTokens).Quo(val.DelegatorShares) - - for _, option := range vote.Options { - subPower := votingPower.Mul(option.Weight) - results[option.Option] = results[option.Option].Add(subPower) - } - totalVotingPower = totalVotingPower.Add(votingPower) + keeper.sk.IterateDelegations(ctx, voter, func(_ int64, delegation stakingtypes.DelegationI) bool { + validatorAddress := delegation.GetValidatorAddr().String() + validator, found := validators[validatorAddress] + if !found { + return false } + validator.DelegatorDeductions = validator.DelegatorDeductions.Add(delegation.GetShares()) + validators[validatorAddress] = validator + votingPower := delegation.GetShares().MulInt(validator.BondedTokens).Quo(validator.DelegatorShares) + for _, option := range vote.Options { + results[option.Option] = results[option.Option].Add(votingPower.Mul(option.Weight)) + } + totalVotingPower = totalVotingPower.Add(votingPower) return false }) - keeper.deleteVote(ctx, vote.ProposalId, voter) + ctx.KVStore(keeper.storeKey).Delete(types.VoteKey(vote.ProposalId, voter)) return false }) - // iterate over the validators again to tally their voting power - for _, val := range currValidators { - if len(val.Vote) == 0 { + for _, validator := range validators { + if len(validator.Vote) == 0 { continue } - sharesAfterDeductions := val.DelegatorShares.Sub(val.DelegatorDeductions) - votingPower := sharesAfterDeductions.MulInt(val.BondedTokens).Quo(val.DelegatorShares) - - for _, option := range val.Vote { - subPower := votingPower.Mul(option.Weight) - results[option.Option] = results[option.Option].Add(subPower) + sharesAfterDeductions := validator.DelegatorShares.Sub(validator.DelegatorDeductions) + votingPower := sharesAfterDeductions.MulInt(validator.BondedTokens).Quo(validator.DelegatorShares) + for _, option := range validator.Vote { + results[option.Option] = results[option.Option].Add(votingPower.Mul(option.Weight)) } totalVotingPower = totalVotingPower.Add(votingPower) } tallyParams := keeper.GetTallyParams(ctx) tallyResults = types.NewTallyResultFromMap(results) - - // 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() { 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) { + if percentVoting.LT(tallyParams.GetQuorum(proposal.IsExpedited)) { return false, true, tallyResults } - // If no one votes (everyone abstains), proposal fails if totalVotingPower.Sub(results[types.OptionAbstain]).Equal(sdk.ZeroDec()) { return false, false, tallyResults } - // If more than 1/3 of voters veto, proposal fails if results[types.OptionNoWithVeto].Quo(totalVotingPower).GT(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) { + if results[types.OptionYes].Quo(totalVotingPower.Sub(results[types.OptionAbstain])).GT(tallyParams.GetThreshold(proposal.IsExpedited)) { + return true, false, tallyResults + } + + return false, false, tallyResults +} + +// TallyIncremental processes at most maxRecords governance work records after incremental tallying is active. +func (keeper Keeper) TallyIncremental( + ctx sdk.Context, + proposal types.Proposal, + maxRecords int, +) (complete bool, processed int, passes bool, burnDeposits bool, tallyResults types.TallyResult) { + if !keeper.IncrementalTallyEnabled(ctx) { + passes, burnDeposits, tallyResults = keeper.TallyLegacy(ctx, proposal) + return true, 0, passes, burnDeposits, tallyResults + } + if maxRecords < 0 { + panic("maximum governance records to process cannot be negative") + } + progress, found := keeper.getTallyProgress(ctx, proposal.ProposalId) + boundary, _, boundaryFound := keeper.getSelectedTallyBoundary(ctx, proposal.ProposalId) + if !found && !boundaryFound && keeper.usesLegacyTallySemantics(ctx, proposal) { + backfillComplete, backfilled := keeper.BackfillVoteDelegationTracking(ctx, proposal.ProposalId, maxRecords-processed) + processed += backfilled + if !backfillComplete { + return false, processed, false, false, types.EmptyTallyResult() + } + } + if !boundaryFound { + if maxRecords == 0 { + return false, processed, false, false, types.EmptyTallyResult() + } + boundary, _ = keeper.selectTallyBoundary(ctx, proposal) + if keeper.usesLegacyTallySemantics(ctx, proposal) { + progress = initializeTallyFromElectorate(boundary.Electorate, proposal.IsExpedited) + keeper.setTallyProgress(ctx, proposal.ProposalId, progress) + return false, maxRecords, false, false, types.EmptyTallyResult() + } + } + updatesComplete, updatesProcessed := keeper.ProcessVoteDelegationUpdatesThrough( + ctx, + maxRecords-processed, + boundary.UpdateSequence, + ) + processed += updatesProcessed + if !updatesComplete { + return false, processed, false, false, types.EmptyTallyResult() + } + + if !found { + progress = initializeTallyFromElectorate(boundary.Electorate, proposal.IsExpedited) + } else if progress.Expedited != proposal.IsExpedited { + panic(fmt.Sprintf("tally round for proposal %d changed", proposal.ProposalId)) + } + if processed == maxRecords { + keeper.setTallyProgress(ctx, proposal.ProposalId, progress) + return false, processed, false, false, types.EmptyTallyResult() + } + + var tallied int + complete, tallied = keeper.processTallyVotes(ctx, proposal.ProposalId, &progress, maxRecords-processed) + processed += tallied + if !complete { + keeper.setTallyProgress(ctx, proposal.ProposalId, progress) + return false, processed, false, false, types.EmptyTallyResult() + } + if processed == 0 { + processed = 1 + } + + passes, burnDeposits, tallyResults = keeper.finishTally(progress) + keeper.deleteTallyProgress(ctx, proposal.ProposalId) + keeper.markTallyVotesForCleanup(ctx, proposal.ProposalId, progress.Expedited) + return true, processed, passes, burnDeposits, tallyResults +} + +// 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) { + if !keeper.IncrementalTallyEnabled(ctx) { + return + } + 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 + } + if keeper.voteNeedsDelegationBackfill(ctx, proposal.ProposalId) { + panic("cannot initialize tally while vote delegation backfill is in progress") + } + boundary, _ := keeper.selectTallyBoundary(ctx, proposal) + keeper.setTallyProgress(ctx, proposal.ProposalId, initializeTallyFromElectorate(boundary.Electorate, proposal.IsExpedited)) +} + +// 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 + } + + store := ctx.KVStore(keeper.storeKey) + iterator := sdk.KVStorePrefixIterator(store, types.TallyCleanupKeyPrefix) + defer func() { _ = iterator.Close() }() + + for ; iterator.Valid() && deleted < maxVotes; iterator.Next() { + proposalID, expedited := types.SplitTallyCleanupKey(iterator.Key()) + cursor := decodeCleanupCursor(iterator.Value()) + count, complete, nextCursor := keeper.cleanupProposalTallyVotes( + ctx, + proposalID, + expedited, + maxVotes-deleted, + cursor, + ) + deleted += count + + cleanupKey := types.TallyCleanupKey(proposalID, expedited) + if complete { + store.Delete(cleanupKey) + } else { + store.Set(cleanupKey, nextCursor) + } + } + + return deleted +} + +func (keeper Keeper) initializeTally(ctx sdk.Context, proposal types.Proposal) tallyProgress { + if keeper.IncrementalTallyEnabled(ctx) { + if boundary, _, found := keeper.getSelectedTallyBoundary(ctx, proposal.ProposalId); found { + return initializeTallyFromElectorate(boundary.Electorate, proposal.IsExpedited) + } + if !keeper.usesLegacyTallySemantics(ctx, proposal) { + if boundary, _, found := keeper.getDeadlineTallyBoundary(ctx, proposal.VotingEndTime); found { + return initializeTallyFromElectorate(boundary.Electorate, proposal.IsExpedited) + } + } + } + return initializeTallyFromElectorate(keeper.snapshotTallyElectorate(ctx), proposal.IsExpedited) +} + +func initializeTallyFromElectorate(electorate tallyElectorate, expedited bool) tallyProgress { + return tallyProgress{ + Results: newTallyOptionResults(), + TotalVotingPower: sdk.ZeroDec(), + TotalBondedTokens: electorate.TotalBondedTokens, + TallyParams: electorate.TallyParams, + Validators: electorate.Validators, + Expedited: expedited, + } +} + +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) + if snapshotValue == nil { + panic(fmt.Sprintf("missing delegation snapshot for proposal %d voter %s", proposalID, voter)) + } + snapshot := keeper.unmarshalVoteDelegations(snapshotValue) + keeper.addVoteToTally(validators, vote, snapshot) + + store.Set(types.TallyVoteKey(proposalID, progress.Expedited, voter), value) + store.Set(types.TallyVoteDelegationsKey(proposalID, progress.Expedited, voter), snapshotValue) + store.Delete(key) + store.Delete(snapshotKey) + store.Delete(types.VoterProposalsKey(voter, proposalID)) + keeper.deleteVoteDelegationSnapshotRevision(ctx, proposalID, voter) + progress.Cursor = key + processed++ + } + + 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 + } + + votingShares := delegation.Shares + validator.ObservedDelegatorShares = validator.ObservedDelegatorShares.Add(votingShares) + votingPower := votingShares.MulInt(validator.BondedTokens).Quo(validator.DelegatorShares) + validator.DelegatorResults.add(vote.Options, votingPower) + } +} + +func (keeper Keeper) voteDelegations( + ctx sdk.Context, + proposalID uint64, + expedited bool, + vote types.Vote, +) types.VoteDelegationSnapshot { + voter := sdk.MustAccAddressFromBech32(vote.Voter) + if !keeper.IncrementalTallyEnabled(ctx) { + return keeper.snapshotVoteDelegations(ctx, proposalID, voter) + } + store := ctx.KVStore(keeper.storeKey) + if bz := store.Get(types.VoteDelegationsKey(proposalID, voter)); bz != nil { + snapshot := keeper.unmarshalVoteDelegations(bz) + return keeper.applyVoteDelegationSnapshotUpdates(ctx, proposalID, voter, snapshot) + } + if bz := store.Get(types.TallyVoteDelegationsKey(proposalID, expedited, voter)); bz != nil { + return keeper.unmarshalVoteDelegations(bz) + } + if keeper.voteNeedsDelegationBackfill(ctx, proposalID) { + return keeper.snapshotVoteDelegations(ctx, proposalID, voter) + } + panic(fmt.Sprintf("missing delegation snapshot for proposal %d voter %s", 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 +} + +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 + } + + percentVoting := progress.TotalVotingPower.Quo(progress.TotalBondedTokens.ToDec()) + if percentVoting.LT(progress.TallyParams.GetQuorum(progress.Expedited)) { + return false, true, tallyResults + } + + if progress.TotalVotingPower.Sub(progress.Results.Abstain).IsZero() { + return false, false, tallyResults + } + + if progress.Results.NoWithVeto.Quo(progress.TotalVotingPower).GT(progress.TallyParams.VetoThreshold) { + return false, true, tallyResults + } + + 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 := types.TallyVoteDelegationsKeyFromVoteKey(iterator.Key()) + 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...) +} diff --git a/sei-cosmos/x/gov/keeper/tally_test.go b/sei-cosmos/x/gov/keeper/tally_test.go index 7c535e504d..6d9e3ca49e 100644 --- a/sei-cosmos/x/gov/keeper/tally_test.go +++ b/sei-cosmos/x/gov/keeper/tally_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "testing" + "time" tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" "github.com/stretchr/testify/require" @@ -499,3 +500,225 @@ 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]))) + require.False(t, store.Has(types.VoterProposalsKey(addrs[0], proposal.ProposalId))) + require.True(t, store.Has(types.VoterProposalsKey(addrs[1], proposal.ProposalId))) + + _, _, 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 TestTallyIncrementalIgnoresDelegationsAddedAfterTallyStarts(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) + _, _, queryResult := app.GovKeeper.Tally(ctx, proposal) + require.True(t, queryResult.Yes.Equal(snapshotValidatorTokens)) + require.True(t, queryResult.No.IsZero()) + + complete, processed, _, _, tallyResult := app.GovKeeper.TallyIncremental(ctx, proposal, 2) + require.True(t, complete) + require.Equal(t, 2, processed) + require.True(t, queryResult.Equals(tallyResult)) + 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 TestTallyIncrementalKeepsDelegationsRemovedAfterTallyStarts(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 TestTallyIncrementalUsesRedelegationsBeforeTallyStarts(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}) + + sourceValidator, 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, sourceValidator, 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[0], + types.NewNonSplitVoteOption(types.OptionYes), + )) + require.NoError(t, app.GovKeeper.AddVote( + ctx, + proposal.ProposalId, + addrs[3], + types.NewNonSplitVoteOption(types.OptionNo), + )) + + delegation, found := app.StakingKeeper.GetDelegation(ctx, addrs[3], valAddrs[0]) + require.True(t, found) + _, err = app.StakingKeeper.BeginRedelegation(ctx, addrs[3], valAddrs[0], valAddrs[1], delegation.GetShares()) + require.NoError(t, err) + app.GovKeeper.InitializeTally(ctx, proposal) + + complete, processed, _, _, tallyResult := app.GovKeeper.TallyIncremental(ctx, proposal, 2) + require.True(t, complete) + require.Equal(t, 2, processed) + require.True(t, tallyResult.Yes.Equal(app.StakingKeeper.TokensFromConsensusPower(ctx, 5))) + 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) + + app.GovKeeper.RemoveFromActiveProposalQueue(ctx, proposal.ProposalId, proposal.VotingEndTime) + proposal.IsExpedited = false + proposal.VotingEndTime = ctx.BlockTime().Add(time.Second) + app.GovKeeper.InsertActiveProposalQueue(ctx, proposal.ProposalId, proposal.VotingEndTime) + 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..7b5772e07d 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,14 @@ 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.IncrementalTallyEnabled(ctx) { + if proposal.VotingEndTime.Before(ctx.BlockTime()) { + return sdkerrors.Wrapf(types.ErrInactiveProposal, "%d", proposalID) + } + if keeper.voteDelegationSnapshotFrozen(ctx, proposal) || keeper.IsVoteDelegationBackfillInProgress(ctx, proposalID) { + return sdkerrors.Wrapf(types.ErrInactiveProposal, "%d", proposalID) + } + } for _, option := range options { if !types.ValidWeightedVoteOption(option) { @@ -61,10 +73,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 +116,152 @@ func (keeper Keeper) SetVote(ctx sdk.Context, vote types.Vote) { addr := sdk.MustAccAddressFromBech32(vote.Voter) store.Set(types.VoteKey(vote.ProposalId, addr), bz) + keeper.initializeVoteDelegationTracking(ctx, vote.ProposalId, addr) +} + +func (keeper Keeper) initializeVoteDelegationTracking(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) { + if !keeper.IncrementalTallyEnabled(ctx) { + return + } + ctx.KVStore(keeper.storeKey).Set(types.VoterProposalsKey(voter, proposalID), []byte{1}) + snapshot := keeper.snapshotVoteDelegations(ctx, proposalID, voter) + keeper.setVoteDelegationSnapshot(ctx, snapshot) +} + +// IncrementalTallyEnabled reports whether bounded governance tallying is active. +func (keeper Keeper) IncrementalTallyEnabled(ctx sdk.Context) bool { + activationCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeterWithMultiplier(ctx)).WithTraceMode(ctx.IsTracing()) + return activationCtx.KVStore(keeper.storeKey).Has(types.IncrementalTallyEnabledKey) +} + +func (keeper Keeper) snapshotVoteDelegations( + ctx sdk.Context, + proposalID uint64, + voter sdk.AccAddress, +) types.VoteDelegationSnapshot { + return keeper.snapshotVoteDelegationsExcept(ctx, proposalID, voter, nil) +} + +func (keeper Keeper) snapshotVoteDelegationsExcept( + ctx sdk.Context, + proposalID uint64, + voter sdk.AccAddress, + excludedValidator sdk.ValAddress, +) types.VoteDelegationSnapshot { + snapshot := types.VoteDelegationSnapshot{ + ProposalId: proposalID, + Voter: voter.String(), + Delegations: []types.VoteDelegation{}, + } + keeper.sk.IterateDelegations(ctx, voter, func(_ int64, delegation stakingtypes.DelegationI) bool { + if excludedValidator != nil && delegation.GetValidatorAddr().Equals(excludedValidator) { + return false + } + snapshot.Delegations = append(snapshot.Delegations, types.VoteDelegation{ + Validator: delegation.GetValidatorAddr().String(), + Shares: delegation.GetShares(), + }) + return false + }) + return snapshot +} + +func (keeper Keeper) refreshVoteDelegationSnapshots( + ctx sdk.Context, + voter sdk.AccAddress, + excludedValidator sdk.ValAddress, +) { + if !keeper.IncrementalTallyEnabled(ctx) { + return + } + store := ctx.KVStore(keeper.storeKey) + prefix := types.VoterProposalsKeyPrefixForAddress(voter) + iterator := sdk.KVStorePrefixIterator(store, prefix) + defer func() { _ = iterator.Close() }() + if !iterator.Valid() { + return + } + + snapshot := keeper.snapshotVoteDelegationsExcept(ctx, 0, voter, excludedValidator) + for ; iterator.Valid(); iterator.Next() { + proposalID := types.GetProposalIDFromBytes(iterator.Key()[len(prefix):]) + proposal, found := keeper.GetProposal(ctx, proposalID) + if !found || keeper.voteDelegationSnapshotFrozen(ctx, proposal) { + continue + } + snapshot.ProposalId = proposalID + keeper.setVoteDelegationSnapshot(ctx, snapshot) + } +} + +func (keeper Keeper) voteDelegationSnapshotFrozen(ctx sdk.Context, proposal types.Proposal) bool { + if _, found := keeper.proposalTallyBoundarySequence(ctx, proposal); found { + return true + } + if keeper.usesLegacyTallySemantics(ctx, proposal) { + return keeper.IsTallying(ctx, proposal.ProposalId) + } + return proposal.VotingEndTime.Before(ctx.BlockTime()) || keeper.IsTallying(ctx, proposal.ProposalId) +} + +func (keeper Keeper) setVoteDelegationSnapshot(ctx sdk.Context, snapshot types.VoteDelegationSnapshot) { + keeper.storeVoteDelegationSnapshot(ctx, snapshot, keeper.voteDelegationUpdateSequence(ctx)) +} + +func (keeper Keeper) storeVoteDelegationSnapshot( + ctx sdk.Context, + snapshot types.VoteDelegationSnapshot, + revision uint64, +) { + voter := sdk.MustAccAddressFromBech32(snapshot.Voter) + bz := keeper.cdc.MustMarshal(&snapshot) + ctx.KVStore(keeper.storeKey).Set(types.VoteDelegationsKey(snapshot.ProposalId, voter), bz) + keeper.setVoteDelegationSnapshotRevision(ctx, snapshot.ProposalId, voter, revision) +} + +// 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) + if keeper.IncrementalTallyEnabled(ctx) { + 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 +278,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 +291,63 @@ 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)) + if !keeper.IncrementalTallyEnabled(ctx) { + return pending + } + 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/keeper/vote_test.go b/sei-cosmos/x/gov/keeper/vote_test.go index 07900b7ca5..fb5d065676 100644 --- a/sei-cosmos/x/gov/keeper/vote_test.go +++ b/sei-cosmos/x/gov/keeper/vote_test.go @@ -1,7 +1,9 @@ package keeper_test import ( + "encoding/hex" "testing" + "time" tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" "github.com/stretchr/testify/require" @@ -9,6 +11,7 @@ import ( seiapp "github.com/sei-protocol/sei-chain/app" 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" ) func TestVotes(t *testing.T) { @@ -92,3 +95,152 @@ func TestVotes(t *testing.T) { require.True(t, votes[1].Options[3].Weight.Equal(sdk.NewDecWithPrec(5, 2))) require.Equal(t, types.OptionEmpty, vote.Option) } + +func TestAddVoteRejectsBlocksAfterVotingEnd(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + addrs := seiapp.AddTestAddrsIncremental(app, ctx, 2, sdk.NewInt(30000000)) + + proposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + proposal.Status = types.StatusVotingPeriod + proposal.VotingEndTime = ctx.BlockTime().Add(time.Second) + app.GovKeeper.SetProposal(ctx, proposal) + app.GovKeeper.InsertActiveProposalQueue(ctx, proposal.ProposalId, proposal.VotingEndTime) + + atVotingEnd := ctx.WithBlockTime(proposal.VotingEndTime) + require.NoError(t, app.GovKeeper.AddVote( + atVotingEnd, + proposal.ProposalId, + addrs[0], + types.NewNonSplitVoteOption(types.OptionYes), + )) + app.GovKeeper.CaptureExactTallyBoundary(atVotingEnd) + require.ErrorIs(t, app.GovKeeper.AddVote( + atVotingEnd, + proposal.ProposalId, + addrs[1], + types.NewNonSplitVoteOption(types.OptionYes), + ), types.ErrInactiveProposal) + + afterVotingEnd := ctx.WithBlockTime(proposal.VotingEndTime.Add(time.Second)) + require.ErrorIs(t, app.GovKeeper.AddVote( + afterVotingEnd, + proposal.ProposalId, + addrs[1], + types.NewNonSplitVoteOption(types.OptionYes), + ), types.ErrInactiveProposal) +} + +func TestVoteDelegationTrackingPreservesHistoricalTraces(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}).WithBlockTime(time.Unix(100, 0)) + 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 + proposal.VotingEndTime = ctx.BlockTime().Add(-time.Second) + app.GovKeeper.SetProposal(ctx, proposal) + + store := ctx.KVStore(app.GetKey(types.StoreKey)) + store.Delete(types.IncrementalTallyEnabledKey) + legacyCtx := ctx.WithIsTracing(true).WithClosestUpgradeName("v6.7") + require.NoError(t, app.GovKeeper.AddVote( + legacyCtx, + proposal.ProposalId, + addrs[0], + types.NewNonSplitVoteOption(types.OptionYes), + )) + require.False(t, store.Has(types.VoterProposalsKey(addrs[0], proposal.ProposalId))) + require.False(t, store.Has(types.VoteDelegationsKey(proposal.ProposalId, addrs[0]))) + require.Len(t, app.GovKeeper.GetAllVotes(legacyCtx), 1) + require.NotPanics(t, func() { + _, _, _ = app.GovKeeper.Tally(legacyCtx, proposal) + }) + + gasBeforeHook := legacyCtx.GasMeter().GasConsumed() + app.GovKeeper.StakingHooks().AfterDelegationModified(legacyCtx, addrs[0], valAddrs[0]) + require.Equal(t, gasBeforeHook, legacyCtx.GasMeter().GasConsumed()) + tracer, ok := legacyCtx.StoreTracer().(interface{ Dump() sdk.StoreTraceDump }) + require.True(t, ok) + trace := tracer.Dump() + require.NotContains(t, trace.Modules[types.ModuleName].Has, hex.EncodeToString(types.IncrementalTallyEnabledKey)) + + proposal.VotingEndTime = ctx.BlockTime().Add(time.Second) + app.GovKeeper.SetProposal(ctx, proposal) + app.GovKeeper.EnableIncrementalTally(ctx) + currentCtx := ctx.WithIsTracing(true).WithClosestUpgradeName("v6.6") + require.NoError(t, app.GovKeeper.AddVote( + currentCtx, + proposal.ProposalId, + addrs[0], + types.NewNonSplitVoteOption(types.OptionYes), + )) + require.True(t, store.Has(types.VoterProposalsKey(addrs[0], proposal.ProposalId))) + require.True(t, store.Has(types.VoteDelegationsKey(proposal.ProposalId, addrs[0]))) + + store.Delete(types.IncrementalTallyEnabledKey) + emptyProposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + emptyProposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, emptyProposal) + complete, _, _, _, _ := app.GovKeeper.TallyIncremental(legacyCtx, emptyProposal, 1) + require.True(t, complete) + require.False(t, store.Has(types.ProposalTallyBoundaryKey(emptyProposal.ProposalId))) + + initializedProposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + initializedProposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, initializedProposal) + app.GovKeeper.InitializeTally(legacyCtx, initializedProposal) + require.False(t, store.Has(types.ProposalTallyBoundaryKey(initializedProposal.ProposalId))) + + boundaryIterator := sdk.KVStorePrefixIterator(store, types.TallyBoundaryMetaKeyPrefix) + require.False(t, boundaryIterator.Valid()) + require.NoError(t, boundaryIterator.Close()) +} + +func TestVoteDelegationSnapshotsFreezeAfterVotingEnd(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}).WithBlockTime(time.Unix(100, 0)) + addrs, valAddrs := createValidators(t, ctx, app, []int64{5, 5, 5}) + + expiredProposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + expiredProposal.Status = types.StatusVotingPeriod + expiredProposal.VotingEndTime = ctx.BlockTime().Add(-time.Second) + app.GovKeeper.SetProposal(ctx, expiredProposal) + + activeProposal, err := app.GovKeeper.SubmitProposal(ctx, TestProposal) + require.NoError(t, err) + activeProposal.Status = types.StatusVotingPeriod + activeProposal.VotingEndTime = ctx.BlockTime().Add(time.Second) + app.GovKeeper.SetProposal(ctx, activeProposal) + + voteCtx := ctx.WithBlockTime(ctx.BlockTime().Add(-2 * time.Second)) + for _, proposal := range []types.Proposal{expiredProposal, activeProposal} { + require.NoError(t, app.GovKeeper.AddVote( + voteCtx, + proposal.ProposalId, + addrs[3], + types.NewNonSplitVoteOption(types.OptionYes), + )) + require.False(t, app.GovKeeper.IsTallying(ctx, proposal.ProposalId)) + } + + store := ctx.KVStore(app.GetKey(types.StoreKey)) + expiredSnapshotKey := types.VoteDelegationsKey(expiredProposal.ProposalId, addrs[3]) + activeSnapshotKey := types.VoteDelegationsKey(activeProposal.ProposalId, addrs[3]) + expiredSnapshot := append([]byte(nil), store.Get(expiredSnapshotKey)...) + activeSnapshot := append([]byte(nil), store.Get(activeSnapshotKey)...) + + 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) + + require.Equal(t, expiredSnapshot, store.Get(expiredSnapshotKey)) + require.NotEqual(t, activeSnapshot, store.Get(activeSnapshotKey)) +} diff --git a/sei-cosmos/x/gov/module.go b/sei-cosmos/x/gov/module.go index 2f61b161df..3629302218 100644 --- a/sei-cosmos/x/gov/module.go +++ b/sei-cosmos/x/gov/module.go @@ -173,6 +173,10 @@ func (am AppModule) RegisterServices(cfg module.Configurator) { if err != nil { panic(err) } + err = cfg.RegisterMigration(types.ModuleName, 3, m.Migrate3to4) + if err != nil { + panic(err) + } } // InitGenesis performs genesis initialization for the gov module. It returns @@ -201,7 +205,7 @@ func (am AppModule) ExportGenesisStream(ctx sdk.Context, cdc codec.JSONCodec) <- } // ConsensusVersion implements AppModule/ConsensusVersion. -func (AppModule) ConsensusVersion() uint64 { return 3 } +func (AppModule) ConsensusVersion() uint64 { return 4 } // AppModuleSimulation functions diff --git a/sei-cosmos/x/gov/simulation/decoder.go b/sei-cosmos/x/gov/simulation/decoder.go index dbfa8c8c84..8c44d1f4b0 100644 --- a/sei-cosmos/x/gov/simulation/decoder.go +++ b/sei-cosmos/x/gov/simulation/decoder.go @@ -41,12 +41,34 @@ 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), + bytes.Equal(kvA.Key[:1], types.VoterProposalsKeyPrefix), + bytes.Equal(kvA.Key[:1], types.VoteDelegationBackfillCutoffKey), + bytes.Equal(kvA.Key[:1], types.VoteDelegationBackfillProgressKeyPrefix), + bytes.Equal(kvA.Key[:1], types.VoteDelegationUpdateSequenceKey), + bytes.Equal(kvA.Key[:1], types.VoteDelegationUpdatesKeyPrefix), + bytes.Equal(kvA.Key[:1], types.VoterVoteDelegationUpdatesKeyPrefix), + bytes.Equal(kvA.Key[:1], types.VoteDelegationSnapshotRevisionKeyPrefix), + bytes.Equal(kvA.Key[:1], types.ProposalDeadlineKeyPrefix), + bytes.Equal(kvA.Key[:1], types.DeadlineBoundaryBlockTimeKey), + bytes.Equal(kvA.Key[:1], types.TallyBoundaryMetaKeyPrefix), + bytes.Equal(kvA.Key[:1], types.GapTallyBoundaryKeyPrefix), + bytes.Equal(kvA.Key[:1], types.ExactTallyBoundaryKeyPrefix), + bytes.Equal(kvA.Key[:1], types.ProposalTallyBoundaryKeyPrefix), + bytes.Equal(kvA.Key[:1], types.IncrementalTallyEnabledKey), + bytes.Equal(kvA.Key[:1], types.ModernTallyRoundKeyPrefix): + 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/01_concepts.md b/sei-cosmos/x/gov/spec/01_concepts.md index a004858b83..7d33dc1af0 100644 --- a/sei-cosmos/x/gov/spec/01_concepts.md +++ b/sei-cosmos/x/gov/spec/01_concepts.md @@ -58,7 +58,7 @@ arbitrary state changes. A proposal can be expedited, making the proposal use shorter voting duration and a higher tally quorum and tally threshold by default. -If an expedited proposal fails to meet the threshold within the scope of shorter voting duration, the expedited proposal is then converted to a regular proposal and resume voting under regular voting conditions. +If an expedited proposal fails to meet the threshold within the shorter voting duration, it is converted to a regular proposal and resumes voting under regular conditions. The regular round receives the remaining configured duration after the expedited tally completes, so tally processing does not consume its voting window. ## Deposit @@ -108,8 +108,8 @@ the moment the vote closes. `Voting period` should always be shorter than ### Expedited Voting period Expedited Proposal will have a shorter `Expedited Voting Period` compared to a regular `Voting Period`. -If the proposal has not passed after the `Expedited Voting Period`, it will be automatically -converted back to a regular proposal and fall back to use `Voting Period` unless the proposal is vetoed. +If the proposal has not passed after the `Expedited Voting Period`, it will be automatically +converted back to a regular proposal and resume for the difference between the regular and expedited voting periods unless the proposal is vetoed. That duration starts after the expedited tally completes. ### Option set diff --git a/sei-cosmos/x/gov/spec/02_state.md b/sei-cosmos/x/gov/spec/02_state.md index 269ff69272..f02aadd788 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, maintained until the proposal's electorate boundary. For pseudocode purposes, here are the two function we will use to read or write in stores: @@ -137,11 +139,19 @@ 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. + +Expired proposals remain queue-ordered. If an earlier proposal does not finish +within the block's vote-processing budget, the queue scan stops and later proposals +wait for the earlier tally to complete. This also prevents the block from initializing +validator snapshots for an unbounded number of proposals after the vote budget is +exhausted. And the pseudocode for the `ProposalProcessingQueue`: @@ -161,7 +171,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 +213,64 @@ 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 and deposits +are rejected after the voting period ends. A vote's per-validator delegation snapshot +is created with the vote and refreshed by staking hooks whenever that voter delegates, +undelegates, or redelegates before the proposal's electorate boundary. The boundary +freezes voter shares, bonded-validator tokens and shares, total bonded tokens, and +tally parameters together. Deadlines strictly between consecutive block times use the +state committed before the later block begins; deadlines equal to a block time use the +state at the start of governance `EndBlock`. Proposals sharing a boundary reuse one +validator electorate snapshot. A vote's delegation snapshot moves with the vote into +the tally archive. +Delegator results are accumulated per validator from those stored shares, so changes +after the boundary 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. + +Delegation changes caused by validator slashing are queued as constant-size updates +instead of rewriting every affected vote snapshot in `BeginBlock`. Before advancing +an affected tally, `EndBlock` folds updates through that proposal's frozen boundary +sequence into canonical vote snapshots under the same record-work budget. Later +updates do not delay or alter the frozen proposal. Read-only tally and export paths +overlay relevant queued updates so they remain consistent while that bounded work is +unfinished. Once an incremental tally has started, tally queries continue from its +persisted accumulator and frozen electorate. + +The version 4 governance store migration records the first proposal ID that does not +need delegation-tracking backfill. That cutoff is retained in application-state +exports. When an older proposal reaches tallying, its +delegation snapshots and active-vote index entries are created incrementally with a +per-proposal cursor under the same per-block vote-record budget as tallying and +cleanup. New votes are rejected once that backfill starts, and tally accumulation +cannot start until it completes. Because pre-upgrade votes have no historical +delegation index, these proposals use one tally-start boundary: snapshots already +backfilled continue following staking hooks, later batches read current shares, and +the validator electorate is frozen when the final batch completes. Votes and proposals +created after the upgrade already have the required tracking data, use deadline +boundaries, and skip backfill. Read-only tally and export operations derive a missing +snapshot while a proposal still needs backfill; after it completes, tally processing +treats a missing snapshot as an invariant failure. +When a legacy expedited proposal converts to a regular round, the new round uses a +deadline boundary and is recorded separately so application-state export preserves +that mode. + +Application-state export serializes all archived and pending votes together with their +effective delegation snapshots and frozen electorates. Import canonicalizes unfinished +deadline-boundary tallies by starting them again from all of their votes and frozen +state; it does not preserve the old vote cursor or pending-update cursor. This replay +produces the same final tally even when live staking state or governance parameters +changed after the electorate boundary. Export also canonicalizes an expired legacy +proposal whose backfill is unfinished: it materializes each effective delegation +snapshot, freezes an electorate at export, and import marks that backfill complete. +Unexpired legacy proposals retain their bounded live-backfill semantics. Expired +proposals do not reopen for votes before their first `EndBlock`. diff --git a/sei-cosmos/x/gov/types/expected_keepers.go b/sei-cosmos/x/gov/types/expected_keepers.go index 5c7d2000ca..92c078c1ef 100644 --- a/sei-cosmos/x/gov/types/expected_keepers.go +++ b/sei-cosmos/x/gov/types/expected_keepers.go @@ -25,6 +25,7 @@ type StakingKeeper interface { ctx sdk.Context, delegator sdk.AccAddress, fn func(index int64, delegation stakingtypes.DelegationI) (stop bool), ) + GetDelegation(ctx sdk.Context, delegator sdk.AccAddress, validator sdk.ValAddress) (stakingtypes.Delegation, bool) } // AccountKeeper defines the expected account keeper (noalias) diff --git a/sei-cosmos/x/gov/types/genesis.go b/sei-cosmos/x/gov/types/genesis.go index f0f2547927..41807ca358 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 @@ -28,12 +29,68 @@ func DefaultGenesisState() *GenesisState { func (data GenesisState) Equal(other GenesisState) bool { return data.StartingProposalId == other.StartingProposalId && + data.VoteDelegationBackfillCutoff == other.VoteDelegationBackfillCutoff && + modernTallyRoundProposalIDsEqual(data.ModernTallyRoundProposalIds, other.ModernTallyRoundProposalIds) && data.Deposits.Equal(other.Deposits) && data.Votes.Equal(other.Votes) && 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) && + tallyElectoratesEqual(data.TallyElectorates, other.TallyElectorates) +} + +func modernTallyRoundProposalIDsEqual(a, b []uint64) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +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 +} + +func tallyElectoratesEqual(a, b []TallyElectorate) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i].ProposalId != b[i].ProposalId || + !a[i].TotalBondedTokens.Equal(b[i].TotalBondedTokens) || + !a[i].TallyParams.Equal(b[i].TallyParams) || + len(a[i].TallyValidators) != len(b[i].TallyValidators) { + return false + } + for j := range a[i].TallyValidators { + if a[i].TallyValidators[j].Address != b[i].TallyValidators[j].Address || + !a[i].TallyValidators[j].BondedTokens.Equal(b[i].TallyValidators[j].BondedTokens) || + !a[i].TallyValidators[j].DelegatorShares.Equal(b[i].TallyValidators[j].DelegatorShares) { + return false + } + } + } + return true } // Empty returns true if a GenesisState is empty @@ -71,13 +128,204 @@ func ValidateGenesis(data *GenesisState) error { data.DepositParams.MinDeposit.String()) } + if err := validateVoteDelegationSnapshots(data.Votes, data.VoteDelegationSnapshots); err != nil { + return err + } + if err := validateTallyElectorates(data.Proposals, data.TallyElectorates); err != nil { + return err + } + if err := validateTallyElectorateVoteSnapshots(data.Votes, data.VoteDelegationSnapshots, data.TallyElectorates); err != nil { + return err + } + if err := validateModernTallyRounds(data.Proposals, data.VoteDelegationBackfillCutoff, data.ModernTallyRoundProposalIds); err != nil { + return err + } + if err := validateModernTallyRoundVoteSnapshots(data.Votes, data.VoteDelegationSnapshots, data.ModernTallyRoundProposalIds); err != nil { + return err + } + + return nil +} + +func validateModernTallyRounds(proposals Proposals, cutoff uint64, proposalIDs []uint64) error { + proposalsByID := make(map[uint64]Proposal, len(proposals)) + for _, proposal := range proposals { + proposalsByID[proposal.ProposalId] = proposal + } + + seen := make(map[uint64]struct{}, len(proposalIDs)) + for _, proposalID := range proposalIDs { + proposal, found := proposalsByID[proposalID] + if !found || proposal.Status != StatusVotingPeriod { + return fmt.Errorf("modern tally round for proposal %d has no voting-period proposal", proposalID) + } + if cutoff == 0 || proposalID >= cutoff { + return fmt.Errorf("modern tally round for proposal %d is not legacy", proposalID) + } + if proposal.IsExpedited { + return fmt.Errorf("modern tally round for proposal %d is expedited", proposalID) + } + if _, found := seen[proposalID]; found { + return fmt.Errorf("duplicate modern tally round for proposal %d", proposalID) + } + seen[proposalID] = struct{}{} + } + return nil +} + +func validateModernTallyRoundVoteSnapshots( + votes Votes, + snapshots []VoteDelegationSnapshot, + proposalIDs []uint64, +) error { + modernRounds := make(map[uint64]struct{}, len(proposalIDs)) + for _, proposalID := range proposalIDs { + modernRounds[proposalID] = struct{}{} + } + snapshotKeys := make(map[string]struct{}, len(snapshots)) + for _, snapshot := range snapshots { + snapshotKeys[fmt.Sprintf("%d/%s", snapshot.ProposalId, snapshot.Voter)] = struct{}{} + } + for _, vote := range votes { + if _, found := modernRounds[vote.ProposalId]; !found { + continue + } + key := fmt.Sprintf("%d/%s", vote.ProposalId, vote.Voter) + if _, found := snapshotKeys[key]; !found { + return fmt.Errorf("modern tally round vote %s has no delegation snapshot", key) + } + } + return nil +} + +func validateTallyElectorateVoteSnapshots( + votes Votes, + snapshots []VoteDelegationSnapshot, + electorates []TallyElectorate, +) error { + electorateProposals := make(map[uint64]struct{}, len(electorates)) + for _, electorate := range electorates { + electorateProposals[electorate.ProposalId] = struct{}{} + } + snapshotKeys := make(map[string]struct{}, len(snapshots)) + for _, snapshot := range snapshots { + snapshotKeys[fmt.Sprintf("%d/%s", snapshot.ProposalId, snapshot.Voter)] = struct{}{} + } + for _, vote := range votes { + if _, found := electorateProposals[vote.ProposalId]; !found { + continue + } + key := fmt.Sprintf("%d/%s", vote.ProposalId, vote.Voter) + if _, found := snapshotKeys[key]; !found { + return fmt.Errorf("tally electorate vote %s has no delegation snapshot", key) + } + } + return nil +} + +func validateTallyElectorates(proposals Proposals, electorates []TallyElectorate) error { + votingProposals := make(map[uint64]struct{}, len(proposals)) + for _, proposal := range proposals { + if proposal.Status == StatusVotingPeriod { + votingProposals[proposal.ProposalId] = struct{}{} + } + } + + seenElectorates := make(map[uint64]struct{}, len(electorates)) + for _, electorate := range electorates { + if _, found := votingProposals[electorate.ProposalId]; !found { + return fmt.Errorf("tally electorate for proposal %d has no voting-period proposal", electorate.ProposalId) + } + if _, found := seenElectorates[electorate.ProposalId]; found { + return fmt.Errorf("duplicate tally electorate for proposal %d", electorate.ProposalId) + } + seenElectorates[electorate.ProposalId] = struct{}{} + if electorate.TotalBondedTokens.IsNil() { + return fmt.Errorf("tally electorate total bonded tokens are not initialized") + } + if electorate.TotalBondedTokens.IsNegative() { + return fmt.Errorf("tally electorate total bonded tokens cannot be negative: %s", electorate.TotalBondedTokens) + } + if err := validateTallyParams(electorate.TallyParams); err != nil { + return fmt.Errorf("invalid tally electorate params for proposal %d: %w", electorate.ProposalId, err) + } + + seenValidators := make(map[string]struct{}, len(electorate.TallyValidators)) + validatorTokens := sdk.ZeroInt() + for _, validator := range electorate.TallyValidators { + if _, err := sdk.ValAddressFromBech32(validator.Address); err != nil { + return fmt.Errorf("invalid tally electorate validator %q: %w", validator.Address, err) + } + if _, found := seenValidators[validator.Address]; found { + return fmt.Errorf("duplicate tally electorate validator %q for proposal %d", validator.Address, electorate.ProposalId) + } + seenValidators[validator.Address] = struct{}{} + if validator.BondedTokens.IsNil() { + return fmt.Errorf("tally electorate validator bonded tokens are not initialized") + } + if !validator.BondedTokens.IsPositive() { + return fmt.Errorf("tally electorate validator bonded tokens must be positive: %s", validator.BondedTokens) + } + if validator.DelegatorShares.IsNil() { + return fmt.Errorf("tally electorate validator shares are not initialized") + } + if !validator.DelegatorShares.IsPositive() { + return fmt.Errorf("tally electorate validator shares must be positive: %s", validator.DelegatorShares) + } + validatorTokens = validatorTokens.Add(validator.BondedTokens) + } + if !validatorTokens.Equal(electorate.TotalBondedTokens) { + return fmt.Errorf( + "tally electorate validator tokens %s do not equal total bonded tokens %s", + validatorTokens, + electorate.TotalBondedTokens, + ) + } + } + 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..d800b14a03 100644 --- a/sei-cosmos/x/gov/types/genesis.pb.go +++ b/sei-cosmos/x/gov/types/genesis.pb.go @@ -7,6 +7,7 @@ import ( fmt "fmt" _ "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" io "io" math "math" math_bits "math/bits" @@ -39,6 +40,14 @@ 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 maintained for each vote. + VoteDelegationSnapshots []VoteDelegationSnapshot `protobuf:"bytes,8,rep,name=vote_delegation_snapshots,json=voteDelegationSnapshots,proto3" json:"vote_delegation_snapshots"` + // tally_electorates defines the frozen electorate for unresolved proposals. + TallyElectorates []TallyElectorate `protobuf:"bytes,9,rep,name=tally_electorates,json=tallyElectorates,proto3" json:"tally_electorates"` + // vote_delegation_backfill_cutoff defines the first proposal ID created after vote delegation tracking began. + VoteDelegationBackfillCutoff uint64 `protobuf:"varint,10,opt,name=vote_delegation_backfill_cutoff,json=voteDelegationBackfillCutoff,proto3" json:"vote_delegation_backfill_cutoff,omitempty" yaml:"vote_delegation_backfill_cutoff"` + // modern_tally_round_proposal_ids defines legacy proposals whose converted regular round uses deadline tallying. + ModernTallyRoundProposalIds []uint64 `protobuf:"varint,11,rep,packed,name=modern_tally_round_proposal_ids,json=modernTallyRoundProposalIds,proto3" json:"modern_tally_round_proposal_ids,omitempty" yaml:"modern_tally_round_proposal_ids"` } func (m *GenesisState) Reset() { *m = GenesisState{} } @@ -123,42 +132,314 @@ func (m *GenesisState) GetTallyParams() TallyParams { return TallyParams{} } +func (m *GenesisState) GetVoteDelegationSnapshots() []VoteDelegationSnapshot { + if m != nil { + return m.VoteDelegationSnapshots + } + return nil +} + +func (m *GenesisState) GetTallyElectorates() []TallyElectorate { + if m != nil { + return m.TallyElectorates + } + return nil +} + +func (m *GenesisState) GetVoteDelegationBackfillCutoff() uint64 { + if m != nil { + return m.VoteDelegationBackfillCutoff + } + return 0 +} + +func (m *GenesisState) GetModernTallyRoundProposalIds() []uint64 { + if m != nil { + return m.ModernTallyRoundProposalIds + } + return nil +} + +// VoteDelegationSnapshot defines the per-validator delegation shares maintained for 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 "" +} + +// TallyElectorate defines the validator and parameter state used to tally one proposal. +type TallyElectorate struct { + ProposalId uint64 `protobuf:"varint,1,opt,name=proposal_id,json=proposalId,proto3" json:"proposal_id,omitempty" yaml:"proposal_id"` + TotalBondedTokens github_com_sei_protocol_sei_chain_sei_cosmos_types.Int `protobuf:"bytes,2,opt,name=total_bonded_tokens,json=totalBondedTokens,proto3,customtype=github.com/sei-protocol/sei-chain/sei-cosmos/types.Int" json:"total_bonded_tokens"` + TallyParams TallyParams `protobuf:"bytes,3,opt,name=tally_params,json=tallyParams,proto3" json:"tally_params"` + TallyValidators []TallyValidator `protobuf:"bytes,4,rep,name=tally_validators,json=tallyValidators,proto3" json:"tally_validators"` +} + +func (m *TallyElectorate) Reset() { *m = TallyElectorate{} } +func (m *TallyElectorate) String() string { return proto.CompactTextString(m) } +func (*TallyElectorate) ProtoMessage() {} +func (*TallyElectorate) Descriptor() ([]byte, []int) { + return fileDescriptor_43cd825e0fa7a627, []int{3} +} +func (m *TallyElectorate) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *TallyElectorate) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_TallyElectorate.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 *TallyElectorate) XXX_Merge(src proto.Message) { + xxx_messageInfo_TallyElectorate.Merge(m, src) +} +func (m *TallyElectorate) XXX_Size() int { + return m.Size() +} +func (m *TallyElectorate) XXX_DiscardUnknown() { + xxx_messageInfo_TallyElectorate.DiscardUnknown(m) +} + +var xxx_messageInfo_TallyElectorate proto.InternalMessageInfo + +func (m *TallyElectorate) GetProposalId() uint64 { + if m != nil { + return m.ProposalId + } + return 0 +} + +func (m *TallyElectorate) GetTallyParams() TallyParams { + if m != nil { + return m.TallyParams + } + return TallyParams{} +} + +func (m *TallyElectorate) GetTallyValidators() []TallyValidator { + if m != nil { + return m.TallyValidators + } + return nil +} + +// TallyValidator defines a validator's frozen state in a proposal electorate. +type TallyValidator struct { + Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + BondedTokens github_com_sei_protocol_sei_chain_sei_cosmos_types.Int `protobuf:"bytes,2,opt,name=bonded_tokens,json=bondedTokens,proto3,customtype=github.com/sei-protocol/sei-chain/sei-cosmos/types.Int" json:"bonded_tokens"` + DelegatorShares github_com_sei_protocol_sei_chain_sei_cosmos_types.Dec `protobuf:"bytes,3,opt,name=delegator_shares,json=delegatorShares,proto3,customtype=github.com/sei-protocol/sei-chain/sei-cosmos/types.Dec" json:"delegator_shares"` +} + +func (m *TallyValidator) Reset() { *m = TallyValidator{} } +func (m *TallyValidator) String() string { return proto.CompactTextString(m) } +func (*TallyValidator) ProtoMessage() {} +func (*TallyValidator) Descriptor() ([]byte, []int) { + return fileDescriptor_43cd825e0fa7a627, []int{4} +} +func (m *TallyValidator) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *TallyValidator) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_TallyValidator.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 *TallyValidator) XXX_Merge(src proto.Message) { + xxx_messageInfo_TallyValidator.Merge(m, src) +} +func (m *TallyValidator) XXX_Size() int { + return m.Size() +} +func (m *TallyValidator) XXX_DiscardUnknown() { + xxx_messageInfo_TallyValidator.DiscardUnknown(m) +} + +var xxx_messageInfo_TallyValidator proto.InternalMessageInfo + +func (m *TallyValidator) GetAddress() string { + if m != nil { + return m.Address + } + 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") + proto.RegisterType((*TallyElectorate)(nil), "cosmos.gov.v1beta1.TallyElectorate") + proto.RegisterType((*TallyValidator)(nil), "cosmos.gov.v1beta1.TallyValidator") } 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, + // 826 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x56, 0xcd, 0x6e, 0xdb, 0x46, + 0x10, 0x16, 0x2d, 0xff, 0x48, 0x2b, 0xf9, 0x6f, 0xad, 0xba, 0xac, 0xa5, 0x8a, 0x2a, 0x0b, 0x14, + 0x82, 0x81, 0x4a, 0xb0, 0x0b, 0xb4, 0x45, 0x81, 0xf6, 0xc0, 0xba, 0x68, 0x5d, 0xa0, 0x80, 0x4b, + 0x19, 0x3a, 0xe4, 0x42, 0xac, 0xc8, 0x35, 0x45, 0x98, 0xe2, 0x32, 0xdc, 0x35, 0x11, 0xbf, 0x40, + 0xce, 0x39, 0xe4, 0x29, 0x72, 0xcd, 0x4b, 0xf8, 0xe8, 0x63, 0x92, 0x83, 0x12, 0xd8, 0x6f, 0xa0, + 0x63, 0x4e, 0x01, 0x77, 0x97, 0x92, 0x68, 0x4b, 0x76, 0x12, 0x38, 0x37, 0xee, 0xec, 0x37, 0xdf, + 0x37, 0x33, 0x3b, 0x33, 0x20, 0x68, 0xd8, 0x84, 0x0e, 0x08, 0x6d, 0xbb, 0x24, 0x6e, 0xc7, 0x7b, + 0x3d, 0xcc, 0xd0, 0x5e, 0xdb, 0xc5, 0x01, 0xa6, 0x1e, 0x6d, 0x85, 0x11, 0x61, 0x04, 0x42, 0x81, + 0x68, 0xb9, 0x24, 0x6e, 0x49, 0xc4, 0x4e, 0x6d, 0x96, 0x17, 0x89, 0x85, 0xc7, 0x4e, 0xc5, 0x25, + 0x2e, 0xe1, 0x9f, 0xed, 0xe4, 0x4b, 0x58, 0xf5, 0xe7, 0x05, 0x50, 0xfe, 0x5b, 0x30, 0x77, 0x18, + 0x62, 0x18, 0xfe, 0x0f, 0x2a, 0x94, 0xa1, 0x88, 0x79, 0x81, 0x6b, 0x85, 0x11, 0x09, 0x09, 0x45, + 0xbe, 0xe5, 0x39, 0xaa, 0xd2, 0x50, 0x9a, 0x8b, 0x86, 0x36, 0x1a, 0x6a, 0xd5, 0x73, 0x34, 0xf0, + 0x7f, 0xd3, 0x67, 0xa1, 0x74, 0x13, 0xa6, 0xe6, 0x23, 0x69, 0x3d, 0x74, 0xe0, 0x21, 0x28, 0x38, + 0x38, 0x24, 0xd4, 0x63, 0x54, 0x5d, 0x68, 0xe4, 0x9b, 0xa5, 0xfd, 0x6a, 0xeb, 0x76, 0xf8, 0xad, + 0x03, 0x81, 0x31, 0x36, 0x2e, 0x86, 0x5a, 0xee, 0xc5, 0x5b, 0xad, 0x20, 0x0d, 0xd4, 0x1c, 0xbb, + 0xc3, 0xdf, 0xc1, 0x52, 0x4c, 0x18, 0xa6, 0x6a, 0x9e, 0xf3, 0xa8, 0xb3, 0x78, 0xba, 0x84, 0x61, + 0x63, 0x55, 0x92, 0x2c, 0x25, 0x27, 0x6a, 0x0a, 0x2f, 0xf8, 0x1f, 0x28, 0xa6, 0xd1, 0x52, 0x75, + 0x91, 0x53, 0xd4, 0x66, 0x51, 0xa4, 0xc1, 0x1b, 0x9b, 0x92, 0xa6, 0x98, 0x5a, 0xa8, 0x39, 0x61, + 0x80, 0x2e, 0x58, 0x93, 0x91, 0x59, 0x21, 0x8a, 0xd0, 0x80, 0xaa, 0x4b, 0x0d, 0xa5, 0x59, 0xda, + 0xff, 0xee, 0x8e, 0xf4, 0x8e, 0x38, 0xd0, 0xf8, 0x36, 0x21, 0x1e, 0x0d, 0xb5, 0xaf, 0x44, 0x31, + 0xb3, 0x34, 0xba, 0xb9, 0xea, 0x4c, 0xa3, 0xa1, 0x0d, 0x56, 0x63, 0x22, 0x8a, 0x2d, 0x74, 0x96, + 0xb9, 0x4e, 0x63, 0x4e, 0xfa, 0x49, 0xf9, 0x85, 0x4c, 0x4d, 0xca, 0x54, 0x84, 0x4c, 0x86, 0x44, + 0x37, 0xcb, 0xf1, 0x14, 0x16, 0x5a, 0xa0, 0xcc, 0x90, 0xef, 0x9f, 0xa7, 0x1a, 0x2b, 0x5c, 0x43, + 0x9b, 0xa5, 0x71, 0x9c, 0xe0, 0xa4, 0x44, 0x55, 0x4a, 0x6c, 0x09, 0x89, 0x69, 0x0a, 0xdd, 0x2c, + 0xb1, 0x09, 0x12, 0xfa, 0xe0, 0x9b, 0xe4, 0x19, 0x2c, 0x07, 0xfb, 0xd8, 0x45, 0xcc, 0x23, 0x81, + 0x45, 0x03, 0x14, 0xd2, 0x3e, 0x61, 0x54, 0x2d, 0xf0, 0xd7, 0xd8, 0x9d, 0xf7, 0xa0, 0x07, 0x63, + 0x9f, 0x8e, 0x74, 0x31, 0x16, 0x13, 0x61, 0xf3, 0xeb, 0x78, 0xe6, 0x2d, 0x85, 0x5d, 0xb0, 0x29, + 0x62, 0xc1, 0x3e, 0xb6, 0x19, 0x89, 0x50, 0xd2, 0x36, 0x45, 0xae, 0xf2, 0xfd, 0xdc, 0x9c, 0xfe, + 0x1a, 0x63, 0x25, 0xfd, 0x06, 0xcb, 0x9a, 0x29, 0x7c, 0x0c, 0xb4, 0x9b, 0x59, 0xf4, 0x90, 0x7d, + 0x7a, 0xe2, 0xf9, 0xbe, 0x65, 0x9f, 0x31, 0x72, 0x72, 0xa2, 0x02, 0x3e, 0x2b, 0xbb, 0xa3, 0xa1, + 0xf6, 0xc3, 0xb8, 0xee, 0x77, 0x39, 0xe8, 0x66, 0x2d, 0x9b, 0x85, 0x21, 0xef, 0xff, 0xe4, 0xd7, + 0x30, 0x04, 0xda, 0x80, 0x38, 0x38, 0x0a, 0x2c, 0x91, 0x51, 0x44, 0xce, 0x02, 0x67, 0x7a, 0xee, + 0xa8, 0x5a, 0x6a, 0xe4, 0xb3, 0x92, 0xf7, 0x38, 0xe8, 0x66, 0x55, 0x20, 0x78, 0xda, 0x66, 0x72, + 0x3f, 0x99, 0x58, 0xaa, 0xbf, 0x54, 0xc0, 0xf6, 0xec, 0xb2, 0xc3, 0x5f, 0x40, 0xe9, 0xf6, 0x5e, + 0xd8, 0x1e, 0x0d, 0x35, 0x28, 0x84, 0x33, 0xeb, 0x00, 0x84, 0x93, 0x35, 0x50, 0x11, 0xb3, 0x1b, + 0xa9, 0x0b, 0x0d, 0xa5, 0x59, 0x14, 0x23, 0x19, 0xc1, 0x7f, 0x41, 0x69, 0x52, 0x98, 0x74, 0xae, + 0xf5, 0xfb, 0xdb, 0x40, 0xbe, 0xcf, 0xb4, 0xb3, 0xfe, 0x54, 0x01, 0x6b, 0x59, 0x14, 0xac, 0x81, + 0x62, 0x8c, 0x7c, 0xcf, 0x41, 0x8c, 0x44, 0x3c, 0xd6, 0xa2, 0x39, 0x31, 0xc0, 0x2e, 0x58, 0xa6, + 0x7d, 0x14, 0x61, 0x2a, 0x62, 0x32, 0xfe, 0x48, 0x38, 0xdf, 0x0c, 0xb5, 0x9f, 0x5d, 0x8f, 0xf5, + 0xcf, 0x7a, 0x2d, 0x9b, 0x0c, 0xda, 0x14, 0x7b, 0x3f, 0xf2, 0x5d, 0x69, 0x13, 0x9f, 0x1f, 0xec, + 0x3e, 0xf2, 0x02, 0xf1, 0x25, 0xf6, 0x2d, 0x3b, 0x0f, 0x31, 0x6d, 0x1d, 0x60, 0xdb, 0x94, 0x6c, + 0xfa, 0xeb, 0x05, 0xb0, 0x7e, 0xa3, 0x9f, 0x3e, 0xbf, 0x6e, 0x01, 0xd8, 0x62, 0x84, 0x21, 0xdf, + 0xea, 0x91, 0xc0, 0xc1, 0x8e, 0xc5, 0xc8, 0x29, 0x0e, 0x1e, 0x22, 0xe2, 0xc3, 0x80, 0x99, 0x9b, + 0x9c, 0xda, 0xe0, 0xcc, 0xc7, 0x9c, 0x18, 0xfe, 0x73, 0x63, 0x0f, 0xe4, 0x3f, 0x6e, 0x0f, 0xc8, + 0xf7, 0x98, 0x1e, 0xf8, 0x0e, 0x10, 0xe3, 0x63, 0x8d, 0x2b, 0x9e, 0x6e, 0x5d, 0x7d, 0x2e, 0x5b, + 0x37, 0x85, 0x4a, 0xc2, 0x75, 0x96, 0xb1, 0x52, 0xfd, 0xbd, 0x02, 0xd6, 0xb2, 0x48, 0xa8, 0x82, + 0x15, 0xe4, 0x38, 0x11, 0xa6, 0x54, 0x3e, 0x71, 0x7a, 0x4c, 0x16, 0xe7, 0x97, 0xa8, 0x5a, 0xb9, + 0x37, 0x5d, 0x30, 0x0f, 0x6c, 0xc8, 0x2e, 0x24, 0x91, 0x25, 0xfb, 0x29, 0xff, 0x20, 0xfd, 0xb4, + 0x3e, 0xe6, 0xed, 0x70, 0x5a, 0xc3, 0xbc, 0xb8, 0xaa, 0x2b, 0x97, 0x57, 0x75, 0xe5, 0xdd, 0x55, + 0x5d, 0x79, 0x76, 0x5d, 0xcf, 0x5d, 0x5e, 0xd7, 0x73, 0xaf, 0xae, 0xeb, 0xb9, 0x47, 0xbf, 0x7e, + 0x92, 0xc4, 0x13, 0xfe, 0x93, 0xc0, 0x85, 0x7a, 0xcb, 0x1c, 0xfa, 0xd3, 0x87, 0x00, 0x00, 0x00, + 0xff, 0xff, 0x9a, 0x07, 0xad, 0x85, 0x75, 0x08, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -181,6 +462,57 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.ModernTallyRoundProposalIds) > 0 { + dAtA2 := make([]byte, len(m.ModernTallyRoundProposalIds)*10) + var j1 int + for _, num := range m.ModernTallyRoundProposalIds { + for num >= 1<<7 { + dAtA2[j1] = uint8(uint64(num)&0x7f | 0x80) + num >>= 7 + j1++ + } + dAtA2[j1] = uint8(num) + j1++ + } + i -= j1 + copy(dAtA[i:], dAtA2[:j1]) + i = encodeVarintGenesis(dAtA, i, uint64(j1)) + i-- + dAtA[i] = 0x5a + } + if m.VoteDelegationBackfillCutoff != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.VoteDelegationBackfillCutoff)) + i-- + dAtA[i] = 0x50 + } + if len(m.TallyElectorates) > 0 { + for iNdEx := len(m.TallyElectorates) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.TallyElectorates[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x4a + } + } + 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 { @@ -249,72 +581,1055 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { i -= size i = encodeVarintGenesis(dAtA, i, uint64(size)) } - i-- - dAtA[i] = 0x12 + i-- + dAtA[i] = 0x12 + } + } + if m.StartingProposalId != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.StartingProposalId)) + i-- + dAtA[i] = 0x8 + } + 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 (m *TallyElectorate) 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 *TallyElectorate) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *TallyElectorate) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.TallyValidators) > 0 { + for iNdEx := len(m.TallyValidators) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.TallyValidators[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + } + { + size, err := m.TallyParams.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + { + size := m.TotalBondedTokens.Size() + i -= size + if _, err := m.TotalBondedTokens.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + 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 *TallyValidator) 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 *TallyValidator) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *TallyValidator) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size := m.DelegatorShares.Size() + i -= size + if _, err := m.DelegatorShares.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + { + size := m.BondedTokens.Size() + i -= size + if _, err := m.BondedTokens.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + if len(m.Address) > 0 { + i -= len(m.Address) + copy(dAtA[i:], m.Address) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.Address))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func encodeVarintGenesis(dAtA []byte, offset int, v uint64) int { + offset -= sovGenesis(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *GenesisState) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.StartingProposalId != 0 { + n += 1 + sovGenesis(uint64(m.StartingProposalId)) + } + if len(m.Deposits) > 0 { + for _, e := range m.Deposits { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + if len(m.Votes) > 0 { + for _, e := range m.Votes { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + if len(m.Proposals) > 0 { + for _, e := range m.Proposals { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + l = m.DepositParams.Size() + n += 1 + l + sovGenesis(uint64(l)) + l = m.VotingParams.Size() + 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)) + } + } + if len(m.TallyElectorates) > 0 { + for _, e := range m.TallyElectorates { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + if m.VoteDelegationBackfillCutoff != 0 { + n += 1 + sovGenesis(uint64(m.VoteDelegationBackfillCutoff)) + } + if len(m.ModernTallyRoundProposalIds) > 0 { + l = 0 + for _, e := range m.ModernTallyRoundProposalIds { + l += sovGenesis(uint64(e)) + } + n += 1 + sovGenesis(uint64(l)) + 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 +} + +func (m *TallyElectorate) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.ProposalId != 0 { + n += 1 + sovGenesis(uint64(m.ProposalId)) + } + l = m.TotalBondedTokens.Size() + n += 1 + l + sovGenesis(uint64(l)) + l = m.TallyParams.Size() + n += 1 + l + sovGenesis(uint64(l)) + if len(m.TallyValidators) > 0 { + for _, e := range m.TallyValidators { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + return n +} + +func (m *TallyValidator) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Address) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + l = m.BondedTokens.Size() + n += 1 + l + sovGenesis(uint64(l)) + l = m.DelegatorShares.Size() + n += 1 + l + sovGenesis(uint64(l)) + return n +} + +func sovGenesis(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozGenesis(x uint64) (n int) { + return sovGenesis(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *GenesisState) 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: GenesisState: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GenesisState: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field StartingProposalId", wireType) + } + m.StartingProposalId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.StartingProposalId |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Deposits", 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.Deposits = append(m.Deposits, Deposit{}) + if err := m.Deposits[len(m.Deposits)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Votes", 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.Votes = append(m.Votes, Vote{}) + if err := m.Votes[len(m.Votes)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Proposals", 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.Proposals = append(m.Proposals, Proposal{}) + if err := m.Proposals[len(m.Proposals)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DepositParams", 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 + } + if err := m.DepositParams.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field VotingParams", 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 + } + if err := m.VotingParams.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 7: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field TallyParams", 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 + } + if err := m.TallyParams.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + 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 + case 9: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field TallyElectorates", 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.TallyElectorates = append(m.TallyElectorates, TallyElectorate{}) + if err := m.TallyElectorates[len(m.TallyElectorates)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 10: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field VoteDelegationBackfillCutoff", wireType) + } + m.VoteDelegationBackfillCutoff = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.VoteDelegationBackfillCutoff |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 11: + if wireType == 0 { + var v uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.ModernTallyRoundProposalIds = append(m.ModernTallyRoundProposalIds, v) + } else if wireType == 2 { + var packedLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + packedLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if packedLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + packedLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var elementCount int + var count int + for _, integer := range dAtA[iNdEx:postIndex] { + if integer < 128 { + count++ + } + } + elementCount = count + if elementCount != 0 && len(m.ModernTallyRoundProposalIds) == 0 { + m.ModernTallyRoundProposalIds = make([]uint64, 0, elementCount) + } + for iNdEx < postIndex { + var v uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.ModernTallyRoundProposalIds = append(m.ModernTallyRoundProposalIds, v) + } + } else { + return fmt.Errorf("proto: wrong wireType = %d for field ModernTallyRoundProposalIds", wireType) + } + 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 m.StartingProposalId != 0 { - i = encodeVarintGenesis(dAtA, i, uint64(m.StartingProposalId)) - i-- - dAtA[i] = 0x8 - } - return len(dAtA) - i, nil -} -func encodeVarintGenesis(dAtA []byte, offset int, v uint64) int { - offset -= sovGenesis(v) - base := offset - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ + if iNdEx > l { + return io.ErrUnexpectedEOF } - dAtA[offset] = uint8(v) - return base + return nil } -func (m *GenesisState) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.StartingProposalId != 0 { - n += 1 + sovGenesis(uint64(m.StartingProposalId)) - } - if len(m.Deposits) > 0 { - for _, e := range m.Deposits { - l = e.Size() - n += 1 + l + sovGenesis(uint64(l)) +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 + } } - } - if len(m.Votes) > 0 { - for _, e := range m.Votes { - l = e.Size() - n += 1 + l + sovGenesis(uint64(l)) + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: VoteDelegation: wiretype end group for non-group") } - } - if len(m.Proposals) > 0 { - for _, e := range m.Proposals { - l = e.Size() - n += 1 + l + sovGenesis(uint64(l)) + 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:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenesis + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy } } - l = m.DepositParams.Size() - n += 1 + l + sovGenesis(uint64(l)) - l = m.VotingParams.Size() - n += 1 + l + sovGenesis(uint64(l)) - l = m.TallyParams.Size() - n += 1 + l + sovGenesis(uint64(l)) - return n -} -func sovGenesis(x uint64) (n int) { - return (math_bits.Len64(x|1) + 6) / 7 -} -func sozGenesis(x uint64) (n int) { - return sovGenesis(uint64((x << 1) ^ uint64((int64(x) >> 63)))) + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil } -func (m *GenesisState) Unmarshal(dAtA []byte) error { +func (m *TallyElectorate) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -337,17 +1652,17 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: GenesisState: wiretype end group for non-group") + return fmt.Errorf("proto: TallyElectorate: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: GenesisState: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: TallyElectorate: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field StartingProposalId", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field ProposalId", wireType) } - m.StartingProposalId = 0 + m.ProposalId = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowGenesis @@ -357,16 +1672,16 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - m.StartingProposalId |= uint64(b&0x7F) << shift + m.ProposalId |= uint64(b&0x7F) << shift if b < 0x80 { break } } case 2: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Deposits", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field TotalBondedTokens", wireType) } - var msglen int + var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowGenesis @@ -376,29 +1691,29 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - msglen |= int(b&0x7F) << shift + stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } - if msglen < 0 { + intStringLen := int(stringLen) + if intStringLen < 0 { return ErrInvalidLengthGenesis } - postIndex := iNdEx + msglen + postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthGenesis } if postIndex > l { return io.ErrUnexpectedEOF } - m.Deposits = append(m.Deposits, Deposit{}) - if err := m.Deposits[len(m.Deposits)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + if err := m.TotalBondedTokens.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 3: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Votes", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field TallyParams", wireType) } var msglen int for shift := uint(0); ; shift += 7 { @@ -425,14 +1740,13 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Votes = append(m.Votes, Vote{}) - if err := m.Votes[len(m.Votes)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + if err := m.TallyParams.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 4: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Proposals", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field TallyValidators", wireType) } var msglen int for shift := uint(0); ; shift += 7 { @@ -459,16 +1773,66 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Proposals = append(m.Proposals, Proposal{}) - if err := m.Proposals[len(m.Proposals)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + m.TallyValidators = append(m.TallyValidators, TallyValidator{}) + if err := m.TallyValidators[len(m.TallyValidators)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex - case 5: + 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 *TallyValidator) 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: TallyValidator: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: TallyValidator: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field DepositParams", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Address", wireType) } - var msglen int + var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowGenesis @@ -478,30 +1842,29 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - msglen |= int(b&0x7F) << shift + stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } - if msglen < 0 { + intStringLen := int(stringLen) + if intStringLen < 0 { return ErrInvalidLengthGenesis } - postIndex := iNdEx + msglen + postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthGenesis } if postIndex > l { return io.ErrUnexpectedEOF } - if err := m.DepositParams.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } + m.Address = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex - case 6: + case 2: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field VotingParams", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field BondedTokens", wireType) } - var msglen int + var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowGenesis @@ -511,30 +1874,31 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - msglen |= int(b&0x7F) << shift + stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } - if msglen < 0 { + intStringLen := int(stringLen) + if intStringLen < 0 { return ErrInvalidLengthGenesis } - postIndex := iNdEx + msglen + postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthGenesis } if postIndex > l { return io.ErrUnexpectedEOF } - if err := m.VotingParams.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + if err := m.BondedTokens.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex - case 7: + case 3: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field TallyParams", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field DelegatorShares", wireType) } - var msglen int + var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowGenesis @@ -544,22 +1908,23 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - msglen |= int(b&0x7F) << shift + stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } - if msglen < 0 { + intStringLen := int(stringLen) + if intStringLen < 0 { return ErrInvalidLengthGenesis } - postIndex := iNdEx + msglen + postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthGenesis } if postIndex > l { return io.ErrUnexpectedEOF } - if err := m.TallyParams.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + if err := m.DelegatorShares.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex diff --git a/sei-cosmos/x/gov/types/genesis_test.go b/sei-cosmos/x/gov/types/genesis_test.go index a0fbebde22..644f0908ca 100644 --- a/sei-cosmos/x/gov/types/genesis_test.go +++ b/sei-cosmos/x/gov/types/genesis_test.go @@ -1,8 +1,10 @@ package types import ( + "bytes" "testing" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/stretchr/testify/require" ) @@ -21,8 +23,108 @@ func TestEqualProposalID(t *testing.T) { require.True(t, state1.Equal(state2)) } +func TestGenesisStateEqualIncludesTallyElectorates(t *testing.T) { + state1 := GenesisState{TallyElectorates: []TallyElectorate{validTallyElectorate(1)}} + state2 := state1 + require.True(t, state1.Equal(state2)) + + state2.TallyElectorates = nil + require.False(t, state1.Equal(state2)) +} + +func TestGenesisStateEqualIncludesVoteDelegationBackfillCutoff(t *testing.T) { + state1 := GenesisState{VoteDelegationBackfillCutoff: 3} + state2 := state1 + require.True(t, state1.Equal(state2)) + + state2.VoteDelegationBackfillCutoff = 4 + require.False(t, state1.Equal(state2)) +} + +func TestGenesisStateEqualIncludesModernTallyRounds(t *testing.T) { + state1 := GenesisState{ModernTallyRoundProposalIds: []uint64{1}} + state2 := state1 + require.True(t, state1.Equal(state2)) + + state2.ModernTallyRoundProposalIds = nil + require.False(t, state1.Equal(state2)) +} + +func TestValidateGenesisModernTallyRounds(t *testing.T) { + state := DefaultGenesisState() + state.VoteDelegationBackfillCutoff = 2 + state.Proposals = Proposals{{ProposalId: 1, Status: StatusVotingPeriod}} + state.ModernTallyRoundProposalIds = []uint64{1} + require.NoError(t, ValidateGenesis(state)) + + state.ModernTallyRoundProposalIds = []uint64{1, 1} + require.ErrorContains(t, ValidateGenesis(state), "duplicate modern tally round") + + state.ModernTallyRoundProposalIds = []uint64{2} + require.ErrorContains(t, ValidateGenesis(state), "has no voting-period proposal") + + state.Proposals[0].IsExpedited = true + state.ModernTallyRoundProposalIds = []uint64{1} + require.ErrorContains(t, ValidateGenesis(state), "is expedited") + + state.Proposals[0].IsExpedited = false + voter := sdk.AccAddress(bytes.Repeat([]byte{1}, 20)) + state.Votes = Votes{NewVote(1, voter, NewNonSplitVoteOption(OptionYes))} + require.ErrorContains(t, ValidateGenesis(state), "modern tally round vote") + + state.VoteDelegationSnapshots = []VoteDelegationSnapshot{{ProposalId: 1, Voter: voter.String()}} + require.NoError(t, ValidateGenesis(state)) +} + +func TestValidateGenesisRequiresSnapshotsForFrozenElectorateVotes(t *testing.T) { + voter := sdk.AccAddress(bytes.Repeat([]byte{1}, 20)) + state := DefaultGenesisState() + state.Proposals = Proposals{{ProposalId: 1, Status: StatusVotingPeriod}} + state.Votes = Votes{NewVote(1, voter, NewNonSplitVoteOption(OptionYes))} + state.TallyElectorates = []TallyElectorate{validTallyElectorate(1)} + + err := ValidateGenesis(state) + require.ErrorContains(t, err, "has no delegation snapshot") + + state.VoteDelegationSnapshots = []VoteDelegationSnapshot{{ + ProposalId: 1, + Voter: voter.String(), + }} + require.NoError(t, ValidateGenesis(state)) +} + +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{})) require.Error(t, ValidateGenesis(nil)) } + +func validTallyElectorate(proposalID uint64) TallyElectorate { + validator := sdk.ValAddress(bytes.Repeat([]byte{2}, 20)) + return TallyElectorate{ + ProposalId: proposalID, + TotalBondedTokens: sdk.OneInt(), + TallyParams: DefaultTallyParams(), + TallyValidators: []TallyValidator{{ + Address: validator.String(), + BondedTokens: sdk.OneInt(), + DelegatorShares: sdk.OneDec(), + }}, + } +} diff --git a/sei-cosmos/x/gov/types/keys.go b/sei-cosmos/x/gov/types/keys.go index 9f590db78b..2cf7faefa0 100644 --- a/sei-cosmos/x/gov/types/keys.go +++ b/sei-cosmos/x/gov/types/keys.go @@ -2,6 +2,7 @@ package types import ( "encoding/binary" + "fmt" "time" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" @@ -37,6 +38,46 @@ 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 +// +// - 0x35: Active proposal voted on by address +// +// - 0x36: First proposal ID that does not require delegation-tracking backfill +// +// - 0x37: Delegation-tracking backfill cursor +// +// - 0x38: Latest deferred vote-delegation update sequence +// +// - 0x39: Deferred vote-delegation update +// +// - 0x3A: Deferred update index by voter +// +// - 0x3B: Last applied delegation update sequence +// +// - 0x3C: Proposal deadline awaiting tally completion +// +// - 0x3D: Last block time checked for proposal deadlines +// +// - 0x3E: Frozen tally electorate +// +// - 0x3F: Frozen electorate for deadlines between block times +// +// - 0x40: Frozen electorate for deadlines equal to a block time +// +// - 0x41: Frozen electorate selected for a proposal tally round +// +// - 0x42: Incremental tally activation marker + +// - 0x43: Legacy proposal's post-expedited modern tally round var ( ProposalsKeyPrefix = []byte{0x00} ActiveProposalQueuePrefix = []byte{0x01} @@ -46,6 +87,30 @@ var ( DepositsKeyPrefix = []byte{0x10} VotesKeyPrefix = []byte{0x20} + + TallyProgressKeyPrefix = []byte{0x30} + TallyVotesKeyPrefix = []byte{0x31} + TallyCleanupKeyPrefix = []byte{0x32} + VoteDelegationsKeyPrefix = []byte{0x33} + TallyVoteDelegationsKeyPrefix = []byte{0x34} + VoterProposalsKeyPrefix = []byte{0x35} + + VoteDelegationBackfillCutoffKey = []byte{0x36} + VoteDelegationBackfillProgressKeyPrefix = []byte{0x37} + + VoteDelegationUpdateSequenceKey = []byte{0x38} + VoteDelegationUpdatesKeyPrefix = []byte{0x39} + VoterVoteDelegationUpdatesKeyPrefix = []byte{0x3A} + VoteDelegationSnapshotRevisionKeyPrefix = []byte{0x3B} + + ProposalDeadlineKeyPrefix = []byte{0x3C} + DeadlineBoundaryBlockTimeKey = []byte{0x3D} + TallyBoundaryMetaKeyPrefix = []byte{0x3E} + GapTallyBoundaryKeyPrefix = []byte{0x3F} + ExactTallyBoundaryKeyPrefix = []byte{0x40} + ProposalTallyBoundaryKeyPrefix = []byte{0x41} + IncrementalTallyEnabledKey = []byte{0x42} + ModernTallyRoundKeyPrefix = []byte{0x43} ) var lenTime = len(sdk.FormatTimeBytes(time.Now())) @@ -107,6 +172,137 @@ func VoteKey(proposalID uint64, voterAddr sdk.AccAddress) []byte { return append(VotesKey(proposalID), address.MustLengthPrefix(voterAddr.Bytes())...) } +// VoteDelegationsKey returns the key for a vote's current delegation snapshot. +func VoteDelegationsKey(proposalID uint64, voterAddr sdk.AccAddress) []byte { + return append(append(VoteDelegationsKeyPrefix, GetProposalIDBytes(proposalID)...), address.MustLengthPrefix(voterAddr.Bytes())...) +} + +// VoterProposalsKey returns the key indexing an address's vote on an active proposal. +func VoterProposalsKey(voterAddr sdk.AccAddress, proposalID uint64) []byte { + return append(VoterProposalsKeyPrefixForAddress(voterAddr), GetProposalIDBytes(proposalID)...) +} + +// VoterProposalsKeyPrefixForAddress returns the active-proposal vote prefix for an address. +func VoterProposalsKeyPrefixForAddress(voterAddr sdk.AccAddress) []byte { + return append(VoterProposalsKeyPrefix, 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)...) +} + +// VoteDelegationBackfillProgressKey returns the key for a proposal's delegation-tracking backfill cursor. +func VoteDelegationBackfillProgressKey(proposalID uint64) []byte { + return append(VoteDelegationBackfillProgressKeyPrefix, GetProposalIDBytes(proposalID)...) +} + +// VoteDelegationUpdateKey returns the key for a deferred delegation snapshot update. +func VoteDelegationUpdateKey(sequence uint64) []byte { + return append(VoteDelegationUpdatesKeyPrefix, GetProposalIDBytes(sequence)...) +} + +// VoterVoteDelegationUpdatesKeyPrefixForAddress returns a voter's deferred-update index prefix. +func VoterVoteDelegationUpdatesKeyPrefixForAddress(voterAddr sdk.AccAddress) []byte { + return append(VoterVoteDelegationUpdatesKeyPrefix, address.MustLengthPrefix(voterAddr.Bytes())...) +} + +// VoterVoteDelegationUpdateKey returns a voter's deferred-update index key. +func VoterVoteDelegationUpdateKey(voterAddr sdk.AccAddress, sequence uint64) []byte { + return append(VoterVoteDelegationUpdatesKeyPrefixForAddress(voterAddr), GetProposalIDBytes(sequence)...) +} + +// VoteDelegationSnapshotRevisionKey returns a vote snapshot's applied-update sequence key. +func VoteDelegationSnapshotRevisionKey(proposalID uint64, voterAddr sdk.AccAddress) []byte { + return append(append(VoteDelegationSnapshotRevisionKeyPrefix, GetProposalIDBytes(proposalID)...), address.MustLengthPrefix(voterAddr.Bytes())...) +} + +// ProposalDeadlineByTimeKey returns the proposal-deadline prefix for an end time. +func ProposalDeadlineByTimeKey(endTime time.Time) []byte { + return append(ProposalDeadlineKeyPrefix, sdk.FormatTimeBytes(endTime)...) +} + +// ProposalDeadlineKey returns the deadline key for one proposal tally round. +func ProposalDeadlineKey(proposalID uint64, endTime time.Time) []byte { + return append(ProposalDeadlineByTimeKey(endTime), GetProposalIDBytes(proposalID)...) +} + +// TallyBoundaryMetaKey returns the frozen electorate key for a boundary identifier. +func TallyBoundaryMetaKey(boundaryID []byte) []byte { + return append(TallyBoundaryMetaKeyPrefix, boundaryID...) +} + +// GapTallyBoundaryKey returns the boundary index for deadlines before a block time. +func GapTallyBoundaryKey(upperTime time.Time) []byte { + return append(GapTallyBoundaryKeyPrefix, sdk.FormatTimeBytes(upperTime)...) +} + +// ExactTallyBoundaryKey returns the boundary index for deadlines equal to a block time. +func ExactTallyBoundaryKey(endTime time.Time) []byte { + return append(ExactTallyBoundaryKeyPrefix, sdk.FormatTimeBytes(endTime)...) +} + +// ProposalTallyBoundaryKey returns the selected-boundary key for a proposal. +func ProposalTallyBoundaryKey(proposalID uint64) []byte { + return append(ProposalTallyBoundaryKeyPrefix, GetProposalIDBytes(proposalID)...) +} + +// ModernTallyRoundKey returns the marker for a post-expedited tally round using deadline semantics. +func ModernTallyRoundKey(proposalID uint64) []byte { + return append(ModernTallyRoundKeyPrefix, 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())...) +} + +// TallyVoteDelegationsKeyFromVoteKey returns the delegation-snapshot key paired with an archived vote key. +func TallyVoteDelegationsKeyFromVoteKey(voteKey []byte) []byte { + kv.AssertKeyAtLeastLength(voteKey, 11) + if voteKey[0] != TallyVotesKeyPrefix[0] { + panic(fmt.Sprintf("invalid tally vote key prefix %d", voteKey[0])) + } + decodeTallyRound(voteKey[9]) + + key := append([]byte(nil), voteKey...) + key[0] = TallyVoteDelegationsKeyPrefix[0] + return key +} + +// 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 +} + +func decodeTallyRound(round byte) bool { + switch round { + case tallyRound(true): + return true + case tallyRound(false): + return false + default: + panic(fmt.Sprintf("invalid tally round %d", round)) + } +} + // Split keys function; used for iterators // SplitProposalKey split the proposal key and returns the proposal id @@ -126,6 +322,15 @@ func SplitInactiveProposalQueueKey(key []byte) (proposalID uint64, endTime time. return splitKeyWithTime(key) } +// SplitTallyCleanupKey returns the proposal and tally round encoded in a cleanup key. +func SplitTallyCleanupKey(key []byte) (proposalID uint64, expedited bool) { + kv.AssertKeyLength(key, 10) + if key[0] != TallyCleanupKeyPrefix[0] { + panic(fmt.Sprintf("invalid tally cleanup key prefix %d", key[0])) + } + return GetProposalIDFromBytes(key[1:9]), decodeTallyRound(key[9]) +} + // SplitKeyDeposit split the deposits key and returns the proposal id and depositor address func SplitKeyDeposit(key []byte) (proposalID uint64, depositorAddr sdk.AccAddress) { return splitKeyWithAddress(key) diff --git a/sei-cosmos/x/gov/types/keys_test.go b/sei-cosmos/x/gov/types/keys_test.go index b98b450620..d20a944d5a 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,31 @@ 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)) + require.Equal(t, append(append(VoterProposalsKeyPrefix, address.MustLengthPrefix(addr.Bytes())...), GetProposalIDBytes(2)...), VoterProposalsKey(addr, 2)) + require.Equal(t, []byte{0x36}, VoteDelegationBackfillCutoffKey) + require.Equal(t, append(VoteDelegationBackfillProgressKeyPrefix, GetProposalIDBytes(2)...), VoteDelegationBackfillProgressKey(2)) + require.Equal(t, append(VoteDelegationUpdatesKeyPrefix, GetProposalIDBytes(3)...), VoteDelegationUpdateKey(3)) + require.Equal(t, append(append(VoterVoteDelegationUpdatesKeyPrefix, address.MustLengthPrefix(addr.Bytes())...), GetProposalIDBytes(3)...), VoterVoteDelegationUpdateKey(addr, 3)) + require.Equal(t, append(append(VoteDelegationSnapshotRevisionKeyPrefix, GetProposalIDBytes(2)...), address.MustLengthPrefix(addr.Bytes())...), VoteDelegationSnapshotRevisionKey(2, addr)) + require.Equal(t, []byte{0x42}, IncrementalTallyEnabledKey) + require.Equal(t, append(ModernTallyRoundKeyPrefix, GetProposalIDBytes(2)...), ModernTallyRoundKey(2)) + + for _, expedited := range []bool{false, true} { + proposalID, decodedExpedited := SplitTallyCleanupKey(TallyCleanupKey(2, expedited)) + require.Equal(t, uint64(2), proposalID) + require.Equal(t, expedited, decodedExpedited) + require.Equal( + t, + TallyVoteDelegationsKey(2, expedited, addr), + TallyVoteDelegationsKeyFromVoteKey(TallyVoteKey(2, expedited, addr)), + ) + } +} diff --git a/sei-cosmos/x/staking/keeper/slash.go b/sei-cosmos/x/staking/keeper/slash.go index 49b04c4890..7d350ddd26 100644 --- a/sei-cosmos/x/staking/keeper/slash.go +++ b/sei-cosmos/x/staking/keeper/slash.go @@ -266,7 +266,8 @@ func (k Keeper) SlashRedelegation(ctx sdk.Context, srcValidator types.Validator, sharesToUnbond = delegation.Shares } - tokensToBurn, err := k.Unbond(ctx, delegatorAddress, valDstAddr, sharesToUnbond) + slashCtx := types.WithSlashDelegationModification(ctx) + tokensToBurn, err := k.Unbond(slashCtx, delegatorAddress, valDstAddr, sharesToUnbond) if err != nil { panic(fmt.Errorf("error unbonding delegator: %v", err)) } diff --git a/sei-cosmos/x/staking/types/hooks.go b/sei-cosmos/x/staking/types/hooks.go index 4c12ffd3d8..f6fdacd282 100644 --- a/sei-cosmos/x/staking/types/hooks.go +++ b/sei-cosmos/x/staking/types/hooks.go @@ -11,6 +11,11 @@ func NewMultiStakingHooks(hooks ...StakingHooks) MultiStakingHooks { return hooks } +// AddHooks appends staking hooks to this ordered hook set. +func (h *MultiStakingHooks) AddHooks(hooks ...StakingHooks) { + *h = append(*h, hooks...) +} + func (h MultiStakingHooks) AfterValidatorCreated(ctx sdk.Context, valAddr sdk.ValAddress) { for i := range h { h[i].AfterValidatorCreated(ctx, valAddr) diff --git a/sei-cosmos/x/staking/types/slash_context.go b/sei-cosmos/x/staking/types/slash_context.go new file mode 100644 index 0000000000..f4bfe5a37f --- /dev/null +++ b/sei-cosmos/x/staking/types/slash_context.go @@ -0,0 +1,20 @@ +package types + +import ( + "context" + + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" +) + +type slashDelegationModificationKey struct{} + +// WithSlashDelegationModification marks delegation changes made while applying a validator slash. +func WithSlashDelegationModification(ctx sdk.Context) sdk.Context { + return ctx.WithContext(context.WithValue(ctx.Context(), slashDelegationModificationKey{}, true)) +} + +// IsSlashDelegationModification reports whether a delegation change is part of a validator slash. +func IsSlashDelegationModification(ctx sdk.Context) bool { + marked, _ := ctx.Context().Value(slashDelegationModificationKey{}).(bool) + return marked +} diff --git a/sei-wasmd/app/app.go b/sei-wasmd/app/app.go index 5ab8ee7b48..609948c172 100644 --- a/sei-wasmd/app/app.go +++ b/sei-wasmd/app/app.go @@ -351,9 +351,8 @@ func NewWasmApp( // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks - app.stakingKeeper = *stakingKeeper.SetHooks( - stakingtypes.NewMultiStakingHooks(app.distrKeeper.Hooks(), app.slashingKeeper.Hooks()), - ) + stakingHooks := stakingtypes.NewMultiStakingHooks(app.distrKeeper.Hooks(), app.slashingKeeper.Hooks()) + app.stakingKeeper = *stakingKeeper.SetHooks(&stakingHooks) // register the proposal types govRouter := govtypes.NewRouter() @@ -413,6 +412,7 @@ func NewWasmApp( app.paramsKeeper, govRouter, ) + stakingHooks.AddHooks(app.govKeeper.StakingHooks()) // NOTE: Any module instantiated in the module manager that is later modified // must be passed by reference here. app.mm = module.NewManager( @@ -531,6 +531,7 @@ func (app *WasmApp) ProcessProposalHandler(ctx sdk.Context, req *abci.RequestPro } func (app *WasmApp) FinalizeBlocker(ctx sdk.Context, req *abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) { + gov.BeginBlocker(ctx, app.govKeeper) distr.BeginBlocker(ctx, []abci.VoteInfo{}, app.distrKeeper) slashing.BeginBlocker(ctx, []abci.VoteInfo{}, app.slashingKeeper) evidence.BeginBlocker(ctx, []abci.Misbehavior{}, app.evidenceKeeper)