Skip to content

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

Open
wen-coding wants to merge 3 commits into
mainfrom
wen/autobahn_epoch_registry
Open

feat(autobahn): Add epoch.Registry to maintain committee/stake (CON-358)#3632
wen-coding wants to merge 3 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
Large consensus-type and wire-format change with new mandatory protobuf fields and shifted verification APIs; incorrect epoch binding or global-range handling could break message acceptance across nodes.

Overview
Adds Epoch (road range, firstBlock, genesis time, committee) and epoch.Registry as the local source of truth; consensus, avail, and data now resolve committees via LatestEpoch() / EpochByIndex() instead of carrying a bare Committee.

Committee drops firstBlock and genesisTimestamp; CommitQC / Proposal.GlobalRange() no longer take a committee argument—proposals carry an explicit global_first on the wire and store a full GlobalRange, which fixes reproposal cases where firstBlock was applied twice.

View and AppProposal gain required epoch_index; ViewSpec requires Epoch for NextGlobalBlock() / NextTimestamp(). QC and full-proposal Verify paths check epoch and road binding (PrepareQC, CommitQC, TimeoutQC, FullProposal, etc.).

Protobuf decode hard-rejects missing epoch_index / global_first. Ledger DB tests and blocksim use BuildCommitQC and parameterless GlobalRange(); NewRoundRobinElection no longer takes genesis args.

Reviewed by Cursor Bugbot for commit 4cae81a. 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, 4:33 AM

@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 75.37797% with 114 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.96%. Comparing base (b118dc3) to head (4cae81a).

Files with missing lines Patch % Lines
sei-tendermint/internal/autobahn/avail/state.go 53.22% 14 Missing and 15 partials ⚠️
sei-tendermint/internal/autobahn/data/state.go 59.32% 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/testonly.go 83.01% 9 Missing ⚠️
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/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 4 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3632      +/-   ##
==========================================
- Coverage   59.85%   58.96%   -0.90%     
==========================================
  Files        2278     2194      -84     
  Lines      189235   179590    -9645     
==========================================
- Hits       113266   105892    -7374     
+ Misses      65888    64406    -1482     
+ Partials    10081     9292     -789     
Flag Coverage Δ
sei-chain-pr 75.86% <75.37%> (?)
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%) ⬆️
...-tendermint/internal/autobahn/avail/block_votes.go 90.90% <100.00%> (+0.43%) ⬆️
...ternal/autobahn/consensus/persist/fullcommitqcs.go 79.01% <100.00%> (+0.26%) ⬆️
...nternal/autobahn/consensus/persist/globalblocks.go 77.96% <100.00%> (+0.18%) ⬆️
...int/internal/autobahn/consensus/persisted_inner.go 97.59% <100.00%> (-0.09%) ⬇️
sei-tendermint/internal/autobahn/data/testonly.go 50.61% <100.00%> (-10.71%) ⬇️
... and 17 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
@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

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

