Skip to content

feat(autobahn): Add epoch.Registry to maintain committee/stake (CON-358)#3632

Merged
wen-coding merged 15 commits into
mainfrom
wen/autobahn_epoch_registry
Jul 9, 2026
Merged

feat(autobahn): Add epoch.Registry to maintain committee/stake (CON-358)#3632
wen-coding merged 15 commits into
mainfrom
wen/autobahn_epoch_registry

Conversation

@wen-coding

@wen-coding wen-coding commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Introduces epoch.Registry as the single source of truth for committee and stake. All layers (consensus, avail, data) read from it via LatestEpoch() / EpochByIndex().
  • Moves firstBlock and genesisTimestamp out of Committee and into Epoch. Proposal now stores GlobalRange directly, making it self-contained on the wire and fixing a reproposal bug where firstBlock was double-counted.
  • Adds EpochIndex to View and AppProposal. All QC Verify methods now check epoch binding. The registry currently holds only the genesis epoch — dynamic epoch transitions are wired in follow-up PRs.

@cursor

cursor Bot commented Jun 24, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Touches consensus verification, message encoding, and availability ingestion—security-critical paths with a breaking wire format and no rolling-upgrade path noted in code.

Overview
Introduces Epoch (committee, firstBlock, genesis time, road range) and epoch.Registry as the shared source of truth; avail and data now resolve committees via Registry instead of a bare Committee.

Committee drops firstBlock / genesisTimestamp; Proposal carries an explicit global_first on the wire and stores GlobalRange() without a committee argument (fixes reproposal double-counting). View, AppProposal, and ViewSpec gain EpochIndex; QC Verify paths take *Epoch and check epoch/road binding. NewProposal no longer accepts a committee (uses ViewSpec.Epoch).

Protobuf adds required epoch_index / global_first fields (hard-reject on decode). sei-db tests/simulators updated to BuildCommitQC, parameterless GlobalRange(), and simplified NewRoundRobinElection. AGENTS.md documents proto optional-field conventions.

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

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedJul 9, 2026, 5:16 PM

@github-actions

Copy link
Copy Markdown

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedJun 24, 2026, 2:30 AM

Comment thread sei-tendermint/internal/autobahn/avail/inner.go Outdated
@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.33051% with 107 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.98%. Comparing base (e7c5eca) to head (db89d88).

Files with missing lines Patch % Lines
sei-tendermint/internal/autobahn/avail/state.go 53.96% 14 Missing and 15 partials ⚠️
sei-tendermint/internal/autobahn/data/state.go 58.62% 8 Missing and 16 partials ⚠️
...dermint/internal/autobahn/pb/autobahn.wireguard.go 31.57% 13 Missing ⚠️
sei-tendermint/internal/autobahn/epoch/registry.go 68.75% 9 Missing and 1 partial ⚠️
sei-tendermint/autobahn/types/timeout.go 57.89% 3 Missing and 5 partials ⚠️
...ei-tendermint/internal/autobahn/consensus/inner.go 66.66% 0 Missing and 5 partials ⚠️
sei-tendermint/autobahn/types/proposal.go 93.84% 1 Missing and 3 partials ⚠️
sei-tendermint/autobahn/types/app_proposal.go 76.92% 1 Missing and 2 partials ⚠️
sei-tendermint/autobahn/types/commit_qc.go 72.72% 0 Missing and 3 partials ⚠️
...ei-tendermint/internal/autobahn/consensus/state.go 78.57% 0 Missing and 3 partials ⚠️
... and 3 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3632      +/-   ##
==========================================
- Coverage   59.88%   58.98%   -0.90%     
==========================================
  Files        2283     2199      -84     
  Lines      189429   179772    -9657     
