Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
937ae20
feat(autobahn): introduce EpochTrio for multi-epoch verification (CON…
wen-coding Jul 13, 2026
2f4c37f
fix(autobahn): reweightForNextEpoch initializes queues for Current∪Ne…
wen-coding Jul 13, 2026
b383ba5
doc(autobahn): explain PushQC epoch window narrowing
wen-coding Jul 13, 2026
ccb2bb4
fix(autobahn): seal registry after fullnode data layer init
wen-coding Jul 13, 2026
8e8d4b3
fix(autobahn): retain Prev-epoch lanes until boundary QC is collected
wen-coding Jul 13, 2026
9fca133
test(autobahn): verify Prev-epoch lanes retained across reweightForNe…
wen-coding Jul 13, 2026
428fe23
fix(autobahn): correct midpoint gate N+2→N+1 and data restart epoch r…
wen-coding Jul 13, 2026
91801ca
fix(autobahn): seed N+2 in AdvanceIfNeeded and restore midpoint gate …
wen-coding Jul 13, 2026
c953b49
fix(autobahn): execution-driven epoch seeding via AdvanceIfNeeded
wen-coding Jul 13, 2026
1e5cfd1
fix(autobahn): preserve block header across epoch reweight, align epo…
wen-coding Jul 13, 2026
32b3dec
fix(autobahn): RLock fast-path for EpochAt/AdvanceIfNeeded, clarify P…
wen-coding Jul 13, 2026
2595936
fix(autobahn): exclude zero-weight signers from LaneQC votes in pushVote
wen-coding Jul 13, 2026
9bdcedc
fix(autobahn): use Option for EpochTrio.Prev and rename laneVoteSet.r…
wen-coding Jul 13, 2026
ef1e401
refactor(autobahn): per-epoch lane vote accumulation and restart epoc…
wen-coding Jul 13, 2026
eedf9d4
fix(autobahn): resolve laneQC against the caller's epoch
wen-coding Jul 13, 2026
c8eb573
fix(autobahn): skip out-of-window CommitQC in PushCommitQC instead of…
wen-coding Jul 13, 2026
78864bb
fix(autobahn): create byHash entries lazily in credit
wen-coding Jul 14, 2026
ea9db9f
feat(autobahn): block at epoch boundary until next committee is seeded
wen-coding Jul 14, 2026
d1b3298
fix(autobahn): gate consensus votes by epoch; mirror boundary wait in…
wen-coding Jul 14, 2026
72b82df
docs(autobahn): document epoch-contiguity and per-epoch credit invari…
wen-coding Jul 14, 2026
f761b3c
fix(autobahn): guard markBlockPersisted against decommissioned lane
wen-coding Jul 14, 2026
2ca409f
fix(autobahn): prevent epochTrio regression in data.PushQC
wen-coding Jul 14, 2026
55d8c2e
fix(autobahn): gate markBlockPersisted on blocks[lane], not nextBlock…
wen-coding Jul 14, 2026
30422be
feat(autobahn): replace SealSeeding with SetupInitialTrio from data W…
wen-coding Jul 14, 2026
289134d
fix(autobahn): snapshot epochTrio once in data.PushQC boundary path
wen-coding Jul 14, 2026
05e97b2
fix(autobahn): seed epochs from executed CommitQC road, not FinalAppS…
wen-coding Jul 14, 2026
f85014c
fix(autobahn): SetupInitialTrio from consensus tip on restart
wen-coding Jul 14, 2026
018bc0c
docs(autobahn): clarify WaitForEpoch vs SetupInitialTrio highestEpoch…
wen-coding Jul 14, 2026
f319c4c
refactor(autobahn): own epochTrio via AtomicSend on inner, Recv on State
wen-coding Jul 14, 2026
a646f4e
docs(autobahn): fix non-current epoch vote drop recovery comment
wen-coding Jul 14, 2026
16f6558
docs(autobahn): note SetupInitialTrio vs CommitQC WAL span assumption
wen-coding Jul 14, 2026
8646e34
fix(autobahn): seed registry at avail Commit tip before restart TrioAt
wen-coding Jul 15, 2026
e2c87f2
fix(autobahn): guard decommissioned lanes; document consensus EpochAt…
wen-coding Jul 15, 2026
eb64d08
refactor(autobahn): retain lanes across epoch advance; trim trio comm…
wen-coding Jul 15, 2026
a2827c2
refactor(autobahn): store LaneQC in blockVotes on quorum
wen-coding Jul 15, 2026
7f22c11
fix(autobahn): seed avail restart trio from CommitQC tip
wen-coding Jul 15, 2026
3c0ad6e
fix(autobahn): accept CommitQC only for Current or Next
wen-coding Jul 15, 2026
8b2d661
refactor(autobahn): add Registry.WaitForTrio for boundary seeding
wen-coding Jul 15, 2026
d0f5388
refactor(autobahn): rename advanceEpoch and store trio inside it
wen-coding Jul 15, 2026
89077db
fix(autobahn): gate CommitQC N+1 on AppQC for epoch N
wen-coding Jul 16, 2026
483b9f4
fix(autobahn): drop stale out-of-window AppQCs
wen-coding Jul 16, 2026
d32df83
refactor(autobahn): make RoadRange half-open [First, Next)
wen-coding Jul 16, 2026
c5ad50c
refactor(autobahn): rename commitTipRoad to nextCommitQC
wen-coding Jul 16, 2026
7126aee
refactor(autobahn): bundle per-lane avail state in laneState
wen-coding Jul 16, 2026
41fe6f6
fix(autobahn): wait for CommitQC before resolving AppVote epoch
wen-coding Jul 16, 2026
6b11740
fix(autobahn): block PushAppQC until road is in epoch window
wen-coding Jul 16, 2026
883d9a9
refactor(autobahn): cache lane vote weight for Current only
wen-coding Jul 16, 2026
d3d4e8b
fix(autobahn): require prune anchor when restarting past epoch 0
wen-coding Jul 16, 2026
50f5ea1
refactor(autobahn): WaitForLaneQCs returns Current epoch instead of t…
wen-coding Jul 16, 2026
0448f7b
refactor(autobahn): collect PushQC blocks from QC headers
wen-coding Jul 16, 2026
3438dc1
docs(autobahn): note appVotes prune on CommitQC anchor, not advanceEpoch
wen-coding Jul 16, 2026
49d2e5c
refactor(autobahn): rename EpochTrio files to EpochDuo
wen-coding Jul 17, 2026
afcd76a
refactor(autobahn): replace EpochTrio with EpochDuo{Prev, Current}
wen-coding Jul 17, 2026
285ff7c
refactor(autobahn): drop registry from consensus inner; seed N+2 on l…
wen-coding Jul 17, 2026
f8f4832
fix(autobahn): reject CommitQC whose epoch disagrees with consensus i…
wen-coding Jul 17, 2026
edeaec7
feat(autobahn): allow AppQC from Prev epoch in proposals via ViewSpec…
wen-coding Jul 17, 2026
c82809f
fix(blocksim): update ViewSpec for EpochDuo field rename
wen-coding Jul 17, 2026
0299d04
refactor(autobahn): SetupInitialDuo seeds from the last known CommitQC
wen-coding Jul 17, 2026
1c71d0d
refactor(autobahn): narrow SetupInitialDuo restart seeding to {N-1,N}
wen-coding Jul 17, 2026
ade98c6
refactor(autobahn): seed restart epochs from data CommitQC + executio…
wen-coding Jul 17, 2026
a46ffa0
fix(autobahn): backpressure too-early CommitQC on Current window
wen-coding Jul 17, 2026
0d9e87b
refactor(autobahn): share epoch-window wait/drop helpers in avail
wen-coding Jul 17, 2026
8228350
fix(autobahn): stop prune from silently inserting CommitQCs
wen-coding Jul 17, 2026
c19c75b
fix(autobahn): seed E+2 on restart when execution tip closes epoch E
wen-coding Jul 17, 2026
5ead0ee
refactor(autobahn): replay AdvanceIfNeeded from SetupInitialDuo
wen-coding Jul 17, 2026
99a4ee8
fix(autobahn): seed N+1 when restart CommitQC tip closes epoch N
wen-coding Jul 17, 2026
efcc631
refactor(autobahn): express restart seeding via EnsureEpoch helpers
wen-coding Jul 17, 2026
1d7e82f
docs(autobahn): note CommitQC tip anchors restart when execution QC p…
wen-coding Jul 17, 2026
a44607e
fix(autobahn): guard missing lanes in avail headers()
wen-coding Jul 17, 2026
f16bb0e
fix(autobahn): discard unverifiable data WAL QCs on restart
wen-coding Jul 18, 2026
0db0b60
fix(autobahn): catch up consensus CommitQCs road-by-road from avail tip
wen-coding Jul 18, 2026
3f7edd5
refactor(autobahn): trust avail CommitQC tip in consensus without re-…
wen-coding Jul 18, 2026
a05f2da
Revert "fix(autobahn): discard unverifiable data WAL QCs on restart"
wen-coding Jul 18, 2026
424d5d6
fix(autobahn): range-check AppQC road against its epoch
wen-coding Jul 18, 2026
b759b0e
refactor(autobahn): seed restart epochs across loaded CommitQC WAL span
wen-coding Jul 18, 2026
53a81b8
fix(autobahn): re-verify CommitQCs on avail reload and consensus tip
wen-coding Jul 18, 2026
8215ee9
fix(autobahn): wait for registry epoch N+1 before admitting CommitQC …
wen-coding Jul 18, 2026
cbdeb36
fix(autobahn): hard-fail fullCommitQC on duo miss instead of ErrPrune…
wen-coding Jul 18, 2026
61cc882
fix(autobahn): fullCommitQC never returns ErrPruned
wen-coding Jul 18, 2026
dd6d4a8
fix(autobahn): only return ErrPruned from fullCommitQC on real prune
wen-coding Jul 18, 2026
c986e78
fix(autobahn): classify duo roads as before/after/in window for export
wen-coding Jul 18, 2026
ab8d98e
refactor(autobahn): EpochForRoad returns before/after window errors
wen-coding Jul 18, 2026
d6bfd73
fix(autobahn): map duo-before to ErrPruned; do not wait on duo-after
wen-coding Jul 18, 2026
4ca12c3
fix(autobahn): AdvanceIfNeeded on last global; validate AppQC before …
wen-coding Jul 18, 2026
6e08874
fix(autobahn): apply CommitQC epoch leashes on PushAppQC tipcut
wen-coding Jul 18, 2026
50c9088
fix(autobahn): require AppQC in E before closing epoch E
wen-coding Jul 18, 2026
f43df2f
refactor(autobahn): drop mid-epoch AppQC-of-N-1 admit wait
wen-coding Jul 19, 2026
5e1db10
refactor(autobahn): gate registry N+1 only when sealing epoch N
wen-coding Jul 19, 2026
c091122
fix(autobahn): seed N+2 on empty closing tipcut via PushQC
wen-coding Jul 20, 2026
edcad5c
Merge branch 'main' into wen/autobahn_epoch_trio
wen-coding Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions sei-db/ledger_db/block/blocksim/block_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,11 @@ func (g *BlockGenerator) buildFullCommitQC() (*types.FullCommitQC, []*types.Bloc
}
}

