Skip to content

(Autobahn) Share vote aggregation (CON-423) - #4152

Merged
wen-coding merged 2 commits into
mainfrom
wen/autobahn-share-vote-aggregation
Sep 12, 2026
Merged

(Autobahn) Share vote aggregation (CON-423)#4152
wen-coding merged 2 commits into
mainfrom
wen/autobahn-share-vote-aggregation

Conversation

@wen-coding

Copy link
Copy Markdown
Contributor

Summary

  • Fold prepare/commit/timeout vote aggregation into one weighted aggregator and one phase adapter. The three phases still differ in bucket key (vote hash vs view), QC constructor, and quorum getter; they no longer duplicate the latest-vote-per-key / monotonic-QC loop.
  • Shared tests cover below-quorum, quorum emit, ignore equal/stale, and replace-at-newer-view. Each phase keeps a QC-validity test plus the property that is actually its own: conflicting prepare/commit proposals do not form a QC; timeout votes with differing prepare-QC payloads still do. Same-view votes after a QC at a non-genesis view leave the published QC unchanged.
  • Ingest is unchanged: verify outside the lock, then insert. Published QC behavior matches main (qc.Load(), then New*QC, then Store).

Quorum wiring

PrepareQuorum, CommitQuorum, and TimeoutQuorum are already the same function on main (totalWeight - Faulty(); prepare/timeout/app return CommitQuorum()). The phase tests size the committee with CommitQuorum() and do not pin which getter is wired into each adapter.

If those thresholds diverged later, swapping the wrong getter would not always fail this suite (a too-strict aggregator in particular would still form a QC). We are not adding two-sided per-phase quorum tests for that: we do not expect the getters to diverge in the near future. QC.Verify would still catch the too-loose direction.

Test plan

  • GOWORK=off go test github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/consensus -count=1
  • make fmtcheck
  • CI unit / race on this package

Made with Cursor

The three phases ran the same latest-vote-per-key, weighted-bucket, monotonic-QC
loop in triplicate. Fold that into one aggregator so quorum and replacement are
tested once, and each phase only keeps the properties that are actually its own.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 65.42%. Comparing base (fd07ebd) to head (eee844d).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4152      +/-   ##
==========================================
- Coverage   66.56%   65.42%   -1.15%     
==========================================
  Files        2196     2081     -115     
  Lines      169150   157378   -11772     
==========================================
- Hits       112602   102964    -9638     
+ Misses      56407    54273    -2134     
  Partials      141      141              
Flag Coverage Δ
sei-chain-pr 90.75% <100.00%> (?)
sei-db 74.50% <ø> (ø)
sei-db-state-db ?

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

Files with missing lines Coverage Δ
...dermint/internal/autobahn/consensus/phase_votes.go 100.00% <100.00%> (ø)
...ei-tendermint/internal/autobahn/consensus/state.go 91.09% <100.00%> (ø)
...int/internal/autobahn/consensus/vote_aggregator.go 100.00% <100.00%> (ø)

... and 117 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.

@github-actions

github-actions Bot commented Sep 11, 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 11, 2026, 6:31 PM

@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 three phase-vote aggregators are correctly folded into one generic voteAggregator plus a phaseVotes adapter — the latest-vote-per-key pruning, bucket-weight accounting, quorum check, and QC-monotonicity guard all preserve main's semantics, and the pushVotepushVerifiedVote rename matches what state.go actually guarantees. Two non-blocking notes: the bucket slice is now materialized before the publication guard, and the "QC advances to a newer view" branch lost its test coverage when timeout_votes_test.go was replaced.

Findings: 0 blocking | 2 non-blocking | 2 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • None at the file/PR level.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

return utils.None[[]V]()
}

votes := make([]V, 0, len(set.votes))

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] The bucket copy now happens before the publication guard, which is an ordering change from main. Previously each phase checked weight >= quorum and the QC-view guard (!old.View().Less(view)) before building the votes slice, so votes arriving after a QC was published at that view cost O(1). Now the aggregator materializes the whole bucket on every such vote and pushVerifiedVote (phase_votes.go:52) immediately discards it. For an equal-weight committee that receives all n votes for a view, the copying work under the phase mutex goes from O(n) to O(n²).

Correctness is unaffected, but the fix is local: have pushVote return the bucket (e.g. utils.Option[*voteSet[V]]) or a bool, and let pushVerifiedVote materialize the slice only on the path where it actually calls newQC.

return
}
// Construct a QC from the votes.
if old, ok := p.qc.Load().Get(); ok && !ph.qcView(old).Less(ph.view(vote)) {

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] The branch where this guard passes — an already-published QC being replaced by one at a later view — is no longer covered. TestTimeoutVotes_ReplacesOlderVoteAndAdvancesQC exercised exactly that (QC at view0, quorum migrates to view1, published QC becomes view1) and was deleted with timeout_votes_test.go. Its replacement, TestVoteAggregator_ReplacesOlderVotesAndEmitsAtNewView, stops at the aggregator and never reaches phaseVotes; every phaseVotes test either publishes the first QC from None or asserts the same-view no-op.

That leaves a liveness-critical property untested: a mis-wired qcView (returning a view that never compares as less) would stall the published QC at its first view and still pass this suite. Restoring one phase-level test that pushes a quorum at view0, then a quorum at view0.Next(), and asserts the published QC advances would close it — and would let the PR's "existing tests pass unchanged" claim hold for the QC-publication logic, not just for the aggregator.

The aggregator already covered replacement; this hits qc.Load() so a stuck
qcView cannot stall the published QC at its first view.

Co-authored-by: Cursor <cursoragent@cursor.com>

@shemnon shemnon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

Non-blocking design question, why a struct of function pointers and not an interface with three impls? Is this for performance or code size?

@wen-coding

Copy link
Copy Markdown
Contributor Author

Non-blocking design question, why a struct of function pointers and not an interface with three impls? Is this for performance or code size?

@shemnon neither. The three vote types run the same loop; they just pull key/view/bucket off the message differently (prepare/commit bucket by hash, timeout by view). A struct of those extractors was the smallest way to share the loop without three new types.

@wen-coding
wen-coding added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit ba432d8 Sep 12, 2026
71 checks passed
@wen-coding
wen-coding deleted the wen/autobahn-share-vote-aggregation branch September 12, 2026 10:15
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.

3 participants