==========================================
- Hits       113431   106034    -7397     
+ Misses      65906    64434    -1472     
+ Partials    10092     9304     -788     
Flag Coverage Δ
sei-chain-pr 76.23% <77.33%> (?)
sei-db 70.41% <ø> (ø)
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-db/ledger_db/block/blocksim/block_generator.go 77.22% <100.00%> (ø)
sei-tendermint/autobahn/types/committee.go 98.48% <100.00%> (-0.09%) ⬇️
sei-tendermint/autobahn/types/epoch.go 100.00% <100.00%> (ø)
sei-tendermint/autobahn/types/opt.go 100.00% <ø> (ø)
sei-tendermint/autobahn/types/prepare_qc.go 92.85% <100.00%> (+0.85%) ⬆️
sei-tendermint/autobahn/types/testonly.go 95.26% <100.00%> (+1.51%) ⬆️
...-tendermint/internal/autobahn/avail/block_votes.go 90.90% <100.00%> (+0.43%) ⬆️
...dermint/internal/autobahn/avail/metrics/metrics.go 100.00% <100.00%> (ø)
...ternal/autobahn/consensus/persist/fullcommitqcs.go 79.01% <100.00%> (+0.26%) ⬆️
...nternal/autobahn/consensus/persist/globalblocks.go 77.96% <100.00%> (+0.18%) ⬆️
... and 18 more

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

Comment thread sei-tendermint/internal/autobahn/data/state.go
Comment thread sei-tendermint/internal/autobahn/avail/state.go Outdated
Comment thread sei-tendermint/internal/autobahn/avail/state.go Outdated
@wen-coding wen-coding changed the title feat(autobahn): introduce epoch.Registry as single source of truth for committee/stake feat(autobahn): Add epoch.Registry to maintain committee/stake (CON-358) Jun 24, 2026
Comment thread sei-tendermint/internal/autobahn/avail/state_test.go Outdated
Comment thread sei-tendermint/internal/autobahn/epoch/registry.go Outdated
@wen-coding wen-coding force-pushed the wen/autobahn_epoch_registry branch from d764443 to 705d8c9 Compare June 24, 2026 03:59
Comment thread sei-tendermint/internal/autobahn/avail/inner.go Outdated
@wen-coding wen-coding force-pushed the wen/autobahn_epoch_registry branch 2 times, most recently from ab4c67c to 9cce5d2 Compare June 24, 2026 17:32
Comment thread sei-tendermint/autobahn/types/proposal.go Outdated
@wen-coding wen-coding force-pushed the wen/autobahn_epoch_registry branch 3 times, most recently from f1f1d55 to 09e677e Compare June 25, 2026 00:46
@wen-coding wen-coding requested a review from pompon0 June 25, 2026 02:36
Comment thread sei-tendermint/autobahn/types/proposal.go Outdated
Comment thread sei-tendermint/internal/autobahn/autobahn.proto Outdated
Comment thread sei-tendermint/autobahn/types/proposal.go Outdated
Comment thread sei-tendermint/autobahn/types/proposal.go Outdated
Comment thread sei-tendermint/autobahn/types/proposal.go Outdated
Comment thread sei-tendermint/autobahn/types/testonly.go Outdated
Comment thread sei-tendermint/autobahn/types/types_test.go Outdated
Comment thread sei-tendermint/internal/autobahn/epoch/registry.go
Comment thread sei-tendermint/internal/autobahn/epoch/registry.go Outdated
Comment thread sei-tendermint/internal/autobahn/epoch/registry.go Outdated
}
for cv := range s.commitVotes.Lock() {
cv.pushVote(s.Data().Committee(), vote)
cv.pushVote(committee, vote)

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.

add a todo to make all the votes structures (commitVotes, etc.) also scoped to a single epoch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

return nil
}
qc := inner.qcs[n]
var ok bool

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.

nit: epoch lookup can be done without lock

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