viewSpec := types.ViewSpec{CommitQC: prev, Epoch: types.NewEpoch(0, types.OpenRoadRange(), genesisTime, committee, 0)}
viewSpec := types.ViewSpec{CommitQC: prev, Epochs: types.EpochDuo{Current: types.NewEpoch(0, types.OpenRoadRange(), genesisTime, committee, 0)}}
leader := committee.Leader(viewSpec.View())
appQC := func() utils.Option[*types.AppQC] {
if n := viewSpec.NextGlobalBlock(); n > 0 {
p := types.NewAppProposal(n-1, viewSpec.View().Index, types.AppHash(g.rand.Bytes(hashSizeBytes)), viewSpec.Epoch.EpochIndex())
p := types.NewAppProposal(n-1, viewSpec.View().Index, types.AppHash(g.rand.Bytes(hashSizeBytes)), viewSpec.Epoch().EpochIndex())
return utils.Some(g.fakeAppQC(p))
}
return utils.None[*types.AppQC]()
Expand Down
1 change: 1 addition & 0 deletions sei-tendermint/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Within sei-tendermint subdirectory
Required fields must be nil-checked at the boundary (constructor and proto Decode) and trusted
everywhere else — do not add defensive nil-checks in internal logic.
* Avoid removing comments and logs which are not obviously obsolete. Keep the original wording, only fixing mistakes or obsolete parts.
* Prefer concise comments that state invariants, contracts, and observable behavior (plus brief correctness reasoning when non-obvious). Avoid narrating how the implementation works step-by-step.

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] Codex flagged this line as a prompt-injection vector to be removed. I disagree: it's a benign, on-topic style guideline consistent with the comment cleanups elsewhere in this PR, with no instruction directed at a reviewer/agent. Recommend keeping it. (Flagging here only to record the resolution of Codex's point.)

