Skip to content

fix(gov): bound EndBlock vote tally work - #4000

Open
codchen wants to merge 4 commits into
mainfrom
codex/bound-governance-tally-work
Open

fix(gov): bound EndBlock vote tally work#4000
codchen wants to merge 4 commits into
mainfrom
codex/bound-governance-tally-work

Conversation

@codchen

@codchen codchen commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Describe your changes and provide context

Governance proposal finalization previously iterated and deleted every stored vote in one EndBlock, allowing a proposal with many zero-power voters to concentrate unbounded work in its expiry block.

This change:

  • shares a 1,000 vote-record budget across tallying and archived-vote cleanup in each EndBlock
  • persists tally results, total voting power, tally parameters, a bonded-validator snapshot, and a vote cursor between blocks
  • moves processed votes into round-specific archives and cleans those archives incrementally
  • rejects new votes after incremental tallying begins
  • isolates expedited and converted regular tally rounds
  • includes both archived and pending votes when exporting an unfinished tally
  • documents the new governance state and adds multi-block regression coverage

Proposal execution, deposit handling, hooks, and final events remain deferred until every vote has been processed.

Testing performed to validate your change

  • go test ./sei-cosmos/x/gov/... -count=1
  • go test -race ./sei-cosmos/x/gov ./sei-cosmos/x/gov/keeper -count=1
  • go vet ./sei-cosmos/x/gov/...
  • make fmtcheck with golangci-lint v2.8.0 rebuilt for Go 1.25
  • golangci-lint v2.8.0 scoped to ./sei-cosmos/x/gov/... — 0 issues

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedAug 26, 2026, 2:09 PM

@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.03226% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.04%. Comparing base (b117536) to head (10412a5).

Files with missing lines Patch % Lines
sei-cosmos/x/gov/keeper/tally.go 90.62% 13 Missing and 8 partials ⚠️
sei-cosmos/x/gov/keeper/vote.go 83.33% 7 Missing and 3 partials ⚠️
sei-cosmos/x/gov/simulation/decoder.go 0.00% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4000      +/-   ##
==========================================
- Coverage   61.29%   58.04%   -3.25%     
==========================================
  Files        2163     2206      +43     
  Lines      188757   185304    -3453     
==========================================
- Hits       115693   107556    -8137     
- Misses      62338    67924    +5586     
+ Partials    10726     9824     -902     
Flag Coverage Δ
sei-chain-pr 60.98% <89.03%> (?)
sei-db 69.80% <ø> (ø)
sei-db-state-db ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
sei-cosmos/x/gov/abci.go 100.00% <100.00%> (ø)
sei-cosmos/x/gov/genesis.go 91.83% <100.00%> (+4.60%) ⬆️
sei-cosmos/x/gov/keeper/grpc_query.go 76.22% <100.00%> (-0.17%) ⬇️
sei-cosmos/x/gov/types/keys.go 93.33% <100.00%> (+1.66%) ⬆️
sei-cosmos/x/gov/simulation/decoder.go 3.22% <0.00%> (-0.23%) ⬇️
sei-cosmos/x/gov/keeper/vote.go 89.84% <83.33%> (-2.16%) ⬇️
sei-cosmos/x/gov/keeper/tally.go 90.59% <90.62%> (-6.18%) ⬇️

... and 380 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codchen
codchen marked this pull request as ready for review August 26, 2026 02:53
@cursor

cursor Bot commented Aug 26, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes core governance tallying, vote storage, and multi-block finalization semantics; incorrect behavior could alter proposal outcomes or stall chain progress under load.

Overview
Governance EndBlock no longer tallies and deletes every vote in one shot when voting ends. It now spends at most 1,000 vote records per block (MaxVotesProcessedPerBlock), with 100 reserved up front for archive cleanup so tallies cannot starve deletion.

Incremental tallying (TallyIncremental) snapshots bonded validators and tally params, walks votes with a persisted cursor, and archives processed votes under round-specific keys (expedited vs regular). Proposal status, deposits, execution, and events stay deferred until the tally completes; AddVote is rejected while a tally is in progress. Delegator power is scaled when live delegations exceed the validator snapshot so results stay bounded if staking changes mid-tally.

Vote queries read pending plus archived votes via visibleVotesStore. Genesis seeds an accumulator for voting-period proposals already past VotingEndTime on import so exports still include all votes but do not reopen voting. Spec and regression tests cover multi-block EndBlock, expedited conversion, and export/import.

Reviewed by Cursor Bugbot for commit 10412a5. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7c3f7c7. Configure here.

Comment thread sei-cosmos/x/gov/keeper/tally.go Outdated
Comment thread sei-cosmos/x/gov/keeper/vote.go

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spreading the gov tally across blocks is the right fix for the unbounded EndBlock work, but freezing the validator/bonded-token snapshot at tally start while still reading delegations live in later blocks makes the tally arithmetic unsound (deductions can exceed snapshot shares, yielding negative validator power). Several secondary gaps — archived votes invisible to vote queries, tally progress dropped on genesis export, and an unbounded (now heavier) Tally on the public query path — should also be addressed.

Findings: 1 blocking | 7 non-blocking | 5 posted inline

