Skip to content

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

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

fix(gov): bound EndBlock vote tally work#4000
codchen wants to merge 16 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✅ passedSep 2, 2026, 3:31 AM

@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.44847% with 281 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.46%. Comparing base (b117536) to head (88ad05c).
⚠️ Report is 39 commits behind head on main.

Files with missing lines Patch % Lines
sei-cosmos/x/gov/keeper/tally.go 87.36% 40 Missing and 18 partials ⚠️
sei-cosmos/x/gov/types/genesis.go 70.00% 24 Missing and 24 partials ⚠️
sei-cosmos/x/gov/abci.go 47.19% 40 Missing and 7 partials ⚠️
sei-cosmos/x/gov/keeper/delegation_updates.go 81.38% 21 Missing and 14 partials ⚠️
sei-cosmos/x/gov/keeper/electorate.go 88.97% 17 Missing and 11 partials ⚠️
sei-cosmos/x/gov/simulation/decoder.go 0.00% 28 Missing ⚠️
sei-cosmos/x/gov/keeper/vote.go 91.66% 8 Missing and 4 partials ⚠️
sei-cosmos/x/gov/types/keys.go 89.47% 5 Missing and 3 partials ⚠️
sei-cosmos/x/gov/keeper/migrations.go 91.89% 3 Missing and 3 partials ⚠️
sei-cosmos/x/gov/keeper/staking_hooks.go 78.57% 4 Missing and 2 partials ⚠️
... and 3 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4000      +/-   ##
==========================================
- Coverage   61.34%   58.46%   -2.89%     
==========================================
  Files        2163     2211      +48     
  Lines      188757   188803      +46     
==========================================
- Hits       115792   110378    -5414     
- Misses      62256    68463    +6207     
+ Partials    10709     9962     -747     
Flag Coverage Δ
sei-chain-pr 68.07% <82.44%> (?)
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 Δ
app/app.go 67.35% <100.00%> (-4.11%) ⬇️
app/legacyabci/begin_block.go 100.00% <100.00%> (ø)
sei-cosmos/x/gov/keeper/deposit.go 85.26% <100.00%> (+0.48%) ⬆️
sei-cosmos/x/gov/keeper/grpc_query.go 76.22% <100.00%> (-0.17%) ⬇️
sei-cosmos/x/gov/keeper/keeper.go 81.94% <100.00%> (+6.13%) ⬆️
sei-cosmos/x/gov/keeper/msg_server.go 85.36% <100.00%> (+2.45%) ⬆️
sei-cosmos/x/staking/keeper/slash.go 78.10% <100.00%> (+0.16%) ⬆️
sei-cosmos/x/staking/types/hooks.go 91.17% <100.00%> (+0.55%) ⬆️
sei-cosmos/x/staking/types/slash_context.go 100.00% <100.00%> (ø)
sei-wasmd/app/app.go 77.00% <100.00%> (-0.22%) ⬇️
... and 13 more

... and 390 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 consensus-critical governance tallying, staking hook ordering, chain migration, and EVM historical replay—any mismatch could alter proposal outcomes or trace results.

Overview
Introduces incremental governance tallying so proposal finalization no longer scans every vote in a single EndBlock. When enabled, tally work shares a per-block record budget (delegation backfill, deferred slash updates, vote processing, and archive cleanup), persists frozen validator electorates at voting deadlines, tracks per-vote delegation snapshots via staking hooks, and archives votes by tally round (including separate expedited vs converted regular rounds).

App / ABCI: Registers gov.BeginBlocker (gap/exact deadline electorate capture), wires GovKeeper staking hooks into the multi-hook chain, and routes legacy behavior when incremental tally is off.

Upgrade & state: Adds Migrate3to4, expanded genesis export/import (snapshots, electorates, modern tally rounds), and new proto state for boundaries, progress, and delegation updates.

EVM RPC: Historical trace/simulation replays run governance BeginBlock and can apply the v6.7 incremental-tally migration at the activation height; regression mocks clear pre-v6.7 incremental flags. Simulation rate-limit tests use a deterministic slot-holding helper.

Reviewed by Cursor Bugbot for commit 88ad05c. 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[bot]
seidroid Bot previously requested changes Aug 26, 2026

@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[bot]
seidroid Bot previously requested changes Aug 26, 2026

@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
Comment thread sei-cosmos/x/gov/keeper/tally.go Outdated
@codchen

codchen commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

@seidroid review

seidroid[bot]
seidroid Bot previously requested changes Aug 27, 2026

@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.