Comment thread
wen-coding marked this conversation as resolved.

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] Codex flagged this line as a possible prompt-injection attempt ("reviewer-directed instructions inside the untrusted PR diff"). I disagree: this is a normal contributor-guide edit — a comment-style guideline consistent with the surrounding bullets — not an instruction directed at a reviewer to change a verdict or bypass process. No action needed; noting for the record since it was raised by the second-opinion pass.

* TestRng instance should be one per test, constructed directly in the test function. In case of nested/table tests, each nested test should create its own instance.
Use TestRng.Split() (before spawning) if you need to pass entropy source to a spawned goroutine
to ensure deterministic entropy across the goroutines.
4 changes: 2 additions & 2 deletions sei-tendermint/autobahn/types/committee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func TestPrepareQCVerifyChecksEpochBinding(t *testing.T) {
wrongEpoch := newProposal(View{Index: ep.RoadRange().First, EpochIndex: ep.EpochIndex() + 1}, time.Time{}, nil, utils.None[*AppProposal](), ep.FirstBlock())
require.Error(t, sign(wrongEpoch).Verify(ep))

outOfRoads := newProposal(View{Index: ep.RoadRange().Last + 1, EpochIndex: ep.EpochIndex()}, time.Time{}, nil, utils.None[*AppProposal](), ep.FirstBlock())
outOfRoads := newProposal(View{Index: ep.RoadRange().Next, EpochIndex: ep.EpochIndex()}, time.Time{}, nil, utils.None[*AppProposal](), ep.FirstBlock())
require.Error(t, sign(outOfRoads).Verify(ep))
}

