Skip to content
Draft

WIP #3834

Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 10 additions & 45 deletions sei-tendermint/autobahn/types/epoch_duo.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,75 +18,40 @@ type EpochDuo struct {
// 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 {
func NewEpochDuo(current *Epoch, prev utils.Option[*Epoch]) (EpochDuo,error) {
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))
return EpochDuo{},fmt.Errorf("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))
return EpochDuo{},fmt.Errorf("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))
panic(fmt.Errorf("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
return EpochDuo{Prev: prev, Current: current},nil
}

// 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) {
func (w EpochDuo) ByRoad(roadIdx RoadIndex) (*Epoch, error) {
if roadIdx >= w.Current.RoadRange().Next {
return nil, fmt.Errorf("road %d after window %v: %w", roadIdx, w, ErrRoadAfterWindow)
return nil, errors.New("road belongs to future epoch")
}
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)
return nil, ErrPruned
}

func (w EpochDuo) String() string {
Expand Down
114 changes: 34 additions & 80 deletions sei-tendermint/internal/autobahn/avail/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package avail

import (
"context"
"errors"
"fmt"

"github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types"
Expand All @@ -10,73 +11,43 @@ import (

// appProgress owns the in-memory AppQC tip and AppVote accumulators.
type appProgress struct {
latestAppQC utils.Option[*types.AppQC]
votes *queue[types.GlobalBlockNumber, appVotes]
anchor utils.Option[*PruneAnchor]
votes *queue[types.GlobalBlockNumber, appVotes]
}

// LastAppQC returns the latest observed AppQC.
func (s *State) LastAppQC() utils.Option[*types.AppQC] {
for inner := range s.inner.Lock() {
return inner.app.latestAppQC
}
panic("unreachable")
}

// WaitForAppQC waits until there is an AppQC for the given index or higher.
// Returns this AppQC and the corresponding CommitQC.
// Together they provide enough information to prune the availability state.
func (s *State) WaitForAppQC(ctx context.Context, idx types.RoadIndex) (*types.AppQC, *types.CommitQC, error) {
for inner, ctrl := range s.inner.Lock() {
for {
if appQC, ok := inner.app.latestAppQC.Get(); ok {
if x := appQC.Proposal().RoadIndex(); x >= idx && inner.commits.qcs.next > x {
return appQC, inner.commits.qcs.q[x], nil
}
}
if err := ctrl.Wait(ctx); err != nil {
return nil, nil, err
}
if anchor,ok := inner.app.anchor.Get(); ok {
return utils.Some(anchor.AppQC)
}
}
panic("unreachable")
return utils.None[*types.AppQC]()
}

// PushAppVote pushes an AppVote to the state.
// Same admit-then-verify as PushAppQC: far-future roads park until the duo
// and CommitQC tip catch up (one stream goroutine; does not block others).
func (s *State) PushAppVote(ctx context.Context, v *types.Signed[*types.AppVote]) error {
idx := v.Msg().Proposal().RoadIndex()
// A vote may arrive before its CommitQC advances the tip.
if err := s.waitForCommitQC(ctx, idx); err != nil {
return err
}
// Too-early roads (ahead of Prev|Current) backpressure; too-late are dropped.
admitted, err := s.waitForEpochDuoOrDropStale(ctx, "AppVote", idx)
qc,epoch,err := s.commitQCAndEpoch(ctx, v.Msg().Proposal().RoadIndex())
if err != nil {
if errors.Is(err,types.ErrPruned) {
return nil
}
return err
}
duo, ok := admitted.Get()
if !ok {
return nil
}
ep := utils.OrPanic1(duo.EpochForRoad(idx))
if got, want := v.Msg().Proposal().EpochIndex(), ep.EpochIndex(); got != want {
return fmt.Errorf("appVote epoch_index %d, want %d", got, want)
if err := v.Msg().Proposal().Verify(qc); err != nil {
return fmt.Errorf("invalid vote: %w", err)
}
committee := ep.Committee()
committee := epoch.Committee()
if err := v.VerifySig(committee); err != nil {
return fmt.Errorf("v.VerifySig(): %w", err)
}
for inner, ctrl := range s.inner.Lock() {
// Early exit if not useful (we collect <=1 AppQC per road index).
if idx < types.NextOpt(inner.app.latestAppQC) {
if qc.Index() < types.NextOpt(inner.app.anchor) {
return nil
}
// Verify the vote against the CommitQC.
qc := inner.commits.qcs.q[idx]
if err := v.Msg().Proposal().Verify(qc); err != nil {
return fmt.Errorf("invalid vote: %w", err)
}
// Push the vote.
n := v.Msg().Proposal().GlobalNumber()
q := inner.app.votes
Expand All @@ -87,30 +58,34 @@ func (s *State) PushAppVote(ctx context.Context, v *types.Signed[*types.AppVote]
if !ok {
return nil
}
updated, err := inner.pushPruneAnchor(&PruneAnchor{AppQC: appQC, CommitQC: qc})
if err != nil {
return err
}
// Anchor is always valid here.
updated := utils.OrPanic1(inner.pushPruneAnchor(&PruneAnchor{AppQC: appQC, CommitQC: qc, Epoch: epoch}))
if updated {
ctrl.Updated()
}
}
return nil
}

// PushAppQC requires a justifying CommitQC. Epoch slide is async in
// runAdvanceEpoch (same as PushCommitQC). Prune before insert so latestAppQC is
// visible before the advance task observes the new tip.
//
// Same admit-then-verify as PushCommitQC.
// PushAppQC requires a justifying CommitQC.
// Prunes state up to AppQC.Proposal().Index().
func (s *State) PushAppQC(ctx context.Context, appQC *types.AppQC, commitQC *types.CommitQC) error {
// If epoch is from the future then we are unable to process AppQC.
// If epoch is pruned, then it is from the past and there is no point participating in the consensus.
epoch, err := s.data.Registry().WaitForEpoch(ctx, appQC.Proposal().EpochIndex())
if err != nil {
if errors.Is(err,types.ErrPruned) {
return nil
}
return err
}
// Check whether it is needed before verifying.
for inner := range s.inner.Lock() {
if types.NextOpt(inner.app.latestAppQC) > appQC.Proposal().RoadIndex() {
if types.NextOpt(inner.app.anchor) >= appQC.Next() {
return nil
}
}
// Pair consistency only; ahead-of-window still waits in waitForEpochDuo.
// Verify appQC <-> commitQC consistency.
if appQC.Proposal().RoadIndex() != commitQC.Proposal().Index() {
return fmt.Errorf("mismatched QCs: appQC index %v, commitQC index %v", appQC.Proposal().RoadIndex(), commitQC.Proposal().Index())
}
Expand All @@ -120,38 +95,17 @@ func (s *State) PushAppQC(ctx context.Context, appQC *types.AppQC, commitQC *typ
if !commitQC.GlobalRange().Has(appQC.Proposal().GlobalNumber()) {
return fmt.Errorf("appQC GlobalNumber not in commitQC range")
}
idx := commitQC.Proposal().Index()
admitted, err := s.waitForEpochDuoOrDropStale(ctx, "AppQC", idx)
if err != nil {
return err
}
duo, ok := admitted.Get()
if !ok {
return nil
}
ep := utils.OrPanic1(duo.EpochForRoad(idx))
if err := appQC.Verify(ep); err != nil {
if err := appQC.Verify(epoch); err != nil {
return fmt.Errorf("appQC.Verify(): %w", err)
}
if err := commitQC.Verify(ep); err != nil {
if err := commitQC.Verify(epoch); err != nil {
return fmt.Errorf("commitQC.Verify(): %w", err)
}
// Seal CommitQC paired with this AppQC: incoming AppQC satisfies the prune
// leash; still wait on the execution leash when idx closes Current.
if duo.Current.RoadRange().Next-1 == idx && ep.EpochIndex() == duo.Current.EpochIndex() {
if err := s.waitSealLeashes(ctx, duo.Current, idx, utils.Some(appQC.Proposal().EpochIndex())); err != nil {
return err
}
}
anchor := &PruneAnchor{AppQC: appQC, CommitQC: commitQC, Epoch: epoch}
for inner, ctrl := range s.inner.Lock() {
updated, err := inner.pushPruneAnchor(&PruneAnchor{AppQC: appQC, CommitQC: commitQC})
if err != nil {
return err
}
if !updated {
return nil
if utils.OrPanic1(inner.pushPruneAnchor(anchor)) {
ctrl.Updated()
}
ctrl.Updated()
}
return nil
}
3 changes: 1 addition & 2 deletions sei-tendermint/internal/autobahn/avail/block_votes.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ func (bv *blockVotes) pushVote(ep *types.Epoch, vote *types.Signed[*types.LaneVo
// reweight recomputes already-stored votes under new Current after advanceEpoch.
// Zero-weight signers are removed from byKey. Callers wake waiters via
// ctrl.Updated() after advanceEpoch (not via a return flag).
func (bv *blockVotes) reweight(newEpoch *types.Epoch) {
c := newEpoch.Committee()
func (bv *blockVotes) reweight(c *types.Committee) {
clear(bv.byHash)
quorum := c.LaneQuorum()
for k, vote := range bv.byKey {
Expand Down
61 changes: 32 additions & 29 deletions sei-tendermint/internal/autobahn/avail/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@ func (s *State) waitForCommitQC(ctx context.Context, idx types.RoadIndex) error
return err
}

func (s *State) commitQCAndEpoch(ctx context.Context, idx types.RoadIndex) (*types.CommitQC, *types.Epoch, error) {
if err := s.waitForCommitQC(ctx, idx); err != nil {
return nil, nil, err
}
for inner := range s.inner.Lock() {
if idx < inner.commits.qcs.first {
return nil, nil, types.ErrPruned
}
qc := inner.commits.qcs.q[idx]
if epoch := inner.epoch.Load(); epoch.EpochIndex()==qc.Proposal().EpochIndex() {
return qc,epoch,nil
}
return qc,inner.app.anchor.OrPanic("missing anchor").Epoch,nil
}
panic("unreachable")
}

// CommitQC returns the CommitQC for the given index.
func (s *State) CommitQC(ctx context.Context, idx types.RoadIndex) (*types.CommitQC, error) {
if err := s.waitForCommitQC(ctx, idx); err != nil {
Expand All @@ -40,55 +57,41 @@ func (s *State) CommitQC(ctx context.Context, idx types.RoadIndex) (*types.Commi
//
// Admit-then-verify is intentional backpressure for ahead-of-window QCs.
func (s *State) PushCommitQC(ctx context.Context, qc *types.CommitQC) error {
idx := qc.Proposal().Index()
if idx > 0 {
if err := s.waitForCommitQC(ctx, idx-1); err != nil {
// Await previous CommitQC.
if i := qc.Proposal().Index(); i>0 {
if err:=s.waitForCommitQC(ctx,i-1); err!=nil {
return err
}
}
admitted, err := s.waitForEpochOrDropStale(ctx, "CommitQC", idx)
// Await Epoch.
epoch, err := s.Epoch(ctx, qc.Proposal().EpochIndex())
if err != nil {
if errors.Is(err,types.ErrPruned); err!=nil {
return nil
}
return err
}
duo, ok := admitted.Get()
if !ok {
return nil
}
ep := duo.Current
if err := qc.Verify(ep); err != nil {
// Verify qc.
if err := qc.Verify(epoch); err != nil {
return fmt.Errorf("qc.Verify(): %w", err)
}
if err := s.waitSealLeashes(ctx, ep, idx, utils.None[types.EpochIndex]()); err != nil {
return err
}

// Push.
for inner, ctrl := range s.inner.Lock() {
if !inner.commits.push(qc) {
return nil
if inner.commits.push(qc) {
ctrl.Updated()
}
// persistedCommitQC advances only after durable persist (or no-op persister).
ctrl.Updated()
return nil
}
return nil
}

// fullCommitQC returns the FullCommitQC for road n.
// ErrRoadBeforeWindow → ErrPruned (export may jump ahead). ErrRoadAfterWindow hard-fails.
func (s *State) fullCommitQC(ctx context.Context, n types.RoadIndex) (*types.FullCommitQC, error) {
qc, err := s.CommitQC(ctx, n)
qc, epoch, err := s.commitQCAndEpoch(ctx, n)
if err != nil {
return nil, err
}
ep, err := s.epochDuo.Load().EpochForRoad(qc.Proposal().Index())
if err != nil {
if errors.Is(err, types.ErrRoadBeforeWindow) {
return nil, types.ErrPruned
}
return nil, err
}
var commitHeaders []*types.BlockHeader
for lane := range ep.Committee().Lanes().All() {
for lane := range epoch.Committee().Lanes().All() {
headers, err := s.headers(ctx, qc.LaneRange(lane))
if err != nil {
return nil, err
Expand Down
Loading
Loading