Blockers

  • None at the file/PR level.
  • 1 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • [suggestion] Cleanup only receives the budget left over after tallying (CleanupTallyVotes(ctx, remainingVotes) in abci.go). While a large tally repeatedly consumes the full 1,000-record budget, archived rounds from earlier proposals are never reclaimed and the 0x31 archive grows. Consider reserving a slice of the budget for cleanup, or draining cleanup before tallying, so archive removal cannot be starved indefinitely.
  • [suggestion] Test gap: there is no multi-block regression test for the expedited→regular conversion path (an expedited tally that spans several blocks, converts, then re-tallies with both round archives live), nor for a delegation change occurring between two blocks of the same tally. Both are the cases the new persisted-snapshot design actually changes; the current tests only exercise single-round, single-block-delta scenarios.
  • 4 suggestion(s)/nit(s) flagged inline on specific lines.
  • 1 non-blocking pre-existing issue(s) listed below under pre-existing issues.

Pre-existing issues

  • [suggestion] grpc_query.go:275 / querier.go:177 compute a provisional tally for a voting-period proposal by calling Tally, which iterates every vote record. This is already an unbounded, publicly reachable query independent of this PR.

Comment thread sei-cosmos/x/gov/keeper/tally.go Outdated
Comment thread sei-cosmos/x/gov/keeper/tally.go Outdated
Comment thread sei-cosmos/x/gov/keeper/vote.go
Comment thread sei-cosmos/x/gov/genesis.go Outdated
Comment thread sei-cosmos/x/gov/spec/02_state.md Outdated
@codchen

codchen commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the remaining review-summary items in d67cd15:

  • cleanup receives the first 100 operations of the shared 1,000-record budget, so an unfinished tally cannot starve older archive cleanup;
  • added multi-block expedited-to-regular coverage with both round archives live;
  • added delegation-change coverage, active-archive query and pagination coverage, read-only tally-query coverage, and genesis export/import restart-lock coverage.

Validation completed:

  • go test ./sei-cosmos/x/gov/... -count=1
  • go test -race ./sei-cosmos/x/gov/... -count=1
  • make fmtcheck GOLANGCI_LINT=/tmp/codex-golangci-lint-2.8.0-go1.25
  • scoped golangci-lint v2.8.0: 0 issues

@masih

masih commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

@seidroid review

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spreading the gov tally across blocks with a persisted accumulator, round-scoped vote archives, a merged read view, and a shared cleanup budget is a solid structure, and the previous round's findings on vote visibility, query mutation, genesis round-trip, cleanup starvation, and spec layout are genuinely addressed. The remaining blocker is the one Codex also flags: mixing a frozen validator snapshot with live delegation reads is still unsound — the greedy per-voter clamp bounds each validator's total but makes a legitimate voter's counted power depend on other delegators' post-deadline stake movements and on vote-key ordering, and can silently drop their vote entirely.

Findings: 1 blocking | 6 non-blocking | 2 posted inline

Blockers

  • None at the file/PR level.
  • 1 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • [suggestion] tallyProgress is consensus-critical state but is serialized with encoding/json, while every other record in x/gov goes through keeper.cdc. The app hash now depends on Go's JSON encoder for sdk.Dec/sdk.Int/proto structs, and the simulation decoder can only hex-dump the blob. A proto message would keep the module consistent and give the store diff a readable decoder.
  • [suggestion] The whole progress blob is re-encoded and re-written to IAVL on every block a tally advances, including the immutable part of the snapshot (validator address, bonded tokens, snapshot shares — roughly 200 bytes per bonded validator). For a long spam-induced tally that is the full validator set written per block. Consider splitting the immutable snapshot from the mutable accumulator, or keying deductions per validator, so only what changed is rewritten.
  • [suggestion] if !complete { return true } in EndBlocker stops IterateActiveProposalsQueue entirely, so a single vote-spammed proposal defers finalization of every other expired proposal until it drains at ~900 records/block. Work per block is correctly bounded, but head-of-line blocking across proposals is a new liveness property worth stating in the spec (or working around by continuing to the next proposal when the leader is out of budget).
  • [suggestion] While a tally is in progress AddVote returns ErrInactiveProposal, but QueryProposal still reports PROPOSAL_STATUS_VOTING_PERIOD, so a client cannot distinguish "voting closed, tallying" from a genuine status mismatch. Consider a distinct error or exposing IsTallying on the proposal query.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.
  • 1 non-blocking pre-existing issue(s) listed below under pre-existing issues.

Pre-existing issues

  • [suggestion] grpc_query.go:274 and querier.go:177 compute a provisional tally for a voting-period proposal by calling Tally, which iterates every vote record with no bound. This is a publicly reachable unbounded query independent of this PR (and this PR makes it cheaper than the previous revision by removing the writes).

Comment thread sei-cosmos/x/gov/keeper/tally.go Outdated
panic(fmt.Sprintf("invalid tally cleanup key length %d", len(key)))
}
proposalID = types.GetProposalIDFromBytes(key[1:9])
switch key[9] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This switch is a second, independent copy of the round-byte encoding defined by types.tallyRound (types/keys.go:140), and it reads inverted (0 → expedited) relative to the boolean it decodes. Because tallyRound is unexported, the keeper cannot reuse it, so flipping the encoding in types would leave this decode silently wrong — cleanup would then delete the other round's archive. Put the decode next to the encode (e.g. an exported types.SplitTallyCleanupKey) so the mapping lives at one choke point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants