Skip to content

Complete multi-epoch ownership across Autobahn layers (CON-358) - #3929

Open
wen-coding wants to merge 12 commits into
mainfrom
wen/autobahn_multi_epoch
Open

Complete multi-epoch ownership across Autobahn layers (CON-358)#3929
wen-coding wants to merge 12 commits into
mainfrom
wen/autobahn_multi_epoch

Conversation

@wen-coding

Copy link
Copy Markdown
Contributor

Summary

  • bound epochs to road ranges and seed the registry across retained CommitQC history
  • make data own CommitQC epochs, avail own applied epoch rotation and backleash, and consensus consume avail's durable ConsensusSpec
  • preserve and recount lane votes across committee changes, including leave/rejoin catch-up, while driving validator and fullnode EVM proxies from data's CommitQC epoch

Test plan

  • go test ./sei-tendermint/autobahn/types/ ./sei-tendermint/internal/autobahn/... ./sei-tendermint/internal/p2p/ -count=1

Made with Cursor

wen-coding and others added 7 commits August 14, 2026 13:53
Publish the next-CommitQC epoch for observation (WaitLane / giga sit on
it) and cover subscribe stay, leave, and rejoin with a mid-Recv stay apply.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use roads[first].epoch when retained CommitQCs exist, otherwise
inner.epoch — no registry lookup or Option error path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Deletion follows the Anchor epoch: prune takes that epoch from the
matching road (registry once at startup), and collectPersistBatch no
longer wakes or drops on hasClosedLane.

Co-authored-by: Cursor <cursoragent@cursor.com>
PruneAndPersist matches main again (BlockNumber, always create). runEvict
only drops maps; runPersist SyncLanes is the sole runtime WAL deleter.

Co-authored-by: Cursor <cursoragent@cursor.com>
Prefer the sealed road's epoch while it is retained; fall back to the
registry once it is not, so empty-tip / data-ahead Anchors still prune
instead of tearing down State.Run.

Co-authored-by: Cursor <cursoragent@cursor.com>
Move lane-lifecycle notes onto State (keeping the original godoc) and
TODO a real validation error for dual-epoch PushVote rejects.

Co-authored-by: Cursor <cursoragent@cursor.com>
Epoch rotation was spread across the layers: consensus re-derived epochs from
the registry, avail exposed a persisted-epoch peek only consensus used, and p2p
read the registry directly. Each layer now owns one step and the next consumes
its published value.

- data seeds the registry, admits CommitQCs with the epoch they verified under,
  and publishes CommitEpoch for the latest admitted CommitQC
- avail applies epochs behind the prune and execution leashes, recounts lane
  votes under the applied committee, and publishes types.ConsensusSpec
- consensus installs the tips it is handed instead of consulting the registry
- p2p EvmProxy reads data.CommitEpoch on both the validator and fullnode paths

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedAug 15, 2026, 6:07 AM

@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.18584% with 76 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.56%. Comparing base (d17806f) to head (7f8e261).

Files with missing lines Patch % Lines
sei-tendermint/internal/autobahn/avail/inner.go 83.51% 10 Missing and 5 partials ⚠️
sei-tendermint/internal/autobahn/avail/testonly.go 70.00% 7 Missing and 8 partials ⚠️
...ei-tendermint/internal/autobahn/consensus/inner.go 69.23% 8 Missing and 4 partials ⚠️
sei-tendermint/internal/autobahn/avail/state.go 82.53% 7 Missing and 4 partials ⚠️
sei-tendermint/internal/autobahn/data/state.go 86.53% 2 Missing and 5 partials ⚠️
...ei-tendermint/internal/autobahn/consensus/state.go 70.00% 4 Missing and 2 partials ⚠️
sei-tendermint/internal/autobahn/epoch/registry.go 94.11% 3 Missing and 1 partial ⚠️
sei-tendermint/internal/p2p/giga/avail.go 57.14% 1 Missing and 2 partials ⚠️
sei-tendermint/autobahn/types/proposal.go 0.00% 2 Missing ⚠️
...endermint/internal/autobahn/avail/subscriptions.go 93.33% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3929      +/-   ##
==========================================
- Coverage   59.51%   58.56%   -0.96%     
==========================================
  Files        2326     2230      -96     
  Lines      199003   188563   -10440     