Expand All @@ -148,7 +148,7 @@ func TestCommitQCVerifyChecksEpochBinding(t *testing.T) {
wrongEpoch := newProposal(View{Index: ep.RoadRange().First, EpochIndex: ep.EpochIndex() + 1}, time.Time{}, nil, utils.None[*AppProposal](), ep.FirstBlock())
require.Error(t, sign(wrongEpoch).Verify(ep))

outOfRoads := newProposal(View{Index: ep.RoadRange().Last + 1, EpochIndex: ep.EpochIndex()}, time.Time{}, nil, utils.None[*AppProposal](), ep.FirstBlock())
outOfRoads := newProposal(View{Index: ep.RoadRange().Next, EpochIndex: ep.EpochIndex()}, time.Time{}, nil, utils.None[*AppProposal](), ep.FirstBlock())
require.Error(t, sign(outOfRoads).Verify(ep))
}

Expand Down
13 changes: 7 additions & 6 deletions sei-tendermint/autobahn/types/epoch.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@ import (
// EpochIndex is the epoch number.
type EpochIndex uint64

// RoadRange is an inclusive range of RoadIndex values [First, Last].
// RoadRange is a half-open range of RoadIndex values [First, Next).
// Matches GlobalRange / LaneRange: Next is exclusive (Next == lastInclusive+1).
type RoadRange struct {
First RoadIndex
Last RoadIndex
Next RoadIndex
}

// OpenRoadRange returns a RoadRange covering all road indices from 0.
// OpenRoadRange returns a RoadRange covering road indices [0, Max).
// Use in tests and genesis epochs where no upper bound is known yet.
func OpenRoadRange() RoadRange { return RoadRange{First: 0, Last: utils.Max[RoadIndex]()} }
func OpenRoadRange() RoadRange { return RoadRange{First: 0, Next: utils.Max[RoadIndex]()} }

// Has reports whether idx falls within this range (inclusive on both ends).
func (r RoadRange) Has(idx RoadIndex) bool { return idx >= r.First && idx <= r.Last }
// Has reports whether idx falls within this range [First, Next).
func (r RoadRange) Has(idx RoadIndex) bool { return idx >= r.First && idx < r.Next }

// Epoch holds the complete context for a single epoch.
// Retrieved from the local Registry; never transmitted on the wire.
Expand Down
94 changes: 94 additions & 0 deletions sei-tendermint/autobahn/types/epoch_duo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package types

import (
"errors"
"fmt"

"github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils"
)

// EpochDuo is a sliding window of up to two consecutive epochs.
// Current is always set; Prev is absent only for epoch 0.
//

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] This is the crux of Codex's P1: the PR title/description describe an EpochTrio{Prev, Current, Next} where lane traffic verifies against Current+Next, but the implementation is a Duo that admits new-committee lane traffic only after the boundary CommitQC advances Current. That is fine today because every epoch shares the genesis committee (makeEpoch uses genesis.Committee()), so no validator is ever outside Current — but once committees actually rotate, a validator that is only in epoch N+1 will have its blocks/lane-votes rejected by PushBlock/PushVote (Current-only verify) for the entire duration of epoch N. Please confirm the Next window is truly unnecessary and reconcile the PR description.

// Next is intentionally not held: new-committee lane traffic is admitted only
// after CommitQC advances Current into the next epoch.
type EpochDuo struct {
Prev utils.Option[*Epoch] // absent if Current is epoch 0
Current *Epoch
}

// ErrRoadBeforeWindow is returned by EpochForRoad when the road is older than
// WindowFirst (behind the duo).
var ErrRoadBeforeWindow = errors.New("road before epoch duo window")

// ErrRoadAfterWindow is returned by EpochForRoad when the road is newer than
// Current (at or past Current.Next).
var ErrRoadAfterWindow = errors.New("road after epoch duo window")

func (w EpochDuo) all() [2]utils.Option[*Epoch] {
return [2]utils.Option[*Epoch]{utils.Some(w.Current), w.Prev}

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] all() does utils.Some(w.Current) unconditionally, and Some(nil) is a present Option wrapping a nil *Epoch. So a zero-value EpochDuo{} is not skipped: EpochForRoad, WindowFirst, and String will deref a nil *Epoch and panic. Registry.DuoAt/WaitForDuo return exactly types.EpochDuo{} on their error paths, so a caller that skips the err check gets a panic rather than a clean failure. Note the asymmetry — EpochForIndex and CurrentForRoad do guard w.Current != nil while these do not. Either guard uniformly or make Current a utils.Option[*Epoch] like Prev.

}

// EpochForRoad returns the epoch whose road range contains roadIdx.
// Prefers Current so an open-range Prev cannot shadow later epochs.
// Returns ErrRoadBeforeWindow or ErrRoadAfterWindow when out of window.

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 doc promises "Prefers Current so an open-range Prev cannot shadow later epochs" and to return ErrRoadAfterWindow when out of window, but preferring Current only helps for roads inside both ranges. If Prev has an open range (Next == Max) and roadIdx >= Current.Next, the loop still matches Prev.Has(roadIdx) and returns Prev instead of ErrRoadAfterWindow. Unreachable in production (the registry only builds bounded epochs and the sole OpenRoadRange caller uses it as Current), but the stated guarantee is broader than the code delivers — either narrow the comment or assert a bounded Prev. TestEpochForRoad_OpenRangePrevDoesNotMaskCurrent only tests a road inside Current, so it wouldn't catch this.

func (w EpochDuo) EpochForRoad(roadIdx RoadIndex) (*Epoch, error) {
for _, oep := range w.all() {
if ep, ok := oep.Get(); ok && ep.RoadRange().Has(roadIdx) {
return ep, nil
}
}
if roadIdx < w.WindowFirst() {
return nil, fmt.Errorf("road %d before window %v: %w", roadIdx, w, ErrRoadBeforeWindow)
}
return nil, fmt.Errorf("road %d after window %v: %w", roadIdx, w, ErrRoadAfterWindow)
}

// EpochOptForRoad is EpochForRoad as an Option (None when out of window).
func (w EpochDuo) EpochOptForRoad(roadIdx RoadIndex) utils.Option[*Epoch] {
if ep, err := w.EpochForRoad(roadIdx); err == nil {
return utils.Some(ep)
}
return utils.None[*Epoch]()
}

// CurrentForRoad returns Current when roadIdx is in Current's range; else None.
// Unlike EpochOptForRoad, Prev is never admitted.
func (w EpochDuo) CurrentForRoad(roadIdx RoadIndex) utils.Option[*Epoch] {
if w.Current != nil && w.Current.RoadRange().Has(roadIdx) {
return utils.Some(w.Current)
}
return utils.None[*Epoch]()
}

// WindowFirst is the earliest road still in Prev|Current.
func (w EpochDuo) WindowFirst() RoadIndex {
if prev, ok := w.Prev.Get(); ok {
return prev.RoadRange().First
}
return w.Current.RoadRange().First
}

// EpochForIndex returns Current or Prev by epoch index.
func (w EpochDuo) EpochForIndex(idx EpochIndex) (*Epoch, error) {
if w.Current != nil && w.Current.EpochIndex() == idx {
return w.Current, nil
}
if prev, ok := w.Prev.Get(); ok && prev.EpochIndex() == idx {
return prev, nil
}
return nil, fmt.Errorf("epoch %d not in window %v", idx, w)
}
Comment on lines +57 to +81

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 In epoch_duo.go, CurrentForRoad (line 58) and EpochForIndex (line 74) each add a redundant w.Current != nil && guard, even though EpochDuo.Current is documented as always set and sibling methods in the same file (all(), WindowFirst()) already dereference it unconditionally. This is pre-existing dead code / a style inconsistency with no functional impact — recommend dropping the nil checks to match the rest of the file.

Extended reasoning...

EpochDuo documents Current as always set (epoch_duo.go:10-11: "Current is always set; Prev is absent only for epoch 0"), and the rest of the file already relies on that invariant without guarding: all() (line 29) does utils.Some(w.Current) unconditionally, and WindowFirst() (line 69) dereferences w.Current.RoadRange().First with no nil check. EpochForRoad, which calls WindowFirst() internally, would already panic on a nil Current before ever reaching CurrentForRoad or EpochForIndex.

Despite that, CurrentForRoad (line 58) and EpochForIndex (line 74) each add a w.Current != nil && guard before touching Current. This is inconsistent with the rest of the file's treatment of the field and reads as defensive code guarding against a state that the type's own documentation says cannot occur.

No code path can actually produce an EpochDuo with a nil Current: the type is only constructed via Registry.DuoAt, WaitForDuo, newInner, and test helpers, all of which always populate Current. Both callers of these two functions also independently prove Current is non-nil before or during the call — e.g. proposal.go's FullProposal.Verify dereferences vs.Epoch() (which is vs.Epochs.Current) earlier in the same call, and CurrentForRoad is only reached via avail's waitCurrentForRoad on a duo sourced from the always-initialized s.epochDuo. So the guards are unreachable dead code today.

The sei-tendermint/AGENTS.md guidance cited in the original finding ("do not add defensive nil-checks in internal logic" for required fields) is written specifically about proto-decoded required fields nil-checked at the boundary, so it does not apply verbatim to this Go struct — but the underlying principle (avoid defensive checks against a documented invariant, which obscures rather than clarifies the guarantee) still holds here as a straightforward internal-consistency issue.

Step-by-step: (1) EpochDuo.Current is documented as always non-nil and every real constructor honors that. (2) all() and WindowFirst() in the same file already dereference Current without a nil check, so a nil Current would panic there before either CurrentForRoad or EpochForIndex could run their own guard. (3) Therefore the w.Current != nil && clauses in CurrentForRoad and EpochForIndex can never actually short-circuit on a real nil — removing them changes no observable behavior, only removes redundant/misleading code.

Fix: drop w.Current != nil && from both CurrentForRoad (line 58) and EpochForIndex (line 74), matching how all() and WindowFirst() already treat Current as unconditionally present.


// String returns a compact description of the epoch indices in the window.
func (w EpochDuo) String() string {
s := "epochs ["
sep := ""
for _, oep := range w.all() {
if ep, ok := oep.Get(); ok {
s += fmt.Sprintf("%s%d", sep, ep.EpochIndex())
sep = ", "
}
}
return s + "]"
}
138 changes: 138 additions & 0 deletions sei-tendermint/autobahn/types/epoch_duo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package types_test

import (
"errors"
"testing"

"github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types"
"github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils"
)

func testDuoEpochs(t *testing.T) (prev, current *types.Epoch) {
t.Helper()
rng := utils.TestRng()
weights := map[types.PublicKey]uint64{}
for range 3 {
weights[types.GenSecretKey(rng).Public()] = 1
}
committee := utils.OrPanic1(types.NewCommittee(weights))
prev = types.NewEpoch(0, types.RoadRange{First: 0, Next: 100}, utils.GenTimestamp(rng), committee, 1)
current = types.NewEpoch(1, types.RoadRange{First: 100, Next: 200}, utils.GenTimestamp(rng), committee, 101)
return prev, current
}

func TestEpochForRoad_HitsCurrentEpoch(t *testing.T) {
_, current := testDuoEpochs(t)
w := types.EpochDuo{Current: current}
ep, err := w.EpochForRoad(150)
if err != nil {
t.Fatalf("EpochForRoad(150): %v", err)
}
if ep != current {
t.Fatalf("got %v, want current", ep)
}
}

func TestEpochForRoad_HitsPrevEpoch(t *testing.T) {
prev, current := testDuoEpochs(t)
w := types.EpochDuo{Prev: utils.Some(prev), Current: current}
ep, err := w.EpochForRoad(50)
if err != nil {
t.Fatalf("EpochForRoad(50): %v", err)
}
if ep != prev {
t.Fatalf("got %v, want prev", ep)
}
}

func TestEpochForRoad_OutsideWindowReturnsError(t *testing.T) {
_, current := testDuoEpochs(t)
w := types.EpochDuo{Current: current}
_, err := w.EpochForRoad(999)
if !errors.Is(err, types.ErrRoadAfterWindow) {
t.Fatalf("EpochForRoad(999) = %v, want ErrRoadAfterWindow", err)
}
_, err = w.EpochForRoad(50)
if !errors.Is(err, types.ErrRoadBeforeWindow) {
t.Fatalf("EpochForRoad(50) current-only = %v, want ErrRoadBeforeWindow", err)
}
}

func TestEpochForRoad_OpenRangePrevDoesNotMaskCurrent(t *testing.T) {
rng := utils.TestRng()
weights := map[types.PublicKey]uint64{types.GenSecretKey(rng).Public(): 1}
committee := utils.OrPanic1(types.NewCommittee(weights))
openEpoch := types.NewEpoch(0, types.OpenRoadRange(), utils.GenTimestamp(rng), committee, 1)
current := types.NewEpoch(1, types.RoadRange{First: 100, Next: 200}, utils.GenTimestamp(rng), committee, 101)
w := types.EpochDuo{Prev: utils.Some(openEpoch), Current: current}
ep, err := w.EpochForRoad(150)
if err != nil {
t.Fatalf("EpochForRoad(150): %v", err)
}
if ep.EpochIndex() != current.EpochIndex() {
t.Fatalf("got epoch %d (Prev with OpenRoadRange masked Current), want current (%d)",
ep.EpochIndex(), current.EpochIndex())
}
}

func TestEpochForRoad_AbsentPrevSkipped(t *testing.T) {
_, current := testDuoEpochs(t)
w := types.EpochDuo{Current: current}
_, err := w.EpochForRoad(50)
if !errors.Is(err, types.ErrRoadBeforeWindow) {
t.Fatalf("EpochForRoad(50) with absent Prev = %v, want ErrRoadBeforeWindow", err)
}
}

func TestEpochForRoad_BeforeAndAfterWithPrev(t *testing.T) {
prev, current := testDuoEpochs(t)
w := types.EpochDuo{Prev: utils.Some(prev), Current: current}
if _, err := w.EpochForRoad(50); err != nil {
t.Fatalf("EpochForRoad(50) in prev: %v", err)
}
if _, err := w.EpochForRoad(150); err != nil {
t.Fatalf("EpochForRoad(150) in current: %v", err)
}
_, err := w.EpochForRoad(200)
if !errors.Is(err, types.ErrRoadAfterWindow) {
t.Fatalf("EpochForRoad(200) = %v, want ErrRoadAfterWindow", err)
}
}

func TestWindowFirst_WithPrev(t *testing.T) {
prev, current := testDuoEpochs(t)
w := types.EpochDuo{Prev: utils.Some(prev), Current: current}
if got, want := w.WindowFirst(), prev.RoadRange().First; got != want {
t.Fatalf("WindowFirst() = %d, want %d", got, want)
}
}

func TestWindowFirst_CurrentOnly(t *testing.T) {
_, current := testDuoEpochs(t)
w := types.EpochDuo{Current: current}
if got, want := w.WindowFirst(), current.RoadRange().First; got != want {
t.Fatalf("WindowFirst() = %d, want %d", got, want)
}
}

func TestEpochOptForRoad(t *testing.T) {
prev, current := testDuoEpochs(t)
w := types.EpochDuo{Prev: utils.Some(prev), Current: current}
if ep, ok := w.EpochOptForRoad(50).Get(); !ok || ep != prev {
t.Fatalf("EpochOptForRoad(50) = %v, want prev", ep)
}
if w.EpochOptForRoad(999).IsPresent() {
t.Fatal("EpochOptForRoad(999) should be None")
}
}

func TestCurrentForRoad(t *testing.T) {
prev, current := testDuoEpochs(t)
w := types.EpochDuo{Prev: utils.Some(prev), Current: current}
if ep, ok := w.CurrentForRoad(150).Get(); !ok || ep != current {
t.Fatalf("CurrentForRoad(150) = %v, want current", ep)
}
if w.CurrentForRoad(50).IsPresent() {
t.Fatal("CurrentForRoad(50) must not admit Prev")
}
}
Loading
Loading