Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
e9ec936
feat(autobahn): multi-epoch Prev|Current window for CommitQC/AppQC (C…
wen-coding Jul 21, 2026
c12aa05
fix(autobahn): clarify restart seed when execution leads tip into N+1…
wen-coding Jul 23, 2026
9dfa24c
docs(autobahn): note >1-epoch tip lead hard-fails on restart (CON-358)
wen-coding Jul 23, 2026
e6bfb4c
refactor(autobahn): rename waitForAppQCEpoch to waitPruneLeash (CON-358)
wen-coding Jul 23, 2026
66604d6
fix(autobahn): count LaneVotes with live duo after epoch advance (CON…
wen-coding Jul 23, 2026
b11b749
fix(autobahn): migrate leftover tests from DataWAL to BlockDB (CON-358)
wen-coding Jul 23, 2026
b1ac945
refactor(autobahn): seed restart epochs from half-open CommitQC RoadR…
wen-coding Jul 23, 2026
b648fb7
fix(autobahn): seed epoch 1 on empty BlockDB restart (CON-358)
wen-coding Jul 23, 2026
1d8bfd6
fix(autobahn): resolve AppProposal epoch by road in giga test (CON-358)
wen-coding Jul 24, 2026
55a41d4
refactor(autobahn): require non-nil Current via NewEpochDuo (CON-358)
wen-coding Jul 24, 2026
24057ae
fix(autobahn): enforce contiguous Prev|Current in NewEpochDuo (CON-358)
wen-coding Jul 24, 2026
cab435e
docs(autobahn): trim EpochDuo comments to contracts and invariants (C…
wen-coding Jul 24, 2026
5db53ed
fix(autobahn): wait tip order before PushQC EpochForRoad (CON-358)
wen-coding Jul 24, 2026
7badc00
fix(autobahn): no-op PushQC before-window on already-applied QC (CON-…
wen-coding Jul 24, 2026
864cc31
docs(autobahn): explain epoch-0 seal prune-leash exemption (CON-358)
wen-coding Jul 24, 2026
4279909
refactor(autobahn): seed restart epochs from CommitQC span only (CON-…
wen-coding Jul 24, 2026
89898b9
fix(autobahn): always placeholder-seed windowLast+2 on restart (CON-358)
wen-coding Jul 24, 2026
a17db0a
fix(autobahn): hard-fail DuoAt when non-genesis Prev is missing (CON-…
wen-coding Jul 24, 2026
6b42802
refactor(autobahn): remove unused EnsureExecTipcut (CON-358)
wen-coding Jul 24, 2026
e441048
refactor(autobahn): DuoAt in newInner and RoadRange.IsLastRoad (CON-358)
wen-coding Jul 24, 2026
d335b4c
fix(autobahn): admit-duo verify snapshot and WaitForDuo gap wake (CON…
wen-coding Jul 24, 2026
5b23ec0
docs(autobahn): clarify Current-only lane vote credits (CON-358)
wen-coding Jul 24, 2026
990ef87
perf(autobahn): avoid QC re-fetch on execute for AdvanceIfNeeded (CON…
wen-coding Jul 24, 2026
b24cd86
fix(autobahn): pass real view EpochDuo into persisted validate (CON-358)
wen-coding Jul 24, 2026
912edba
refactor(autobahn): make blockVotes.reweight void (CON-358)
wen-coding Jul 24, 2026
c4209e1
refactor(autobahn): drop unused Epoch return from fullCommitQC (CON-358)
wen-coding Jul 24, 2026
5f27858
refactor(autobahn): drop highestEpoch; SetupInitialDuo returns error …
wen-coding Jul 24, 2026
cfaed53
fix(autobahn): restart tip check uses avail ≥ data (CON-358)
wen-coding Jul 24, 2026
4d97d68
test(autobahn): make GenRegistry duo-only and rewrite epoch fixtures …
wen-coding Jul 24, 2026
5d49986
refactor(autobahn): use Option for optional nextDuo; drop internal he…
wen-coding Jul 24, 2026
0ec0724
refactor(autobahn): drop unused latest; fold thin twin tests (CON-358)
wen-coding Jul 24, 2026
bf0ef23
refactor(autobahn): trim EpochDuo docs and inline thin helpers (CON-358)
wen-coding Jul 26, 2026
489b3f0
refactor(autobahn): drop unreachable EpochForRoad after-window path (…
wen-coding Jul 26, 2026
ee9b912
fix(autobahn): bind AppQC to epoch and reject Prev Current-era global…
wen-coding Jul 26, 2026
0b66271
fix(autobahn): tipcut AppQC matches main; harden admit and tip checks…
wen-coding Jul 26, 2026
0695c33
fix(autobahn): drop avail≥data restart tip hard-fail (CON-358)
wen-coding Jul 26, 2026
38f9cb5
fix(autobahn): allow tipcut AppQC Current-1 lag; require road < view …
wen-coding Jul 26, 2026
858cc94
fix(autobahn): TestCommitQC AppQC uses prior tipcut road (CON-358)
wen-coding Jul 26, 2026
93ebd40
fix(autobahn): prune-leash epoch 0 seal; harden tip restart paths (CO…
wen-coding Jul 26, 2026
1ad962f
fix(blocksim): stamp tipcut AppQC with prior road (CON-358)
wen-coding Jul 26, 2026
3e07f64
fix(autobahn): align future-AppQC fallback with sibling guards (CON-358)
wen-coding Jul 26, 2026
59dd31a
docs(autobahn): clarify QC admit-then-verify; Debug stale drops (CON-…
wen-coding Jul 26, 2026
e9a8667
Filter tipcut AppQC to the propose epoch duo window.
wen-coding Jul 27, 2026
9dfd7d8
Enforce EpochDuo Prev iff Current>0 and tighten AppQC epoch checks.
wen-coding Jul 27, 2026
3f63fcc
Drop redundant road≥view AppQC clear in buildProposal.
wen-coding Jul 27, 2026
b6901f4
Inline avail seal leashes; drop duplicate WaitForDuo.
wen-coding Jul 27, 2026
814e954
Align PushAppVote with admit-then-verify parking.
wen-coding Jul 27, 2026
9f26556
Keep one LaneQC per blockVotes; drop zero-weight on reweight.
wen-coding Jul 28, 2026
50b19f0
Park PushVote until Current accepts the signer.
wen-coding Jul 28, 2026
eb0e700
Address pompon0 avail API/structure nits.
wen-coding Jul 28, 2026
a1ecb40
Avoid uint64 wrap in AppQC Current-1 epoch checks.
wen-coding Jul 28, 2026
e55cc61
Fix seidroid blockers and AppQC lint.
wen-coding Jul 28, 2026
112ef30
Remove FirstBlock/FirstTimestamp from Epoch.
wen-coding Jul 28, 2026
aebafda
Fix NewEpoch call sites in sei-db after Epoch floor removal.
wen-coding Jul 28, 2026
e291028
Wait for in-window AppQC before tipcut propose; reject stale fallback.
wen-coding Jul 28, 2026
c1c6afe
Park PushVote until Current accepts the signer.
wen-coding Jul 28, 2026
ffa0484
Index competing headers in byHash after LaneQC for reconstruction.
wen-coding Jul 28, 2026
e4786d1
Deduplicate AppQC window, seal, and road-wait helpers.
wen-coding Jul 28, 2026
97b80e5
Narrow tipcut AppQC wait API; keep in-window comments.
wen-coding Jul 28, 2026
b3512c9
Share waitRoadOrDropStale for Push* admit soft-drops.
wen-coding Jul 28, 2026
bf0b1d3
Move road admit classification onto EpochDuo.
wen-coding Jul 28, 2026
13b615d
Name Current vs Duo admit paths; seal AppQC on Current only.
wen-coding Jul 28, 2026
bf3c445
Share insertCommitQCAtTip for tipcut seal+insert under lock.
wen-coding Jul 28, 2026
8c9299d
Address review nits on EpochDuo admit logging and App window checks.
wen-coding Jul 28, 2026
3508272
Restore per-hash LaneQC without caching qc on the vote set.
wen-coding Jul 28, 2026
d4ab639
Restore nextBlockToPersist comment explaining persist cursor semantics.
wen-coding Jul 29, 2026
ad560e6
Restore trimmed main comments on errors, admit capacity, and PushBlock.
wen-coding Jul 29, 2026
0febb30
Rename waitCurrentRoad/waitDuoRoad to waitForEpoch/waitForEpochDuo.
wen-coding Jul 29, 2026
a9922d9
Move avail epoch seal out of Push into a Run advance loop.
wen-coding Jul 29, 2026
0e9caf9
Align tipcut AppQC with interlocking: optional attach, seal leashes o…
wen-coding Jul 29, 2026
30f14d1
Fix flaky data client-server test isolation
wen-coding Jul 29, 2026
6bec193
Drop lane votes invalidated by epoch advance
wen-coding Jul 29, 2026
bdb2fd3
Clarify AppQC filtering comments
wen-coding Jul 29, 2026
18f657f
Harden epoch restart and persistence snapshots
wen-coding Jul 29, 2026
8f3cf44
Tighten Autobahn test and error diagnostics
wen-coding Jul 29, 2026
0c5dcb1
Adapt epoch recovery to unified BlockDB iterator
wen-coding Jul 29, 2026
2f75f9c
Harden AppProposal and availability epoch handling
wen-coding Jul 30, 2026
9a770b3
Split availability state by responsibility
wen-coding Jul 30, 2026
bbe37e1
Make optional AppProposal verification panic-safe
wen-coding Jul 30, 2026
1c3af75
Adapt recovery tests to epoch-aware registry
wen-coding Jul 30, 2026
3194e5c
Clarify availability state ownership
wen-coding Jul 30, 2026
a13b120
Simplify single-field progress wrappers
wen-coding Jul 30, 2026
7600dca
Merge branch 'main' into wen/autobahn_epoch_trio
wen-coding Jul 31, 2026
7213d04
Restore rpc import after main merge
wen-coding Jul 31, 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: 1 addition & 3 deletions sei-db/ledger_db/block/block_db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1484,8 +1484,6 @@ const (
testSeed = 20260615
)

var genesisTime = time.Unix(1_700_000_000, 0)

// batch is a contiguous run of blocks at global numbers [first, next) together
// with the QC that finalizes them. next == first+len(blocks).
type batch struct {
Expand Down Expand Up @@ -1577,7 +1575,7 @@ func buildFullCommitQC(
} else {
appQC = utils.None[*types.AppQC]()
}
ep := types.NewEpoch(0, types.OpenRoadRange(), genesisTime, committee, 0)
ep := types.NewEpoch(0, types.OpenRoadRange(), committee)
cqc := types.BuildCommitQC(ep, keys, prev, laneQCs, appQC)
return types.NewFullCommitQC(cqc, headers), blockList
}
Expand Down
30 changes: 22 additions & 8 deletions sei-db/ledger_db/block/blocksim/block_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,29 @@ 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.NewEpochDuo(
types.NewEpoch(0, types.OpenRoadRange(), committee),
utils.None[*types.Epoch](),
),
GenesisFirstBlock: 0,
GenesisTimestamp: genesisTime,
}
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())
return utils.Some(g.fakeAppQC(p))
}
return utils.None[*types.AppQC]()
}()
// AppQC certifies the prior tipcut (road == prev.Index), not the tipcut
// being built. buildProposal drops AppQCs whose block is not finalized;
// Verify additionally requires road < view.
var appQC utils.Option[*types.AppQC]
if cqc, ok := prev.Get(); ok {
p := types.NewAppProposal(
cqc.GlobalRange().Next-1,
cqc.Proposal().Index(),
types.AppHash(g.rand.Bytes(hashSizeBytes)),
cqc.Proposal().EpochIndex(),
)
appQC = utils.Some(g.fakeAppQC(p))
}
proposal := utils.OrPanic1(types.NewProposalForTesting(
committee,
viewSpec,
Expand Down
1 change: 1 addition & 0 deletions sei-tendermint/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Within sei-tendermint subdirectory
optional uint64 index = 1; // required
optional AppProposal app = 4; // optional
* 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.
* 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.
13 changes: 11 additions & 2 deletions sei-tendermint/autobahn/types/app_qc.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,17 @@ func (m *AppQC) Next() RoadIndex {
return m.Proposal().Next()
}

// Verify verifies the AppQC against the committee.
func (m *AppQC) Verify(c *Committee) error {
// Verify checks epoch_index / road against ep, then quorum under ep's committee.
func (m *AppQC) Verify(ep *Epoch) error {
p := m.Proposal()
if got, want := p.EpochIndex(), ep.EpochIndex(); got != want {
return fmt.Errorf("appQC epoch %d, want %d", got, want)
}
if rr := ep.RoadRange(); !rr.Has(p.RoadIndex()) {
return fmt.Errorf("app road_index %v not in epoch %d roads [%v,%v)",
p.RoadIndex(), ep.EpochIndex(), rr.First, rr.Next)
}
c := ep.Committee()
return m.vote.verifyQC(c, c.AppQuorum(), m.sigs)
}

Expand Down
23 changes: 23 additions & 0 deletions sei-tendermint/autobahn/types/app_qc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package types

import (
"testing"

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

func TestAppQCVerifyChecksEpochAndRoad(t *testing.T) {
rng := utils.TestRng()
committee, keys := GenCommittee(rng, 4)
ep := NewEpoch(1, RoadRange{First: 100, Next: 200}, committee)

ok := makeAppQCFor(keys, 0, 150, GenAppHash(rng), 1)
require.NoError(t, ok.Verify(ep))

wrongEpoch := makeAppQCFor(keys, 0, 150, GenAppHash(rng), 0)
require.Error(t, wrongEpoch.Verify(ep))

wrongRoad := makeAppQCFor(keys, 0, 50, GenAppHash(rng), 1)
require.Error(t, wrongRoad.Verify(ep))
}
4 changes: 4 additions & 0 deletions sei-tendermint/autobahn/types/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ type GlobalBlock struct {
Payload *Payload
// Highest known finalized state.
FinalAppState utils.Option[*AppProposal]
// Covering CommitQC road (from assembleGlobalBlock).
RoadIndex RoadIndex
// True when GlobalNumber is last in that QC's GlobalRange.
LastInCommitQC bool
}

// NewBlock creates a new Block.
Expand Down
14 changes: 7 additions & 7 deletions sei-tendermint/autobahn/types/committee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,10 @@ func TestPrepareQCVerifyChecksEpochBinding(t *testing.T) {

require.NoError(t, sign(ProposalAt(ep, View{Index: ep.RoadRange().First})).Verify(ep))

wrongEpoch := newProposal(View{Index: ep.RoadRange().First, EpochIndex: ep.EpochIndex() + 1}, time.Time{}, nil, utils.None[*AppProposal](), ep.FirstBlock())
wrongEpoch := newProposal(View{Index: ep.RoadRange().First, EpochIndex: ep.EpochIndex() + 1}, time.Time{}, nil, utils.None[*AppProposal](), 1)
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](), 1)
require.Error(t, sign(outOfRoads).Verify(ep))
}

Expand All @@ -145,10 +145,10 @@ func TestCommitQCVerifyChecksEpochBinding(t *testing.T) {

require.NoError(t, sign(ProposalAt(ep, View{Index: ep.RoadRange().First})).Verify(ep))

wrongEpoch := newProposal(View{Index: ep.RoadRange().First, EpochIndex: ep.EpochIndex() + 1}, time.Time{}, nil, utils.None[*AppProposal](), ep.FirstBlock())
wrongEpoch := newProposal(View{Index: ep.RoadRange().First, EpochIndex: ep.EpochIndex() + 1}, time.Time{}, nil, utils.None[*AppProposal](), 1)
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](), 1)
require.Error(t, sign(outOfRoads).Verify(ep))
}

Expand All @@ -171,18 +171,18 @@ func TestCommitQCVerifyChecksWeight(t *testing.T) {
func TestAppQCVerifyChecksWeight(t *testing.T) {
rng := utils.TestRng()
ep, keys := makeEpoch(rng)
vote := NewAppVote(NewAppProposal(0, 0, GenAppHash(rng), ep.EpochIndex()))
vote := NewAppVote(NewAppProposal(0, ep.RoadRange().First, GenAppHash(rng), ep.EpochIndex()))

heavyOnly := NewAppQC([]*Signed[*AppVote]{
Sign(keys[0], vote),
})
require.NoError(t, heavyOnly.Verify(ep.Committee()))
require.NoError(t, heavyOnly.Verify(ep))

lightMajority := NewAppQC([]*Signed[*AppVote]{
Sign(keys[1], vote),
Sign(keys[2], vote),
})
require.Error(t, lightMajority.Verify(ep.Committee()))
require.Error(t, lightMajority.Verify(ep))
}

func TestTimeoutQCVerifyChecksEpochBinding(t *testing.T) {
Expand Down
49 changes: 23 additions & 26 deletions sei-tendermint/autobahn/types/epoch.go
Original file line number Diff line number Diff line change
@@ -1,51 +1,48 @@
package types

import (
"time"

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

// 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.
// Use in tests and genesis epochs where no upper bound is known yet.
func OpenRoadRange() RoadRange { return RoadRange{First: 0, Last: utils.Max[RoadIndex]()} }
// OpenRoadRange returns [0, Max) for tests/genesis when no upper bound is known yet.
func OpenRoadRange() RoadRange { return RoadRange{First: 0, Next: utils.Max[RoadIndex]()} }

func (r RoadRange) Has(idx RoadIndex) bool { return idx >= r.First && idx < r.Next }

// 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 }
// IsLastRoad is true when idx+1 == Next (same shape as GlobalRange.IsLastBlock).
func (r RoadRange) IsLastRoad(idx RoadIndex) bool { return idx+1 == r.Next }

// Epoch holds the complete context for a single epoch.
// Retrieved from the local Registry; never transmitted on the wire.
//
// First global block / timestamp floors are not on Epoch: they come from the
// last CommitQC of the prior epoch (or genesis GenDoc via Registry), and the
// registry does not store CommitQCs.
type Epoch struct {
utils.ReadOnly
epochIndex EpochIndex
roads RoadRange
firstTimestamp time.Time
committee *Committee
firstBlock GlobalBlockNumber
epochIndex EpochIndex
roads RoadRange
committee *Committee
}

// NewEpoch constructs an Epoch.
func NewEpoch(index EpochIndex, roads RoadRange, firstTimestamp time.Time, committee *Committee, firstBlock GlobalBlockNumber) *Epoch {
func NewEpoch(index EpochIndex, roads RoadRange, committee *Committee) *Epoch {
return &Epoch{
epochIndex: index,
roads: roads,
firstTimestamp: firstTimestamp,
committee: committee,
firstBlock: firstBlock,
epochIndex: index,
roads: roads,
committee: committee,
}
}

func (e *Epoch) EpochIndex() EpochIndex { return e.epochIndex }
func (e *Epoch) RoadRange() RoadRange { return e.roads }
func (e *Epoch) FirstTimestamp() time.Time { return e.firstTimestamp }
func (e *Epoch) Committee() *Committee { return e.committee }
func (e *Epoch) FirstBlock() GlobalBlockNumber { return e.firstBlock }
func (e *Epoch) EpochIndex() EpochIndex { return e.epochIndex }
func (e *Epoch) RoadRange() RoadRange { return e.roads }
func (e *Epoch) Committee() *Committee { return e.committee }
103 changes: 103 additions & 0 deletions sei-tendermint/autobahn/types/epoch_duo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package types

import (
"errors"
"fmt"

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

// EpochDuo is a Prev|Current epoch window. Current is always set; Prev is
// absent iff Current is epoch 0, otherwise contiguous with Current (index
// Current-1 and Prev.Next == Current.First).
type EpochDuo struct {
Prev utils.Option[*Epoch]
Current *Epoch
}

// NewEpochDuo builds a Prev|Current window. Panics unless
// prev≠None ⇔ current.EpochIndex()>0, and when Prev is present it must be
// contiguous with Current.
func NewEpochDuo(current *Epoch, prev utils.Option[*Epoch]) EpochDuo {
Comment thread
wen-coding marked this conversation as resolved.
cur := current.EpochIndex()
p, hasPrev := prev.Get()
if hasPrev != (cur > 0) {
panic(fmt.Sprintf("NewEpochDuo: Prev present=%v but Current epoch %d (want Prev iff Current>0)",
hasPrev, cur))
}
if hasPrev {
if p.EpochIndex()+1 != cur {
panic(fmt.Sprintf("NewEpochDuo: Prev epoch %d not contiguous with Current %d",
p.EpochIndex(), cur))
}
if got, want := p.RoadRange().Next, current.RoadRange().First; got != want {
panic(fmt.Sprintf("NewEpochDuo: Prev roads end at %d, Current starts at %d", got, want))
}
}
return EpochDuo{Prev: prev, Current: current}
}

var ErrRoadBeforeWindow = errors.New("road before epoch duo window")
var ErrRoadAfterWindow = errors.New("road after epoch duo window")

// RoadStatus classifies a road relative to a Prev|Current window for admit waits.
type RoadStatus int

const (
RoadReady RoadStatus = iota // in the admitted window
RoadStale // behind the window (soft-drop / ErrPruned)
RoadFuture // ahead of the window (backpressure wait)
)

// RoadStatusCurrent classifies roadIdx against Current only (CommitQC tip).
func (w EpochDuo) RoadStatusCurrent(roadIdx RoadIndex) RoadStatus {
if w.Current.RoadRange().Has(roadIdx) {
return RoadReady
}
if roadIdx < w.Current.RoadRange().First {
return RoadStale
}
return RoadFuture
}

// RoadStatusDuo classifies roadIdx against Prev|Current (AppVote/AppQC).
func (w EpochDuo) RoadStatusDuo(roadIdx RoadIndex) RoadStatus {
_, err := w.EpochForRoad(roadIdx)
if err == nil {
return RoadReady
}
if errors.Is(err, ErrRoadBeforeWindow) {
return RoadStale
}
return RoadFuture
}

// EpochForRoad returns the epoch containing roadIdx.
// Window is [Prev.First or Current.First, Current.Next). Outside →
// ErrRoadBeforeWindow / ErrRoadAfterWindow. Under contiguous Prev|Current there
// is no gap, so a miss after the after-window check is always before-window.
func (w EpochDuo) EpochForRoad(roadIdx RoadIndex) (*Epoch, error) {
if roadIdx >= w.Current.RoadRange().Next {
return nil, fmt.Errorf("road %d after window %v: %w", roadIdx, w, ErrRoadAfterWindow)
}
if w.Current.RoadRange().Has(roadIdx) {
return w.Current, nil
}
if prev, ok := w.Prev.Get(); ok && prev.RoadRange().Has(roadIdx) {
return prev, nil
}
return nil, fmt.Errorf("road %d before window %v: %w", roadIdx, w, ErrRoadBeforeWindow)
}

func (w EpochDuo) String() string {
s := "epochs ["
sep := ""
// Prev then Current so a window {4,5} prints as "epochs [4, 5]".
for _, oep := range [2]utils.Option[*Epoch]{w.Prev, utils.Some(w.Current)} {
if ep, ok := oep.Get(); ok {
s += fmt.Sprintf("%s%d", sep, ep.EpochIndex())
sep = ", "
}
}
return s + "]"
}
Loading
Loading