Well-structured refactor that introduces epoch.Registry and bakes firstBlock/epoch_index into Proposal so GlobalRange() returns absolute block numbers and messages are self-contained on the wire. No confirmed blocking bugs; test coverage is strong. One latent epoch-index footgun in ViewSpec.View() (raised by Codex) is worth hardening before dynamic epochs land, plus a few minor doc/coverage notes.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Cursor produced no second-opinion output (cursor-review.md was empty), and REVIEW_GUIDELINES.md from the base branch was empty — this review proceeded without repo-specific guidelines or a Cursor pass.
  • epoch/registry_test.go only covers EpochByIndex. Consider adding coverage for VerifyInWindow, LatestEpoch, FirstBlock, and GenesisTimestamp, since these are now on the hot path for all committee lookups (avail/data/consensus).
  • Doc mismatch on the new Proposal.global_first field: autobahn.proto describes it as "first global block number of this proposal's global range" while the generated pb.go comment says "first global block number of the epoch". The proto wording is the accurate one (for non-first proposals global_first == CommitQC.GlobalRange().Next, not the epoch's first block); the generated comment is misleading. Regenerate/realign the comment. Also AppProposal.epoch_index lacks the "// required" annotation the other fields carry, even though Decode rejects it when nil.
  • data/state.go PushBlock now reads the epoch under the lock, verifies outside the lock, then re-acquires the lock to insert — a good pattern, but it relies on inner.qcs[n] still being present on the second lock (guarded by the n < inner.first re-check). Worth a brief comment noting nextQC is monotonic so qcs[n] cannot disappear for first <= n < nextQC.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

return view
}
return View{Index: idx, Number: 0}
return View{Index: idx, Number: 0, EpochIndex: vs.Epoch.EpochIndex()}

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] Latent epoch-index bug (also flagged by Codex). The early-return branch above (if view := NextViewOpt(vs.TimeoutQC); view.Index == idx { return view }) returns the view from NextViewOpt, which for a None TimeoutQC yields View{Index:0, Number:0, EpochIndex:0}. So when idx == 0 and there is no TimeoutQC, View() returns EpochIndex: 0 regardless of vs.Epoch.EpochIndex(), and View.Verify(ep) would then reject it for any non-zero epoch. Today this is masked by the road-range invariant (a non-genesis epoch's first road index is > 0, so idx == 0 only occurs for epoch 0), so it is not currently triggerable — but it silently depends on that invariant and will break when dynamic epochs are wired up. Suggest stamping the epoch index unconditionally, e.g. set view.EpochIndex = vs.Epoch.EpochIndex() before returning in the branch above (the value from a real TimeoutQC should already match vs.Epoch).

@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, internally consistent refactor that introduces epoch.Registry, bakes firstBlock/epochIndex into the wire Proposal/View/AppProposal so GlobalRange() returns absolute numbers, and routes all committee lookups through the registry. The change is well-covered by updated tests (including a new epoch-binding test and a reproposal regression test) and I found no blocking correctness or security issues; a few non-blocking observations follow.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Second-opinion passes gave no substantive coverage: codex-review.md reports "No material findings" but explicitly says it was static-only (Go 1.25.6 toolchain unavailable, network blocked — no tests run), and cursor-review.md is empty. This synthesis review is therefore effectively the sole gate; REVIEW_GUIDELINES.md is also empty, so no repo-specific standards were applied.
  • Wire compatibility is intentionally broken (Proposal/View/AppProposal decode hard-reject missing global_first/epoch_index). This is called out in-code as acceptable because Autobahn is pre-production with no rolling-upgrade path — fine, but worth confirming no persisted PersistedInner/AppQC state from older binaries needs to survive a restart, since those decoders now also require the new fields.
  • Deferred multi-epoch correctness is well-flagged but real: consensus/inner.go newInner assumes LatestEpoch() (TODO notes a restart after an epoch transition would fail validation), and Registry.VerifyInWindow only verifies against the latest epoch (TODO to expand to neighbor epochs). Until dynamic epochs land these are inert (only genesis epoch 0 exists, and non-zero epochIndex messages are correctly rejected), but they are latent bugs to resolve before enabling epoch transitions.
  • epoch.NewRegistry returns (*Registry, error) yet never produces a non-nil error; callers must handle a nil error today. Harmless/future-proofing, but slightly misleading — consider documenting that the error is reserved for future validation.
  • Registry read accessors use the for s := range r.mu.RLock() { return ... }; panic("unreachable") iterator pattern; correctness depends on the RWMutex iterator yielding exactly once. It matches the surrounding codebase idiom, so no change requested — just noting the reliance.

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

Large, mostly-mechanical refactor that introduces epoch.Registry and bakes firstBlock/epochIndex into proposals/QCs so GlobalRange() returns absolute numbers and messages are self-contained on the wire. The change is internally consistent, removes the reproposal double-counting bug, adds epoch-binding verification throughout, and is extensively covered by updated tests; I found no correctness blockers, only minor code-quality notes.

Findings: 0 blocking | 4 non-blocking | 1 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Wire-format change is intentionally hard-breaking: View.epoch_index, Proposal.global_first, and AppProposal.epoch_index are all required on decode with no rolling-upgrade path (per the code comment, acceptable because autobahn is pre-production). Worth calling out for release/coordination awareness — all nodes must upgrade in lockstep and any persisted pre-change WAL/state is unreadable.
  • Forward-looking scaffolding currently has no live effect: epoch.Registry.NewRegistry always returns a nil error, and Registry.VerifyInWindow returns []*types.Epoch that every caller discards (if _, err := ...). Since the registry only ever holds the genesis epoch (index 0), all the new EpochByIndex(...)/epoch-binding checks effectively reduce to epochIndex == 0 today. This matches the stated intent of wiring the read side first, but reviewers should be aware the multi-epoch paths are untested until AddEpoch lands (several TODOs flag this, e.g. consensus/inner.go restart-after-transition and avail per-epoch vote weighting).
  • Second-opinion inputs: Codex reported "No material findings" (tests not runnable — Go 1.25.6 toolchain download blocked in its sandbox); the Cursor review file and REVIEW_GUIDELINES.md were both empty.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

// with a block not finalized by prev.
func (s *State) WaitForLaneQCs(
ctx context.Context, prev utils.Option[*types.CommitQC],
ctx context.Context, ep *types.Epoch, prev utils.Option[*types.CommitQC],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] The ep *types.Epoch parameter is unused: the body iterates inner.blocks and resolves the committee via inner.laneQC(...) (which reads i.epoch), never touching ep. Either use ep (e.g. to select the committee/lane set for the requested epoch) or drop the parameter to avoid a misleading signature. Callers currently pass vs.Epoch / registry.LatestEpoch(), implying it matters.

@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 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit 3d7872b. Configure here.

Comment thread sei-tendermint/autobahn/types/proposal.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.

A large but mostly mechanical refactoring that introduces epoch.Registry and bakes firstBlock/epochIndex/globalFirst into the wire messages, threading an *Epoch through all autobahn verification paths; it also fixes a genuine reproposal firstBlock double-counting bug and adds extensive test coverage. Core epoch-binding and globalFirst validation are correctly guarded by signatures, quorum, and FullProposal.Verify; no blocking issues found.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Codex and Cursor second-opinion passes produced no findings; Codex additionally noted it could not run a targeted Go compile check because the sandbox lacks the required Go 1.25.6 toolchain, so this review is based on static analysis only — recommend confirming CI go test -race is green before merge.
  • Wire/WAL-breaking change: ProposalConv.Decode now hard-rejects messages missing global_first (and View/AppProposal require epoch_index). This is deliberate and documented as acceptable for pre-production autobahn, but it means any autobahn state persisted by an older binary (consensus/data WALs) will fail to decode on restart across this upgrade — worth calling out explicitly in release notes for any running test nets.
  • PR description references registry.CommitteeFor(roadIndex), but no such method exists — the code routes committee lookups through EpochByIndex(...) / LatestEpoch().Committee(). Minor description drift, no code impact.
  • Registry.VerifyInWindow returns []*types.Epoch so callers can skip re-verification, but every current caller ignores the returned slice (if _, err := ...). Fine as a forward-looking API, just currently dead return value.
  • Proposal.Verify now checks r.first > r.next inline (proposal.go:245) which is already enforced by the subsequent r.Verify(c) (LaneRange.Verify at proposal.go:56); harmless defense-in-depth but redundant.

@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, well-tested read-side refactor that introduces epoch.Registry/Epoch and bakes an absolute global_first + epoch_index into Proposal/AppProposal/View, routing all committee lookups through the registry. The change is internally consistent and thoroughly re-tests the affected verification paths; I found no blocking correctness or security issues, only a few non-blocking observations (including a downgrade of Codex's severity assessment).

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Codex (High) — global_first trusted without structural validation in Proposal/CommitQC/FullCommitQC.Verify: I confirmed the type-layer Verify methods do not re-derive/continuity-check global_first, but I disagree with the High severity. The absolute numbering is anchored to the trusted local registry.FirstBlock() via inner.nextQC, and both data.inner.insertQC and State.PushQC require gr.First == inner.nextQC (gaps rejected, stale skipped), while a valid signed CommitQC implies a quorum of honest validators already ran FullProposal.Verify (which checks GlobalRange().First == NextGlobalBlock()). So arbitrary absolute block numbers cannot be committed to state under the honest-majority model. Worth considering the suggested continuity assertion in CommitQC.Verify purely as defense-in-depth for a future QC-sync path that bypasses the data layer's nextQC anchoring.
  • Wire-format breaking change: ProposalConv/AppProposalConv/ViewConv now hard-reject messages missing global_first / epoch_index. The code comments and PR description state autobahn is pre-production with no rolling-upgrade path, so this is acceptable — flagging only for release-coordination awareness (all nodes must upgrade together).
  • Behavioral change in data.State.PushBlock: it now blocks (WaitUntil n < inner.nextQC) until the QC for n is present before verifying, whereas it previously verified+inserted immediately. Worth confirming all callers (e.g. giga data-sync RPC handlers) pass a bounded/cancelable context so a block whose QC never arrives cannot wedge a goroutine indefinitely.
  • Second-opinion inputs: cursor-review.md and REVIEW_GUIDELINES.md are both empty (Cursor produced no output and no repo-specific review standards were available); reviewed without them.
  • Registry currently returns the genesis epoch for every lookup and several call sites carry TODOs for multi-epoch wiring (consensus newInner assumes LatestEpoch(), VerifyInWindow only checks latest, avail vote weighting is single-committee). These are intentional per the PR's read-side-first strategy but represent latent correctness gaps that must be closed before dynamic epochs land — a restart after an epoch transition would currently fail persisted-state validation.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

return fmt.Errorf("roadIndex() = %v, want %v", got, want)
}
if got, want := m.GlobalNumber(), qc.GlobalRange(c); got < want.First || got >= want.Next {
if got, want := m.GlobalNumber(), qc.GlobalRange(); got < want.First || got >= want.Next {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] Now that qc.GlobalRange() no longer takes a committee, the c *Committee parameter of AppProposal.Verify (line 47) is unused. Consider dropping it and updating the caller in avail/state.go (PushAppVote) to reduce dead surface.

@wen-coding wen-coding force-pushed the wen/autobahn_epoch_registry branch from 18a5cd2 to cd2a1fa Compare July 7, 2026 22:58

@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 disciplined, well-tested refactor that introduces epoch.Registry/types.Epoch as the authoritative committee source and bakes global_first / epoch bindings into proposals so GlobalRange() is self-contained. I found no correctness or security blockers; API replacement is consistent (no dangling call sites) and the new wire fields are quorum-signed, so global_first can't be forged. Non-blocking notes only.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Wire-breaking change: Proposal.global_first, View.epoch_index, and AppProposal.epoch_index are hard-rejected on decode when absent (proposal.go ProposalConv.Decode, app_proposal.go, ViewConv.Decode). There is no rolling-upgrade path, so every node must upgrade in lockstep. This is explicitly acknowledged in the PR as acceptable because autobahn is pre-production — worth surfacing for deploy coordination.
  • Second-opinion inputs were empty/negative: REVIEW_GUIDELINES.md and cursor-review.md are empty, and codex-review.md reports no findings (it also notes it could not compile/run tests due to a Go 1.24 vs required 1.25.6 toolchain mismatch in its sandbox). No prompt-injection content was present in the PR.
  • Several avail/data entry points resolve the epoch from a message-supplied index before or around verification (avail/state.go PushAppVote uses v.Msg().Proposal().EpochIndex() to pick the committee for the initial sig check; PushCommitQC/insertQC/PushQC key off qc.Proposal().EpochIndex()). This is safe today — only the genesis epoch exists and the values are later cross-checked against the trusted local commitQC (AppProposal.Verify enforces epoch equality) — but the cross-checks should be re-audited when dynamic epochs (AddEpoch) land. The code already flags this with TODOs.
  • Numerous TODOs mark the intentionally-unwired dynamic-epoch behavior (consensus/inner.go restart-after-transition assumes LatestEpoch; avail per-epoch vote weighting; Registry.VerifyInWindow neighbor epochs; epoch rotation in pushCommitQC/pushTimeoutQC). Reasonable for an incremental read-side-first PR, but restart correctness after a future epoch transition is not yet covered.

@wen-coding wen-coding force-pushed the wen/autobahn_epoch_registry branch from cd2a1fa to d31d417 Compare July 9, 2026 01:55
@wen-coding wen-coding marked this pull request as draft July 9, 2026 02:01

@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, well-tested refactor introducing epoch.Registry and threading Epoch/epoch_index through the autobahn consensus verification stack; the read-side wiring is consistent and unknown/forged epoch indices are rejected at every call site. No blocking issues found; a few non-blocking notes on wire-compatibility and nil-panic footguns.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Wire-format break: ProposalConv.Decode, ViewConv.Decode, and AppProposalConv.Decode now hard-reject any message missing global_first/epoch_index, and View gained a signed field, so old and new nodes cannot interoperate. The PR acknowledges autobahn is pre-production with no rolling-upgrade path (label non-app-hash-breaking) — acceptable, but worth confirming no deployed peers still speak the old format before merge.
  • ViewSpec.Epoch is now required and dereferenced (viewSpec.Epoch.Committee(), vs.Epoch.EpochIndex()) in NewProposal, ViewSpec.View(), NextGlobalBlock(), and NextTimestamp(); a nil Epoch panics rather than erroring. It's documented in the struct comment and all current call sites set it, but it's a footgun for future callers — a guarded error or a constructor that enforces non-nil would be safer.
  • Several correctness-critical TODOs are left for follow-up dynamic-epoch PRs and are load-bearing: Registry.VerifyInWindow/LatestEpoch only ever consult epoch 0, and newInner assumes LatestEpoch() (comment notes a restart after an epoch transition would fail validation). Fine for this read-side-only PR since only the genesis epoch exists, but these must land before dynamic epochs are enabled.
  • Second-opinion passes: OpenAI Codex reported no material findings (targeted tests couldn't run due to a Go 1.24.13 vs required 1.25.6 sandbox mismatch); the Cursor review file was empty (no output produced).

@wen-coding wen-coding force-pushed the wen/autobahn_epoch_registry branch 12 times, most recently from 057de21 to 8d77263 Compare July 9, 2026 03:53
- 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>
@wen-coding wen-coding force-pushed the wen/autobahn_epoch_registry branch from 8d77263 to bbfb131 Compare July 9, 2026 03:58
…verage in runPersist

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@wen-coding wen-coding marked this pull request as ready for review July 9, 2026 04:10

@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 mechanical refactor introducing epoch.Registry as the single source of committee/stake, moving firstBlock/genesisTimestamp into Epoch, storing GlobalRange directly on the wire, and adding epoch-binding checks to all QC Verify paths. The change is internally consistent, well-covered by new and updated tests, and the wire fields (global_first, epoch_index) are authenticated via the existing signed-proposal hash; only minor non-blocking nits found.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Second-opinion passes produced little: Codex reported "No material findings" (and noted it could not run tests due to a Go 1.24 vs required 1.25.6 mismatch); the Cursor review file was empty (no output). No additional issues were surfaced by either.
  • Wire compatibility is intentionally broken: Proposal.global_first, View.epoch_index, and AppProposal.epoch_index are optional in the proto but hard-rejected when absent on decode. This is acknowledged in-code as acceptable because Autobahn is pre-production with no rolling-upgrade path — fine, but worth confirming no already-persisted WAL/snapshot from before this change needs to be decodable.
  • Hot-path lock: EvmProxy (giga_router_common.go / giga_router_validator.go) now resolves the committee via r.data.Registry().LatestEpoch().Committee(), which takes an RWMutex RLock on every EVM proxy request where it previously read an immutable field directly. Negligible today, but a per-request lock on a routing hot path is worth keeping in mind.
  • Correctness of the epoch-binding refactor relies on the genesis epoch using OpenRoadRange() (Last = MaxUint), so View.Verify's road-range check is effectively a no-op in production until epoch transitions land. The numerous TODOs (per-epoch weight counting in avail, epoch resolution on restart in consensus/newInner, cross-epoch batch lanes in runPersist) correctly scope this PR to the single-genesis-epoch case.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread sei-tendermint/autobahn/types/app_proposal.go
Comment thread sei-tendermint/autobahn/types/proposal.go Outdated
…emove redundant lane checks

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 but well-structured refactor introducing epoch.Registry as the single source of truth for committee/stake and adding EpochIndex binding to all QC verification paths; the bulk of the diff is mechanical test/API plumbing, the new epoch-binding logic is coherent and well-tested at the types layer, and I found no blocking correctness or security issues. Verdict is neutral only because of a few non-blocking observations and because one of the second-opinion passes produced no output.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Second-opinion coverage: cursor-review.md was empty (no output produced by that pass), and Codex reported no material findings (its targeted go test could not run because the Go 1.25.6 toolchain download was blocked by the sandbox). This review therefore rests primarily on this pass; the epoch-binding paths would benefit from a real go test ./sei-tendermint/autobahn/... ./sei-tendermint/internal/autobahn/... run in CI before merge.
  • Wire trust model for global_first: Proposal.GlobalRange().First is now decoded verbatim from the new global_first wire field rather than derived from a trusted local committee.FirstBlock(). Continuity (First == prev.GlobalRange().Next) is only re-enforced in FullProposal.Verify via NextGlobalBlock(); standalone CommitQC.Verify/PrepareQC.Verify accept the committee-signed value as-is. This is consistent with the honest-quorum trust assumption, but a short comment on newProposal/NextGlobalBlock noting that standalone QC verification trusts the quorum-attested offset would help future readers.
  • Test coverage gap on the new defensive branches: the numerous unknown epoch_index %d error returns and epoch-mismatch rejections added across the avail/consensus/data state layers (e.g. PushCommitQC, PushAppVote, PushQC, insertQC, PushBlock) are effectively unreachable today because the registry only holds the genesis epoch, so none have direct unit tests. Worth adding coverage when AddEpoch/epoch transitions land.
  • Acknowledged future-correctness TODOs are extensive and should be tracked as a follow-up gate before dynamic epochs ship: consensus/inner.go newInner assumes LatestEpoch() and will fail validation on restart after an epoch transition; avail weight counting and lane-union in runPersist are single-epoch only; consensus does not rotate epoch on CommitQC/TimeoutQC advance. All are clearly commented, and the PR explicitly scopes dynamic transitions to follow-up PRs, so this is just tracking, not a blocker.
  • Breaking wire/persistence format change (added required View.epoch_index, AppProposal.epoch_index, Proposal.global_first): decode hard-rejects messages/persisted state lacking these fields, so there is no rolling-upgrade path from pre-PR encodings. The code comments state autobahn is pre-production and this is intentional; flagging only so it is a conscious release decision.

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