The incremental tally structure — persisted accumulator, round-scoped archives, merged read view, shared cleanup budget — is sound and the budget arithmetic checks out against the new tests. The remaining blocker, which Codex also flags, is that mixing a frozen validator snapshot with live delegation reads still lets post-deadline stake movement change a proposal's outcome; the PR's own TestTallyIncrementalScalesDelegationsAddedAfterSnapshot asserts a case where an account with zero stake at the deadline captures 80% of a validator's voting power.

Findings: 1 blocking | 6 non-blocking | 3 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 (tally.go:334-340), 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 simulation/decoder.go can only hex-dump the blob. A proto message would keep the module consistent and give the store diff a readable decoder. (Raised in the previous round; still unaddressed.)
  • [suggestion] The whole progress blob — including the immutable snapshot (validator address, bonded tokens, delegator shares, ~200 bytes per bonded validator) — is re-marshalled and re-written to IAVL on every block a tally advances. For the long spam-induced tally this PR is designed to absorb, that is the full validator set written per block, which partly undoes the bounding. Consider splitting the immutable snapshot from the mutable accumulator. (Raised in the previous round; still unaddressed.)
  • [suggestion] While a tally is in progress AddVote returns ErrInactiveProposal (vote.go:23) but QueryProposal still reports PROPOSAL_STATUS_VOTING_PERIOD, so a client cannot distinguish "voting closed, tallying" from a genuine status mismatch. A distinct error or exposing IsTallying on the proposal query would make this diagnosable. (Raised in the previous round; still unaddressed.)
  • 2 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] keeper/grpc_query.go:274 and keeper/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 (which does make it cheaper than before by removing the writes).

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/abci.go
@codchen

codchen commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

@seidroid review

seidroid[bot]
seidroid Bot previously requested changes Aug 27, 2026

@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.

The incremental tally machinery (persisted accumulator, round-scoped archives, merged read view, shared cleanup budget) is well-structured and the per-block budget arithmetic checks out against the new tests. The remaining blocker — which Codex also raises — is that voting power is now frozen when the vote is cast rather than at the voting deadline, which lets stake moved before voting closes capture (and nullify) a validator's snapshotted voting power.

Findings: 1 blocking | 7 non-blocking | 3 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 (keeper/tally.go:356-368), 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 simulation/decoder.go can only hex-dump the blob. A proto message would keep the module consistent and give the store diff a readable decoder. (Raised in the two previous rounds; still unaddressed.)
  • [suggestion] The whole progress blob — including the immutable part of the snapshot (validator address, bonded tokens, delegator shares, ~200 bytes per bonded validator) — is re-marshalled and re-written to IAVL on every block a tally advances (keeper/tally.go:70). For the long spam-induced tally this PR is designed to absorb, that is the full validator set written per block, which partly undoes the bounding. Consider splitting the immutable snapshot from the mutable accumulator. (Raised in the two previous rounds; still unaddressed.)
  • [suggestion] While a tally is in progress AddVote returns ErrInactiveProposal (keeper/vote.go:25) but QueryProposal still reports PROPOSAL_STATUS_VOTING_PERIOD, so a client cannot distinguish "voting closed, tallying" from a genuine status mismatch. A distinct error or exposing IsTallying on the proposal query would make this diagnosable. (Raised in the two previous rounds; still unaddressed.)
  • [suggestion] Ownership of the cleanup cursor key is split across two functions: cleanupProposalTallyVotes deletes TallyCleanupKey itself when it finishes (keeper/tally.go:408-410), and its only caller deletes or rewrites the same key immediately afterwards (keeper/tally.go:120-125). One of the two should own the key so a future change cannot leave the cursor and the archive out of step.
  • 2 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] keeper/grpc_query.go:274 and keeper/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 (which does make it cheaper than before by removing the writes it used to perform).

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

codchen commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

@seidroid review

seidroid[bot]
seidroid Bot previously requested changes Aug 27, 2026

@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.

The staking-hook approach genuinely closes the previous round's vote-time/tally-time snapshot mismatch, and the head-of-line and key-encoding threads are addressed. One blocker remains: votes recorded before this binary ships have no delegation snapshot and no VoterProposals index entry, so processTallyVotes falls back to a live delegation read against a frozen validator snapshot — reintroducing the exact power-capture attack for any proposal whose voting period spans the upgrade.

Findings: 1 blocking | 7 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 (keeper/tally.go:357,364), 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 simulation/decoder.go can only hex-dump the blob. A proto message would keep the module consistent and give the store diff a readable decoder. (Raised in the three previous rounds; still unaddressed. Codex raises it too.)
  • [suggestion] The whole progress blob — including the immutable snapshot (validator address, bonded tokens, delegator shares, ~200 bytes per bonded validator) — is re-marshalled and re-written to IAVL on every block a tally advances (keeper/tally.go:70). For the long spam-induced tally this PR is designed to absorb, that is the full validator set written per block, which partly undoes the bounding. Consider splitting the immutable snapshot from the mutable accumulator. (Raised in the three previous rounds; still unaddressed. Codex raises it too.)
  • [suggestion] While a tally is in progress AddVote returns ErrInactiveProposal (keeper/vote.go:25) but QueryProposal still reports PROPOSAL_STATUS_VOTING_PERIOD, so a client cannot distinguish "voting closed, tallying" from a genuine status mismatch. A distinct error or exposing IsTallying on the proposal query would make this diagnosable. (Raised in the three previous rounds; still unaddressed. Codex raises it too.)
  • [suggestion] Ownership of the cleanup cursor key is still split across two functions: cleanupProposalTallyVotes deletes TallyCleanupKey itself when it finishes (keeper/tally.go:409-411), and its only caller deletes or rewrites the same key immediately afterwards (keeper/tally.go:120-125). One of the two should own the key so a future change cannot leave the cursor and the archive out of step. (Raised in the previous round.)
  • [suggestion] simulation/decoder.go:53-56 hex-dumps VoteDelegationsKeyPrefix and TallyVoteDelegationsKeyPrefix values, but unlike tallyProgress these are proto VoteDelegationSnapshot records that cdc.MustUnmarshal can decode. Decoding them would make store diffs for this app-hash-breaking change readable at no cost.
  • 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] keeper/grpc_query.go:274 and keeper/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 (which does make it cheaper than before by removing the writes it used to perform).

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

codchen commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

@seidroid review

seidroid[bot]
seidroid Bot previously requested changes Aug 28, 2026

@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.

The vote-delegation snapshot design now closes the snapshot-consistency hole the last four rounds blocked on: snapshots are maintained at the staking-hook choke point, frozen at tally start, archived with the vote, and backfilled for pre-upgrade votes, with the live-read fallbacks removed. The one remaining blocker is that the new Migrate3to4 backfill is itself unbounded over the same attacker-stageable vote set this PR exists to bound, concentrating that work in the upgrade block.

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

Blockers

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

Non-blocking

  • None at the file/PR level.
  • 6 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] keeper/grpc_query.go:274 and keeper/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 (which does make it cheaper than before by removing the writes it used to perform).

Comment thread sei-cosmos/x/gov/keeper/migrations.go Outdated
Comment thread sei-cosmos/x/gov/keeper/vote.go
Comment thread sei-cosmos/x/gov/keeper/tally.go
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 Outdated
Comment thread sei-cosmos/x/gov/simulation/decoder.go Outdated
@codchen

codchen commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator Author

@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.

The automated review did not complete; see the failing AI Review check for details.

@codchen

codchen commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator Author

@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.

The automated review did not complete; see the failing AI Review check for details.

@codchen

codchen commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

@seidrod review

@codchen

codchen commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

@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.

The redesign around frozen "tally boundaries" (gap/exact electorate captures at BeginBlock/EndBlock, vote-delegation snapshots maintained at the staking-hook choke point and deferred for the slash path, plus an O(1) Migrate3to4 with a bounded per-proposal backfill) genuinely closes the snapshot-consistency and unbounded-migration blockers from the previous five rounds — voter shares and the validator electorate are now frozen from the same state, so ObservedDelegatorShares <= DelegatorShares holds by construction. What remains are the long-standing non-blocking items (JSON encoding of consensus state, whole-blob progress rewrites, cleanup-cursor ownership, tallying-status observability, simulation decoder) plus a new liveness coupling between the global delegation-update queue and every proposal's tally.

Findings: 0 blocking | 8 non-blocking | 7 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • None at the file/PR level.
  • 7 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] keeper/grpc_query.go (QueryTallyResult) and keeper/querier.go 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; this PR leaves it read-only (no writes) but adds a full bonded-validator electorate snapshot to the same path when no tally progress exists yet.

Comment thread sei-cosmos/x/gov/keeper/tally.go
Comment thread sei-cosmos/x/gov/keeper/tally.go
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 Outdated
Comment thread sei-cosmos/x/gov/simulation/decoder.go Outdated
Comment thread sei-cosmos/x/gov/genesis.go
@seidroid
seidroid Bot dismissed stale reviews from themself September 1, 2026 04:11

Superseded: latest AI review found no blocking issues.

}

func removeFutureGovernanceActivation(ctx sdk.Context, a *app.App, version string) {
if semver.Compare(version, "v6.7") < 0 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We are planning to ship this in 6.8 so this needs to change unless I have missed something.

Comment thread sei-cosmos/x/gov/types/genesis.go
@codchen

codchen commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

:seidroid review

@codchen

codchen commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

@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.

The latest revision splits the tally accumulator out of the progress blob into per-validator proto records, moves tally progress/boundaries/deferred updates onto keeper.cdc, gives CleanupTallyVotes sole ownership of the cleanup cursor, and documents the global update-queue ordering — all of which resolve the prior round's inline findings, and I found no new correctness problems in the accumulator split (load/flush/cleanup are consistent across single-block, multi-block, and expedited→regular rounds). Three non-blocking items remain: two carried over from the previous review (tallying state is not observable on queries, and the genesis electorate panic message covers three distinct failures) plus a nil-Dec panic in the new genesis validation.

Findings: 0 blocking | 6 non-blocking | 3 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] The per-validator accumulator work is not counted against MaxVotesProcessedPerBlock: flushTallyValidatorAccumulators writes one record per touched validator on every non-final block, and finishTally reads one record per bonded validator on the completing block (keeper/tally.go:518-547, 559-570). It is bounded by the validator set so it does not undo the guarantee, but since the whole point of the budget is that EndBlock work is accounted for, it is worth either folding these into the record count or noting the extra per-block term in spec/02_state.md.
  • 3 suggestion(s)/nit(s) flagged inline on specific lines.
  • 2 non-blocking pre-existing issue(s) listed below under pre-existing issues.

Pre-existing issues

  • [suggestion] keeper/grpc_query.go (QueryTallyResult) and keeper/querier.go 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; this PR leaves it write-free but adds a bonded-validator electorate snapshot to the same path when no tally progress exists yet.
  • [suggestion] ValidateGenesis -> validateTallyParams (x/gov/types/params.go:154-200) calls IsNegative/IsPositive/GT on TallyParams decimals without a nil check, so a genesis file omitting e.g. quorum panics rather than returning a validation error. Pre-existing on the base branch; the new electorate validation reuses the same function.

}
for _, electorate := range data.TallyElectorates {
proposal, found := proposalsByID[electorate.ProposalId]
if !found || proposal.Status != types.StatusVotingPeriod || proposal.VotingEndTime.After(ctx.BlockTime()) {

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] Still a single panic message covering three distinct failures — the proposal is missing, it is not in the voting period, or its voting end time is still in the future — but only the third is described, so two of the three produce a misleading halt during genesis import. (Codex raises this too.)

The third condition is also the one an operator is most likely to hit accidentally: ValidateGenesis cannot check it (validateTallyElectorates has no block time), so a state export replayed with a genesis_time earlier than an exported electorate's proposal VotingEndTime panics the node here with text pointing at the wrong thing. Splitting the conditions into distinct messages — and deciding deliberately whether the future-end-time case should be a validation error rather than a panic — would make a failed restart diagnosable.

if proposal.VotingEndTime.Before(ctx.BlockTime()) {
return false, sdkerrors.Wrapf(types.ErrInactiveProposal, "%d", proposalID)
}
if keeper.voteDelegationSnapshotFrozen(ctx, proposal) || keeper.IsVoteDelegationBackfillInProgress(ctx, proposalID) {

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] While the electorate is frozen (or a backfill is running) AddVote returns ErrInactiveProposal, but QueryProposal still reports PROPOSAL_STATUS_VOTING_PERIOD, so a client cannot distinguish "voting closed, tallying" from a genuine status mismatch — and with a multi-block tally plus a multi-block legacy backfill this window is observable for many blocks. AddDeposit (deposit.go:119-123) has the same behaviour, and nothing in grpc_query.go exposes the tallying/backfill state. A distinct error or a tallying flag on the proposal query would make it diagnosable. (Raised in the previous five rounds; Codex raises it too.)

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() {

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] delegation.Shares is a non-nullable customtype Dec, so a genesis file whose vote_delegation_snapshots[].delegations[] entry omits shares leaves it as the zero Dec with a nil *big.Int, and IsPositive() (sdk.Dec.IsPositive is (d.i).Sign() == 1) nil-derefs — validate-genesis panics instead of reporting the bad field.

The surrounding new validation already guards this: validateTallyElectorates checks BondedTokens.IsNil() and DelegatorShares.IsNil() before comparing (lines 263-274). Adding the same if delegation.Shares.IsNil() guard here keeps the new validators consistent.

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