==========================================
- Hits       118438   110429    -8009     
+ Misses      69300    67701    -1599     
+ Partials    11265    10433     -832     
Flag Coverage Δ
sei-chain-pr 81.39% <83.18%> (?)
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-tendermint/autobahn/types/epoch.go 100.00% <100.00%> (ø)
sei-tendermint/autobahn/types/testonly.go 96.66% <100.00%> (+0.54%) ⬆️
...-tendermint/internal/autobahn/avail/block_votes.go 95.55% <100.00%> (+4.64%) ⬆️
...int/internal/autobahn/consensus/persisted_inner.go 97.50% <100.00%> (-0.10%) ⬇️
sei-tendermint/internal/p2p/giga_router_common.go 58.71% <ø> (ø)
...ei-tendermint/internal/p2p/giga_router_fullnode.go 45.00% <100.00%> (+1.41%) ⬆️
...i-tendermint/internal/p2p/giga_router_validator.go 93.22% <100.00%> (+0.11%) ⬆️
...endermint/internal/autobahn/avail/subscriptions.go 88.05% <93.33%> (-0.08%) ⬇️
sei-tendermint/autobahn/types/proposal.go 91.80% <0.00%> (-0.64%) ⬇️
sei-tendermint/internal/p2p/giga/avail.go 59.58% <57.14%> (+3.16%) ⬆️
... and 7 more

... and 142 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/avail/inner.go
seidroid[bot]
seidroid Bot previously requested changes Aug 14, 2026

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

Substantial and generally well-structured rework of multi-epoch ownership across data/avail/consensus, with good test coverage of the new registry, backleash, and vote-recount paths. One blocking issue: the consensus restore path can install an avail ConsensusSpec tip that is behind the persisted WAL tip, rolling the view backwards and discarding the anti-equivocation vote record; several hot-path locking regressions and a lost CommitQC verification also need attention.

Findings: 1 blocking | 12 non-blocking | 8 posted inline

Blockers

  • None at the file/PR level.
  • 1 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • Registry placeholders (makeEpoch) are stamped with the genesis committee, and loadFromBlockDB verifies retained CommitQCs against them. Once real rotation lands, a restart after a membership/weight change would fail QC verification at startup. The // TODO: replace placeholders with execution-derived epochs acknowledges this and ActivateEpoch still has no production caller, so it is not reachable today — but it is worth capturing as a tracked follow-up rather than only a TODO, since the failure mode is "node will not start". (Codex raised this as P1; downgraded here because the rotation path is not wired.)
  • Related: ActivateEpoch overwrites an existing placeholder at index latest+1, but data.qcEntry.epoch, avail.road.epoch and Anchor.Epoch already hold pointers to the placeholder. After a real rotation those stashed pointers and the registry entry for the same index would disagree. Worth an explicit invariant (e.g. reject ActivateEpoch for an index already handed out) before rotation is enabled.
  • Registry.state moved from RWMutex to Watch, so EpochByIndex/EpochAt/LatestEpoch/FirstBlock now take an exclusive lock. WaitForEpoch needs the update signal, but the pure reads could stay on a read path; consider keeping an RWMutex plus a separate notification channel if registry reads show up in profiles.
  • RoadRange.IsLastRoad and GlobalRange.IsLastBlock are added but never used outside epoch_test.go (IsLastBlock has no test at all). Either wire them into the boundary checks that currently open-code idx+1 == r.Next / p.Index() == LastRoad(...), or drop them.
  • No test covers restart with the durable avail tip sitting exactly on LastRoad(M) — the case that triggers the ConsensusSpec walk-back in refreshConsensusSpec. Given that path is the one that produces the view rollback flagged inline, it deserves a direct newInner test.
  • 7 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread sei-tendermint/internal/autobahn/consensus/inner.go
return inner{}, err
}
logger.Info("restored consensus state", "state", innerProtoConv.Encode(&persisted))
return inner{persistedInner: persisted, epoch: spec.Epoch}, nil

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] The persisted CommitQC is now never signature-verified on restore: persistedInner.validate dropped its cqc.Verify(ep) call, this branch returns persisted.CommitQC (not spec.CommitQC), and TestNewInnerCommitQCInvalidSignatureError was deleted. sameTip only compares indices, so a corrupt or tampered WAL entry at the right index is accepted and then used as justification for the node's next proposal.

Since the two are known to be at the same index and spec.CommitQC is the avail-verified one, installing it here keeps the check without re-verifying:

if sameTip {
    if err := persisted.validate(spec.Epoch); err != nil { return inner{}, err }
    persisted.CommitQC = spec.CommitQC // avail-verified
    ...
}

Worth noting that TimeoutQC.Verify(viewEp, p.CommitQC) justifies against p.CommitQC, so an unverified value there weakens that check too.

n := h.BlockNumber()
for inner, ctrl := range s.inner.Lock() {
applied := inner.epoch.Load()
if !laneProposalAccepted(applied, p) &&

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] Block verification moved from outside the lock (the removed Registry().VerifyInWindow(...) call) to inside s.inner.Lock(). laneProposalAccepted runs block.Verify (which hashes the entire payload, including every tx) plus p.VerifySig, and on the || fallback it runs both a second time against anchorEpoch. That is now serialized on the single mutex that also guards PushVote, PushCommitQC, RecvBatch, headers, and the persist loop, for every inbound block from every peer — a meaningful throughput/latency regression and an easy remote stall vector.

The epochs needed for verification (applied, anchorEpoch) can be snapshotted under the lock and the verification done outside it, as PushCommitQC does with waitBackleash + qc.Verify.

Separately: verification failure now return nil (silent drop) where it previously returned fmt.Errorf("block.Verify(): %w", err), so a peer sending unverifiable blocks produces no error at all. The PushVote TODO about returning a meaningful validation error was narrowed to "accept future-epoch joiner votes" — the missing-error concern now applies to both paths and is worth keeping in a TODO.

break
votes := make([]*types.Signed[*types.LaneVote], len(batch))
for i, h := range batch {
votes[i] = types.Sign(r.state.key, types.NewLaneVote(h))

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] Restructuring the loop to return from inside the Lock() iterator moved types.Sign under the avail inner mutex; previously the break exited the lock scope and signing happened outside it (the removed TODO(gprusak) sat exactly there).

This is worst on the new rewind path: after a localJoinGen bump, clear(r.next) makes the next batch cover every retained block across every lane (up to BlocksPerLane × lanes), so that many signature operations run back to back while holding the lock that gates all block/vote/QC ingest.

Keep the collection under the lock and the signing outside, e.g. assign batch to a variable declared before the Lock() loop and break out as before, or collect into a local and sign after the iterator ends.

return nil, false
}

func (bv blockVotes) header(want types.BlockHeaderHash) (*types.BlockHeader, bool) {

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] header replaces what was an O(1) byHash[want] map lookup with a linear scan that recomputes h.Hash() (protobuf encode + hash) for every retained vote. headers() calls this once per block in a LaneRange, per lane, per road — so it is O(range × committee) header hashes, and it runs while holding the inner mutex.

The reason for the change is sound (byHash no longer holds zero-applied-weight votes, so it can miss headers), but the fix can keep the index: add a byHeaderHash map[BlockHeaderHash]*BlockHeader populated in pushVote unconditionally (independent of weight, so recount never touches it) and look up there.

if anchorEpoch != nil {
// Tip epoch after prune: keep applied at least the anchor's verify-epoch.
if i.epoch.Load().EpochIndex() < anchorEpoch.EpochIndex() {
i.epoch.Store(anchorEpoch)

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] prune now writes i.epoch directly, which bypasses everything ApplyEpoch does for an epoch change: addLane for the new committee's lanes, recountVotes(ep), and the localJoinGen bump.

Concretely, if anchorEpoch is ahead of applied, after this store laneQC evaluates byHash.weight >= ep.Committee().LaneQuorum() against weights that were accumulated under the old committee, lanes introduced by the new epoch are absent (so PushBlock/PushVote silently drop for them), and a local rejoin is not observed by LaneVotesRecv.

If the intent is only to keep applied from lagging the anchor, routing through the same steps as ApplyEpoch (or calling a shared helper) would make the two paths agree by construction.

Also note this invalidates the comment on line 21: "ApplyEpoch is the sole writer after construction" is no longer true, since prune is also called from runEvict.

return utils.None[*ethrpc.Client]()
}
validator := r.data.Registry().LatestEpoch().Committee().EvmShard(sender)
validator := r.data.CommitEpoch().Load().Committee().EvmShard(sender)

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] CommitEpoch() takes data.State's inner lock to hand back the subscription, so this acquires the central data-plane mutex on every EvmProxy call — i.e. on every routed EVM request. That lock is also held by QC/block insertion, PushAppHash, and the persist/evict loops.

CommitEpoch() returns a stable AtomicRecv whose Load() is lock-free, so capture it once when the router is constructed and store it on gigaValidatorRouter, then just Load() here. Same applies to giga_router_fullnode.go:68.

epochIdx,
types.RoadRange{First: firstRoad, Next: FirstRoad(epochIdx + 1)},
ep0.FirstTimestamp(),
ep0.Committee(),

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] Placeholders are built with ep0.Committee(), ep0.FirstTimestamp() and ep0.FirstBlock(), and loadFromBlockDB verifies retained CommitQCs against whatever EpochByIndex returns. That is correct only while every epoch has the genesis committee.

The TODO above ensureLocked(s, windowLast+1) covers the committee, but the timestamp and first-block fields are silently wrong too, and they feed ViewSpec.NextTimestamp()/NextGlobalBlock() for any view resolved against a placeholder. Worth widening the TODO to name all three fields, or having makeEpoch mark the epoch as a placeholder so a consumer that needs a real timestamp/first-block can fail loudly instead of reading genesis values.

wen-coding and others added 3 commits August 14, 2026 21:51
Catch up applied epoch from durable seal/exec on restart, keep consensus
tip when the avail spec lags, and remove ApplyEpoch so tests drive
advances through the production sync path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Collect headers under the lock, then Sign after unlock so ed25519 does
not serialize on the central avail mutex (especially on rejoin rewind).

Co-authored-by: Cursor <cursoragent@cursor.com>
@wen-coding
wen-coding dismissed seidroid[bot]’s stale review August 15, 2026 05:35

Fixed, please recheck

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

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 c25237f. Configure here.

Comment thread sei-tendermint/internal/autobahn/epoch/registry.go
wen-coding and others added 2 commits August 14, 2026 23:05
ConsensusSpec backleashes the view after LastRoad(applied), so the old
N+2 placeholder lookahead is redundant with runEpochSync catchup.

Co-authored-by: Cursor <cursoragent@cursor.com>
Avoid re-locking data.State on every EvmProxy call; AtomicRecv.Load is
already lock-free once subscribed.

Co-authored-by: Cursor <cursoragent@cursor.com>
@wen-coding

Copy link
Copy Markdown
Contributor Author

@seidroid please re-review

@seidroid

seidroid Bot commented Aug 15, 2026

Copy link
Copy Markdown

Claude finished @wen-coding's task in 5m 13s —— View job


Re-review — wen/autobahn_multi_epoch @ 7f8e261

  • Gather context (diff since previous review commit ec44b37)
  • Re-check blocker: consensus restore installs spec tip behind persisted tip
  • Re-check the 7 inline suggestions
  • Check Cursor's new finding (stale applied epoch after ActivateEpoch)
  • Scan new commits for fresh issues
  • Post updated review

The blocker is fixed. No blocking issues remain. One new finding below plus three carried over.


✅ Resolved since last review

Prior finding Where it landed
[blocker] restore installs a spec tip behind the persisted WAL tip, rolling the view back and dropping the anti-equivocation vote record restoreInner now branches on specViewIdx < persistedViewIdx and keeps the WAL tip + votes (consensus/inner.go:162-174). Covered by TestNewInner_KeepsPersistedTipWhenSpecBehind, which asserts both View().Index == last+1 and that the PrepareVote survives.
Persisted CommitQC never signature-verified on restore newInner re-verifies against the QC's own certify epoch (consensus/inner.go:136-144). Since View.Verify checks both EpochIndex match and RoadRange.Has(Index), a tampered EpochIndex can't steer verification to a friendlier committee. TestNewInner_RejectsUnverifiedCommitQC pins it.
prune writing i.epoch directly, bypassing addLane/recountVotes/localJoinGen prune now only sets anchorEpoch; installEpoch is genuinely the sole writer after construction (avail/inner.go:319-345), and the doc comment on line 26 is accurate again.
types.Sign moved under the avail lock in RecvBatch Split into recvHeaders (under lock) + signing in RecvBatch (avail/subscriptions.go:77-117).
Registry().LatestEpoch() taking the data lock on every EvmProxy call commitEpoch is captured once at router construction; EvmProxy is now a lock-free Load() on both routers.
Original Cursor finding — restart rewinds applied epoch at the boundary newInner floors applied at the durable tip's verify-epoch and then re-drives via advanceEpochs, with AppQCCoversEpoch supplying the exec leash from data when avail's in-memory road AppQCs are empty. TestAdvanceEpochs_BoundaryTipUsesDataAppQC pins exactly that path.

Also worth calling out: myView in newState is now seeded with the restored CommitQC/TimeoutQC instead of a bare ViewSpec{Epoch:} — that was a latent restart bug in its own right.


Non-blocking

1. runEpochSync stops republishing ConsensusSpec while parked in WaitForEpochavail/state.go:698-703 (new this round)

refreshConsensusSpec is documented as having exactly one live caller, and that caller can be parked in a wait that only registry updates can end:

if ei, ok := waitRegistry.Get(); ok {
    if _, err := s.data.Registry().WaitForEpoch(ctx, ei); err != nil { return err }
    continue
}

waitRegistry is set when leashesMet && canAdvance == nil — i.e. all of epoch M's roads are admitted (roads.next >= FirstRoad(M+1)) and exec is satisfied, but M+1 isn't registered yet (AdvanceIfNeeded only fires when execution reaches LastRoad(M)). While parked there, markCommitQCsPersisted can advance persistedCommitQC several roads and bumpEpochWake has no listener, so consensusSpec stays frozen at the older durable tip and consensus sits out the tail views of epoch M.

Reachable when a node admits epoch M's CommitQCs from peers faster than its own WAL flush — the roads.next leash is satisfied by peer QCs, but the spec tracks the durable tip. It self-heals once execution registers M+1, so it's a participation gap rather than a deadlock. The fix is to not drop the epochWake subscription across the registry wait (re-check epochWake.Load() != seen before parking, or wait on both). Fix this →

2. Block verification is still inside the avail lock, and failures are still silentavail/state.go:362-367 (carried over, unaddressed)

main ran this outside the lock; the PR moved it in:

for inner, ctrl := range s.inner.Lock() {
    applied := inner.epoch.Load()
    if !laneProposalAccepted(applied, p) && (… || !laneProposalAccepted(inner.anchorEpoch, p)) {
        return nil
    }

laneProposalAcceptedLaneProposal.VerifyBlock.VerifyPayload().Hash(), which protobuf-encodes and hashes the entire tx payload, plus VerifySig — and both run a second time on the anchorEpoch fallback. That is now serialized on the mutex that also gates PushVote, PushCommitQC, RecvBatch, headers, and the persist loop, for every inbound block from every peer. Snapshotting applied/anchorEpoch under the lock and verifying outside it (the shape PushCommitQC already uses with waitBackleash) keeps the semantics. Separately, the verification failure returns nil where main returned fmt.Errorf("block.Verify(): %w", err); the PushVote TODO that used to cover "return a meaningful validation error" was narrowed to joiner votes, so that concern is now unrecorded on both paths. Fix this →

3. blockVotes.header is an O(committee) scan that recomputes Hash()avail/block_votes.go:75-83 (carried over, unaddressed)

The reason for dropping byHash[want] is sound (it no longer holds zero-applied-weight votes, so it can miss headers), but the index can be kept: a byHeaderHash map[BlockHeaderHash]*BlockHeader populated unconditionally in pushVote is weight-independent, so recount never has to touch it. As written, headers() does O(range × committee) header hashes per road, under the inner lock. Header hashing is far cheaper than block hashing, so this is a real but modest cost.

4. ActivateEpoch — Cursor's new finding is correct but latent, and the argument itself is unguardedepoch/registry.go:124-144

ActivateEpoch overwrites s.m[next], which by then may be a placeholder already handed out by reference to data.qcEntry.epoch, avail.road.epoch, Anchor.Epoch, and avail's applied i.epoch. Those stashed pointers would then disagree with the registry entry for the same index, and since ApplyEpoch is gone there is no path that re-drives avail off the new committee. Cursor's report is accurate on mechanism. It is not reachable today — grep finds no production caller of ActivateEpoch — which is why I'm keeping it non-blocking, but it should be a tracked follow-up rather than only a TODO, because it must be closed before rotation is enabled.

While there: roads types.RoadRange is caller-supplied and never checked against {FirstRoad(next), FirstRoad(next+1)}. backleash uses IndexForRoad (pure arithmetic) while epochForRoad uses ep.RoadRange().Has(road), so a mismatched range silently splits those two views of the same road. Rejecting an index already handed out, and validating roads, are both one-line guards at the choke point every activation passes through.

5. Registry placeholders: the TODO under-describes the problemepoch/registry.go:148-160

makeEpoch copies ep0.Committee(), ep0.FirstTimestamp() and ep0.FirstBlock(). The // TODO: replace placeholders with execution-derived epochs names only the committee, but the timestamp and first-block are wrong too and feed ViewSpec.NextTimestamp() / NextGlobalBlock() for any view resolved against a placeholder. Either widen the TODO to name all three, or mark the epoch as a placeholder so a consumer needing a real timestamp fails loudly instead of silently reading genesis.

6. Nits

  • RoadRange.IsLastRoad and GlobalRange.IsLastBlock are still unused outside tests (IsLastBlock has no test at all), while AdvanceIfNeeded open-codes roadIndex != LastRoad(tipEpoch). Wire them in or drop them.
  • Registry.state on Watch means EpochByIndex/EpochAt/LatestEpoch/FirstBlock take an exclusive lock. Only WaitForEpoch needs the notification; an RWMutex plus a separate signal would keep the reads shared if these show up in profiles.
  • giga/avail.go:127-132: when streamLaneProposalsOnce returns nil because the server's epoch closed the lane but the local a.Lane(peer) still reports the same LaneID, closed is set to None and WaitForNextLane returns immediately — a redial spin until the local epoch catches up. Pre-existing (the old code passed None unconditionally), and this change strictly narrows the window, so purely informational.

Verification notes

I could not run go test or go build in this environment — both were denied by the tool policy, so the findings above are from reading the diff rather than from a green run. The PR's own test plan (go test ./sei-tendermint/autobahn/types/ ./sei-tendermint/internal/autobahn/... ./sei-tendermint/internal/p2p/ -count=1) still needs to be confirmed against 7f8e261. To let me run these on future invocations, add Bash(go test:*) / Bash(go build:*) to --allowedTools.
· branch wen/autobahn_multi_epoch

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.

1 participant