vs := types.ViewSpec{CommitQC: prev, Epoch: ep}
p := types.NewAppProposal(cqc.GlobalRange().Next-1, vs.View().Index, types.GenAppHash(rng), ep.EpochIndex())
appQC = utils.Some(TestAppQC(keys, p))
} else {

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.

nit: None is the zero value, no need to set it explicitly

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

// App hash at that block.
optional bytes app_hash = 3 [(wireguard.max_size) = 32]; // required
// Epoch this proposal belongs to.
optional uint64 epoch_index = 4;

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.

// required

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

optional Timestamp timestamp = 5; // required
repeated LaneRange lane_ranges = 3 [(wireguard.max_count) = 100]; // Sorted by lane.
optional AppProposal app = 4; // optional
optional uint64 global_first = 6; // first global block number of this proposal's global range

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.

// required

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

idx,
types.RoadIndex(offset),
types.GenAppHash(rng),
0,

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.

LatestEpoch().Index?

wen-coding and others added 13 commits July 9, 2026 09:48
- Introduce epoch.Registry with genesis epoch and EpochByIndex/LatestEpoch/VerifyInWindow
- Add Epoch type (index, roads, firstBlock, timestamp, committee)
- Wire Registry through avail/consensus/data State constructors and run loops
- BuildCommitQC takes *Epoch; WaitForLaneQCs iterates epoch committee lanes
- View gains EpochIndex field; View.Less includes EpochIndex in ordering
- global_first proto field stores GlobalRange.First directly (not epoch offset)
- Remove GlobalFirst() method from Proposal; enforce epoch binding via FullProposal.Verify
- Various reviewer feedback: rename registryState field mu→state, add TestViewLess,
  fix NextGlobalBlock comment, add TestWaitForLaneQCs_OnlyReturnsCommitteeLanes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…verage in runPersist

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…emove redundant lane checks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… var, fix error wrapping

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…poch-scoped votes TODO

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
makeEpoch now delegates to GenEpochWithCommittee so epoch index,
RoadRange.First, firstBlock, and timestamp are all randomized,
preventing tests from silently passing on zero values.

GenEpochWithCommittee guarantees Last >= First+10 (was +1) so
callers can safely use First+1 as a view index.

The two TimeoutQC tests build a CommitQC at RoadRange.First and
use First+1 as the timeout view index, satisfying the
NextIndexOpt(prev)==view.Index invariant checked by Verify.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CommitQCAt(ep, keys) creates a CommitQC at ep.RoadRange().First signed
by the given keys. GenCommitQC(rng) now delegates to it instead of
generating random unsigned bytes.

committee_test.go: use CommitQCAt for TimeoutQC prev; use ep.EpochIndex()
in TestAppQCVerifyChecksWeight instead of hardcoded 0.

conv_test.go: use commitQC.Proposal().EpochIndex() instead of hardcoded 0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…vention

Per pompon0's review request: annotate proto fields with // required or
// optional to clarify intent, since all fields use the optional keyword
for presence detection and wireguard bounds checking. Required fields are
nil-checked at the boundary (constructor and proto Decode) and trusted
everywhere else.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pointer fields are required by default per AGENTS.md convention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ded 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@wen-coding wen-coding force-pushed the wen/autobahn_epoch_registry branch from 38db0ce to 3122be2 Compare July 9, 2026 16:50

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

A large but well-structured refactor introducing epoch.Registry as the single source of truth for committee/stake, moving firstBlock/genesisTimestamp into Epoch, adding EpochIndex to View/AppProposal, and threading epoch-binding checks through all QC Verify methods. Test coverage is extensively updated and the new wire fields are correctly covered by the signed message hash; no blocking issues found.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Both second-opinion review files are empty: codex-review.md (OpenAI Codex) and cursor-review.md (Cursor) produced no output, so this synthesis reflects only the primary pass.
  • epoch/registry_test.go only exercises EpochByIndex (genesis-found and unknown-not-found). VerifyInWindow, LatestEpoch, FirstBlock, and GenesisTimestamp are only covered indirectly through other packages — consider adding direct unit tests, especially for VerifyInWindow's fn-accept/reject and returned-epoch-slice contract.
  • Security model note (verified OK, no action needed): the newly wire-transmitted Proposal.global_first and epoch_index are trusted-but-verified — FullProposal.Verify checks GlobalRange().First == vs.NextGlobalBlock() and data.insertQC enforces contiguity against nextQC, while quorum-signed CommitQCs rely on honest validators having run FullProposal.Verify before signing. Both fields are included in the signed hash via ProposalConv/ViewConv/AppProposalConv.Encode, so field substitution / signature replay is not possible.
  • The many TODO: ... once epoch transitions are wired up comments (avail, consensus inner/state, registry.VerifyInWindow, runPersist cross-epoch lane union) are appropriate for this staged rollout since the registry is genesis-only and any non-zero epoch_index message is rejected by EpochByIndex; worth tracking these as a follow-up checklist so none are missed when AddEpoch lands.
  • Decode paths hard-reject messages missing global_first / epoch_index (documented as acceptable because autobahn is pre-production with no rolling-upgrade path) — confirm no persisted WAL / snapshot written by a pre-change binary must be read back, otherwise restart would fail validation.

…mitQC

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

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

A large, mechanical, well-tested refactor that introduces epoch.Registry as the single source of truth for committee/stake and threads Epoch through all QC/proposal Verify paths; it also fixes a reproposal global-range double-count bug. No blocking correctness or security issues found; single-epoch invariants and TODOs consistently guard the not-yet-wired multi-epoch paths.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Both configured second-opinion reviews produced no output: ./codex-review.md (OpenAI Codex) and ./cursor-review.md (Cursor) were empty, and ./REVIEW_GUIDELINES.md was empty, so this synthesis relies solely on my own pass.
  • Wire-format change is intentionally breaking: Proposal.Decode hard-rejects messages lacking global_first, and View/AppProposal now require epoch_index. This is documented in-code as acceptable because autobahn is pre-production with no rolling-upgrade path — confirm no persisted WALs or in-flight peers from older builds need to be drained before deploy.
  • epoch.Registry.VerifyInWindow returns a []*types.Epoch specifically so callers 'can skip re-verification', but its two callers (avail.State.PushBlock / PushVote) discard the slice (if _, err := ...). This is forward-looking API surface with no current effect — fine, but worth a note so the intended optimization isn't forgotten.
  • NewRegistry returns (*Registry, error) yet can never return a non-nil error; likewise the per-epoch weight counting in avail (pushVote uses inner.epoch while VerifyInWindow verified against LatestEpoch) is only safe because a single epoch exists today — both are explicitly TODO-tracked for the follow-up epoch-transition PRs.
  • In persist FullCommitQC/GlobalBlock persisters, firstBlock is reassigned to loaded[0]'s first block on restart (previously it was the genesis FirstBlock); since LoadedFirst() consults loaded before ConsumeLoaded empties it, the NewState skipTo path is unaffected, but the post-consume value now differs from prior behavior — low risk, worth a glance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

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

A large, cleanly-structured refactor introducing epoch.Registry and moving firstBlock/genesisTimestamp into Epoch, with EpochIndex binding added to consensus messages and QC verification and a self-contained on-wire GlobalRange (fixing a reproposal double-count bug). Extensive test updates accompany the change and the single-genesis-epoch assumption is consistently documented with TODOs; no blocking correctness issues found.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Second-opinion inputs were empty: both ./codex-review.md and ./cursor-review.md contained no content, so this review reflects only this pass. ./REVIEW_GUIDELINES.md was also empty, so no repo-specific standards were applied.
  • Consensus wire-format break: the new proto fields (View.epoch_index, Proposal.global_first, AppProposal.epoch_index) are treated as required and Decode hard-rejects any message lacking them (e.g. proposal.go returns global_first: missing). The in-code rationale (autobahn is pre-production, no rolling-upgrade path) is reasonable, but this is worth an explicit call-out given the PR carries the non-app-hash-breaking label yet changes the consensus message wire format — any node/persisted message from before this PR will fail to decode.
  • Divergent epoch-resolution patterns: consensus/inner.go pushCommitQC verifies against the receiver's current epoch (i.epoch), while avail/state.go PushCommitQC and data/state.go insertQC resolve the epoch from the QC via Registry.EpochByIndex(qc.Proposal().EpochIndex()). Both are correct under today's single-epoch invariant, but the two approaches will need reconciling when dynamic epoch transitions land (already noted by several TODOs).
  • epoch.Registry.VerifyInWindow returns []*types.Epoch so callers can skip re-verification of already-checked epochs, but the current callers (avail PushBlock/PushVote) discard the slice with _. Harmless today; just an unused affordance until multi-epoch is wired up.
  • data/state.go NewState WAL-load path and PushBlock now dereference inner.qcs[n] to resolve the block's epoch; this relies on the invariant that every loaded/awaited block number is covered by a present QC (WAL reconcile + gap checks + WaitUntil(n < inner.nextQC)). The invariant appears to hold, but a defensive nil-check or comment reaffirming it would guard against a future regression turning it into a nil-deref panic.

@wen-coding wen-coding added this pull request to the merge queue Jul 9, 2026
Merged via the queue into main with commit d9c188d Jul 9, 2026
96 of 98 checks passed
@wen-coding wen-coding deleted the wen/autobahn_epoch_registry branch July 9, 2026 18:18
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