From 1332ad0ac46c4eef1a718a0cfb2dfaf3199ab423 Mon Sep 17 00:00:00 2001 From: Wen Date: Sat, 8 Aug 2026 21:52:55 -0700 Subject: [PATCH 01/14] feat(autobahn): identify lanes as (validator, e_join) (CON-358) LaneID pairs validator with join epoch so leave/rejoin does not collide with a recycled validator slot. TipEpoch leave prunes maps on the same tick as DeleteLane; Subscribe fails with ErrLanePruned after drop. Co-authored-by: Cursor --- sei-db/ledger_db/block/block_db_test.go | 14 +- sei-db/ledger_db/block/blocksim/blocksim.go | 15 +- sei-tendermint/autobahn/types/block.go | 8 +- sei-tendermint/autobahn/types/committee.go | 98 +++- .../autobahn/types/committee_activate_test.go | 71 +++ .../autobahn/types/committee_test.go | 82 +-- sei-tendermint/autobahn/types/lane_id.go | 83 +++ sei-tendermint/autobahn/types/lane_id_test.go | 24 + sei-tendermint/autobahn/types/proposal.go | 4 +- .../autobahn/types/proposal_test.go | 26 +- sei-tendermint/autobahn/types/testonly.go | 18 +- sei-tendermint/autobahn/types/types_test.go | 1 + .../autobahn/types/wireguard_test.go | 4 +- .../internal/autobahn/autobahn.proto | 21 +- .../internal/autobahn/avail/conv_test.go | 2 +- .../internal/autobahn/avail/inner.go | 96 +++- .../internal/autobahn/avail/inner_test.go | 178 +++++-- .../internal/autobahn/avail/state.go | 216 ++++++-- .../internal/autobahn/avail/state_test.go | 57 +- .../internal/autobahn/avail/subscriptions.go | 17 +- .../autobahn/avail/subscriptions_test.go | 49 ++ .../internal/autobahn/avail/testonly.go | 5 +- .../autobahn/consensus/persist/blocks.go | 85 ++- .../autobahn/consensus/persist/blocks_test.go | 136 ++++- .../internal/autobahn/epoch/registry.go | 33 +- .../internal/autobahn/pb/autobahn.hashable.go | 1 + .../internal/autobahn/pb/autobahn.pb.go | 497 ++++++++++-------- .../autobahn/pb/autobahn.wireguard.go | 46 +- .../internal/autobahn/producer/mempool.go | 39 +- .../autobahn/producer/mempool_test.go | 96 +++- .../internal/autobahn/producer/state.go | 96 +++- sei-tendermint/internal/p2p/giga/api.go | 4 +- sei-tendermint/internal/p2p/giga/avail.go | 48 +- .../internal/p2p/giga/avail_test.go | 10 +- .../internal/p2p/giga/consensus_test.go | 3 +- .../internal/p2p/giga/pb/api.wireguard.go | 8 +- .../protoutils/alloc_scan_load_test.go | 5 +- 37 files changed, 1616 insertions(+), 580 deletions(-) create mode 100644 sei-tendermint/autobahn/types/committee_activate_test.go create mode 100644 sei-tendermint/autobahn/types/lane_id.go create mode 100644 sei-tendermint/autobahn/types/lane_id_test.go create mode 100644 sei-tendermint/internal/autobahn/avail/subscriptions_test.go diff --git a/sei-db/ledger_db/block/block_db_test.go b/sei-db/ledger_db/block/block_db_test.go index 7b8bde798b..ccd4415f98 100644 --- a/sei-db/ledger_db/block/block_db_test.go +++ b/sei-db/ledger_db/block/block_db_test.go @@ -1511,18 +1511,10 @@ func writeAll(t *testing.T, db types.BlockDB, batches []batch) { } } -// buildCommittee returns a deterministic round-robin committee (global numbering -// from 0) and the secret keys that sign its QCs. +// buildCommittee returns a deterministic committee (via GenCommittee with a +// fixed seed) and the secret keys that sign its QCs. func buildCommittee() (*types.Committee, []types.SecretKey) { - rng := utils.TestRngFromSeed(testSeed) - keys := make([]types.SecretKey, committeeSize) - replicas := make([]types.PublicKey, committeeSize) - for i := range keys { - keys[i] = types.GenSecretKey(rng) - replicas[i] = keys[i].Public() - } - committee := utils.OrPanic1(types.NewRoundRobinElection(replicas)) - return committee, keys + return types.GenCommittee(utils.TestRngFromSeed(testSeed), committeeSize) } // generateBatches builds a deterministic sequence of contiguous finalized diff --git a/sei-db/ledger_db/block/blocksim/blocksim.go b/sei-db/ledger_db/block/blocksim/blocksim.go index e70e8d4a87..c22d83a52a 100644 --- a/sei-db/ledger_db/block/blocksim/blocksim.go +++ b/sei-db/ledger_db/block/blocksim/blocksim.go @@ -238,19 +238,10 @@ func recoverResumeState( return prev, highest, nil } -// buildCommittee creates a round-robin committee of the given size along with -// the secret keys that sign its QCs, with global numbering starting at 0. +// buildCommittee creates a committee of the given size along with the secret +// keys that sign its QCs (via types.GenCommittee). func buildCommittee(rng tmutils.Rng, size int) (*types.Committee, []types.SecretKey, error) { - keys := make([]types.SecretKey, size) - replicas := make([]types.PublicKey, size) - for i := range keys { - keys[i] = types.GenSecretKey(rng) - replicas[i] = keys[i].Public() - } - committee, err := types.NewRoundRobinElection(replicas) - if err != nil { - return nil, nil, fmt.Errorf("failed to build committee: %w", err) - } + committee, keys := types.GenCommittee(rng, size) return committee, keys, nil } diff --git a/sei-tendermint/autobahn/types/block.go b/sei-tendermint/autobahn/types/block.go index d3e283bd23..7b99f5f35b 100644 --- a/sei-tendermint/autobahn/types/block.go +++ b/sei-tendermint/autobahn/types/block.go @@ -15,10 +15,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" ) -// LaneID represents a lane identifier (currently it is the same as NodeID, -// since the producer uniquely identifies the lane). -type LaneID = PublicKey - // NodeID represents a unique identifier for a node in the network. type NodeID string @@ -223,7 +219,7 @@ func (p *Payload) Hash() PayloadHash { var BlockHeaderConv = protoutils.Conv[*BlockHeader, *pb.BlockHeader]{ Encode: func(h *BlockHeader) *pb.BlockHeader { return &pb.BlockHeader{ - Lane: PublicKeyConv.Encode(h.lane), + LaneId: LaneIDConv.Encode(h.lane), BlockNumber: utils.Alloc(uint64(h.blockNumber)), ParentHash: h.parentHash[:], PayloadHash: h.payloadHash[:], @@ -238,7 +234,7 @@ var BlockHeaderConv = protoutils.Conv[*BlockHeader, *pb.BlockHeader]{ if err != nil { return nil, fmt.Errorf("ParentHash: %w", err) } - lane, err := PublicKeyConv.DecodeReq(h.Lane) + lane, err := LaneIDConv.DecodeReq(h.LaneId) if err != nil { return nil, fmt.Errorf("lane: %w", err) } diff --git a/sei-tendermint/autobahn/types/committee.go b/sei-tendermint/autobahn/types/committee.go index 34da1345b3..07b6bbfb19 100644 --- a/sei-tendermint/autobahn/types/committee.go +++ b/sei-tendermint/autobahn/types/committee.go @@ -22,8 +22,10 @@ func (s ImSlice[T]) At(i int) T { return s.s[i] } func (s ImSlice[T]) All() iter.Seq[T] { return slices.Values(s.s) } // Committee represents the consensus committee. +// Lanes carry membership (validator + e_join); weights are voting stake. type Committee struct { - replicas ImSlice[PublicKey] + lanes ImSlice[LaneID] // sorted; one lane per member + byValidator map[PublicKey]LaneID weights map[PublicKey]uint64 totalWeight uint64 } @@ -36,15 +38,20 @@ func (c *Committee) HasReplica(k PublicKey) bool { } func (c *Committee) HasLane(l LaneID) bool { - _, ok := c.weights[l] - return ok + got, ok := c.byValidator[l.validator] + return ok && got.eJoin == l.eJoin } -// Lanes is the list of nodes which are eligible to produce blocks. -func (c *Committee) Lanes() ImSlice[LaneID] { return c.replicas } +func (c *Committee) Lane(v PublicKey) utils.Option[LaneID] { + lane, ok := c.byValidator[v] + if !ok { + return utils.None[LaneID]() + } + return utils.Some(lane) +} -// Replicas is the list of nodes which are eligible to participate in the consensus. -func (c *Committee) Replicas() ImSlice[PublicKey] { return c.replicas } +// Lanes is the list of nodes which are eligible to produce blocks. +func (c *Committee) Lanes() ImSlice[LaneID] { return c.lanes } // Deterministic random oracle selecting a replica with probability proportional to the weight. func (c *Committee) randomReplica(seed []byte) PublicKey { @@ -54,10 +61,10 @@ func (c *Committee) randomReplica(seed []byte) PublicKey { total.SetUint64(c.totalWeight) y := x.Mod(&x, &total).Uint64() // TODO(gprusak): this can be optimized to O(1) lookup - for k := range c.replicas.All() { - w := c.weights[k] + for lane := range c.lanes.All() { + w := c.weights[lane.validator] if y < w { - return k + return lane.validator } y -= w } @@ -116,37 +123,80 @@ func (c *Committee) LaneQuorum() uint64 { return c.Faulty() + 1 } +// NewCommittee is genesis: e_join = 0 for every member. func NewCommittee(weights map[PublicKey]uint64) (*Committee, error) { + weights, totalWeight, err := normalizeWeights(weights) + if err != nil { + return nil, err + } + lanes := make([]LaneID, 0, len(weights)) + for v := range weights { + lanes = append(lanes, NewLaneID(v, 0)) + } + return finalizeCommittee(lanes, weights, totalWeight) +} + +// ActivateCommittee for epoch e>0: copy e_join from prev on stay, stamp e on join. +func ActivateCommittee(prev *Committee, weights map[PublicKey]uint64, e EpochIndex) (*Committee, error) { + if e == 0 { + return nil, errors.New("ActivateCommittee: epoch must be > 0") + } + weights, totalWeight, err := normalizeWeights(weights) + if err != nil { + return nil, err + } + lanes := make([]LaneID, 0, len(weights)) + for v := range weights { + eJoin := e + if prevLane, ok := prev.Lane(v).Get(); ok { + eJoin = prevLane.eJoin + } + lanes = append(lanes, NewLaneID(v, eJoin)) + } + return finalizeCommittee(lanes, weights, totalWeight) +} + +func normalizeWeights(weights map[PublicKey]uint64) (map[PublicKey]uint64, uint64, error) { weights = maps.Clone(weights) totalWeight := uint64(0) for k, w := range weights { if w == 0 { delete(weights, k) + continue } if utils.Max[uint64]()-totalWeight < w { - return nil, fmt.Errorf("total weight overflow") + return nil, 0, fmt.Errorf("total weight overflow") } totalWeight += w } if totalWeight == 0 { - return nil, errors.New("total weight is 0") + return nil, 0, errors.New("total weight is 0") } if len(weights) > MaxValidators { - return nil, fmt.Errorf("too many validators: got %d, want <= %d", len(weights), MaxValidators) + return nil, 0, fmt.Errorf("too many validators: got %d, want <= %d", len(weights), MaxValidators) + } + return weights, totalWeight, nil +} + +// finalizeCommittee sorts lanes and rejects duplicate validators (multiple e_join). +func finalizeCommittee(lanes []LaneID, weights map[PublicKey]uint64, totalWeight uint64) (*Committee, error) { + slices.SortFunc(lanes, LaneID.Compare) + for i := 1; i < len(lanes); i++ { + if lanes[i].validator == lanes[i-1].validator { + return nil, fmt.Errorf( + "duplicate validator in committee lanes: %q with e_join %d and %d", + lanes[i].validator, lanes[i-1].eJoin, lanes[i].eJoin, + ) + } + } + byValidator := make(map[PublicKey]LaneID, len(lanes)) + for _, lane := range lanes { + byValidator[lane.validator] = lane } - replicas := slices.SortedFunc(maps.Keys(weights), func(a, b PublicKey) int { return a.Compare(b) }) return &Committee{ - replicas: ImSlice[PublicKey]{replicas}, + lanes: ImSlice[LaneID]{lanes}, + byValidator: byValidator, weights: weights, totalWeight: totalWeight, }, nil } - -// NewRoundRobinElection creates a Committee with equal weights for each replica. -func NewRoundRobinElection(replicas []PublicKey) (*Committee, error) { - weights := map[PublicKey]uint64{} - for _, k := range replicas { - weights[k] = 1 - } - return NewCommittee(weights) -} diff --git a/sei-tendermint/autobahn/types/committee_activate_test.go b/sei-tendermint/autobahn/types/committee_activate_test.go new file mode 100644 index 0000000000..e94e4bf554 --- /dev/null +++ b/sei-tendermint/autobahn/types/committee_activate_test.go @@ -0,0 +1,71 @@ +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 TestActivateCommittee_StayLeaveRejoin(t *testing.T) { + rng := utils.TestRng() + a := GenSecretKey(rng).Public() + b := GenSecretKey(rng).Public() + c := GenSecretKey(rng).Public() + d := GenSecretKey(rng).Public() + + requireLanesSorted := func(t *testing.T, committee *Committee) { + t.Helper() + lanes := committee.Lanes() + for i := 1; i < lanes.Len(); i++ { + require.Less(t, lanes.At(i-1).Compare(lanes.At(i)), 0) + } + } + + // Epoch 0: A,B,D join. + c0, err := NewCommittee(map[PublicKey]uint64{a: 1, b: 1, d: 1}) + require.NoError(t, err) + require.Equal(t, NewLaneID(a, 0), c0.Lane(a).OrPanic("a")) + require.Equal(t, NewLaneID(b, 0), c0.Lane(b).OrPanic("b")) + require.Equal(t, NewLaneID(d, 0), c0.Lane(d).OrPanic("d")) + require.False(t, c0.HasLane(NewLaneID(c, 0))) + requireLanesSorted(t, c0) + + // Epoch 1: A,B,D stay → copy e_join=0. + c1, err := ActivateCommittee(c0, map[PublicKey]uint64{a: 1, b: 1, d: 1}, 1) + require.NoError(t, err) + require.Equal(t, NewLaneID(a, 0), c1.Lane(a).OrPanic("a")) + require.Equal(t, NewLaneID(b, 0), c1.Lane(b).OrPanic("b")) + require.Equal(t, NewLaneID(d, 0), c1.Lane(d).OrPanic("d")) + requireLanesSorted(t, c1) + + // Epoch 2: B,D leave; C joins. A stays. + c2, err := ActivateCommittee(c1, map[PublicKey]uint64{a: 1, c: 1}, 2) + require.NoError(t, err) + require.Equal(t, NewLaneID(a, 0), c2.Lane(a).OrPanic("a")) + require.Equal(t, NewLaneID(c, 2), c2.Lane(c).OrPanic("c")) + require.False(t, c2.HasLane(NewLaneID(b, 0))) + require.False(t, c2.HasLane(NewLaneID(d, 0))) + require.False(t, c2.HasReplica(b)) + requireLanesSorted(t, c2) + + // Epoch 3: D rejoins; C and A stay. + c3, err := ActivateCommittee(c2, map[PublicKey]uint64{a: 1, c: 1, d: 1}, 3) + require.NoError(t, err) + require.Equal(t, NewLaneID(a, 0), c3.Lane(a).OrPanic("a")) + require.Equal(t, NewLaneID(c, 2), c3.Lane(c).OrPanic("c")) + require.Equal(t, NewLaneID(d, 3), c3.Lane(d).OrPanic("d")) + require.False(t, c3.HasLane(NewLaneID(d, 0))) + requireLanesSorted(t, c3) +} + +func TestFinalizeCommittee_RejectsDuplicatePubKeyDifferentEJoin(t *testing.T) { + rng := utils.TestRng() + v := GenSecretKey(rng).Public() + _, err := finalizeCommittee( + []LaneID{NewLaneID(v, 0), NewLaneID(v, 1)}, + map[PublicKey]uint64{v: 1}, + 1, + ) + require.Error(t, err) +} diff --git a/sei-tendermint/autobahn/types/committee_test.go b/sei-tendermint/autobahn/types/committee_test.go index 996e2a812a..c1a6a28150 100644 --- a/sei-tendermint/autobahn/types/committee_test.go +++ b/sei-tendermint/autobahn/types/committee_test.go @@ -25,11 +25,11 @@ func TestNewCommittee_FiltersOutZeroWeightValidators(t *testing.T) { if committee.HasReplica(zeroWeightKey) { t.Fatal("HasReplica() = true for zero-weight validator, want false") } - if got := committee.Replicas().Len(); got != 1 { - t.Fatalf("Replicas().Len() = %v, want 1", got) + if !committee.HasLane(NewLaneID(nonZeroWeightKey, 0)) { + t.Fatal("HasLane(nonZero@e0) = false, want true") } - if got := committee.Replicas().At(0); got != nonZeroWeightKey { - t.Fatalf("Replicas().At(0) = %v, want %v", got, nonZeroWeightKey) + if got := committee.Lanes().Len(); got != 1 { + t.Fatalf("Lanes().Len() = %v, want 1", got) } if got := committee.Weight(nonZeroWeightKey); got != 7 { t.Fatalf("Weight() = %v, want 7", got) @@ -91,7 +91,7 @@ func makeEpoch(rng utils.Rng) (*Epoch, []SecretKey) { func TestLaneQCVerifyChecksWeight(t *testing.T) { rng := utils.TestRng() ep, keys := makeEpoch(rng) - vote := NewLaneVote(NewBlock(keys[0].Public(), 0, GenBlockHeaderHash(rng), GenPayload(rng)).Header()) + vote := NewLaneVote(NewBlock(NewLaneID(keys[0].Public(), 0), 0, GenBlockHeaderHash(rng), GenPayload(rng)).Header()) heavyOnly := NewLaneQC([]*Signed[*LaneVote]{ Sign(keys[0], vote), @@ -104,38 +104,22 @@ func TestLaneQCVerifyChecksWeight(t *testing.T) { require.Error(t, lightMajority.Verify(ep.Committee())) } -func TestPrepareQCVerifyChecksWeight(t *testing.T) { +func TestCommitQCVerifyChecksWeight(t *testing.T) { rng := utils.TestRng() ep, keys := makeEpoch(rng) - vote := NewPrepareVote(ProposalAt(ep, View{EpochIndex: ep.EpochIndex(), Index: ep.RoadRange().First})) + vote := NewCommitVote(ProposalAt(ep, View{EpochIndex: ep.EpochIndex(), Index: ep.RoadRange().First})) - heavyOnly := NewPrepareQC([]*Signed[*PrepareVote]{ + heavyOnly := NewCommitQC([]*Signed[*CommitVote]{ Sign(keys[0], vote), }) require.NoError(t, heavyOnly.Verify(ep)) - lightMajority := NewPrepareQC([]*Signed[*PrepareVote]{ + lightMajority := NewCommitQC([]*Signed[*CommitVote]{ Sign(keys[1], vote), Sign(keys[2], vote), }) require.Error(t, lightMajority.Verify(ep)) } -func TestPrepareQCVerifyChecksEpochBinding(t *testing.T) { - rng := utils.TestRng() - ep, keys := makeEpoch(rng) - sign := func(p *Proposal) *PrepareQC { - return NewPrepareQC([]*Signed[*PrepareVote]{Sign(keys[0], NewPrepareVote(p))}) - } - - 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()) - 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()) - require.Error(t, sign(outOfRoads).Verify(ep)) -} - func TestCommitQCVerifyChecksEpochBinding(t *testing.T) { rng := utils.TestRng() ep, keys := makeEpoch(rng) @@ -152,39 +136,6 @@ func TestCommitQCVerifyChecksEpochBinding(t *testing.T) { require.Error(t, sign(outOfRoads).Verify(ep)) } -func TestCommitQCVerifyChecksWeight(t *testing.T) { - rng := utils.TestRng() - ep, keys := makeEpoch(rng) - vote := NewCommitVote(ProposalAt(ep, View{EpochIndex: ep.EpochIndex(), Index: ep.RoadRange().First})) - - heavyOnly := NewCommitQC([]*Signed[*CommitVote]{ - Sign(keys[0], vote), - }) - require.NoError(t, heavyOnly.Verify(ep)) - lightMajority := NewCommitQC([]*Signed[*CommitVote]{ - Sign(keys[1], vote), - Sign(keys[2], vote), - }) - require.Error(t, lightMajority.Verify(ep)) -} - -func TestAppQCVerifyChecksWeight(t *testing.T) { - rng := utils.TestRng() - ep, keys := makeEpoch(rng) - vote := NewAppVote(NewAppProposal(0, 0, GenAppHash(rng), ep.EpochIndex())) - - heavyOnly := NewAppQC([]*Signed[*AppVote]{ - Sign(keys[0], vote), - }) - require.NoError(t, heavyOnly.Verify(ep.Committee())) - - lightMajority := NewAppQC([]*Signed[*AppVote]{ - Sign(keys[1], vote), - Sign(keys[2], vote), - }) - require.Error(t, lightMajority.Verify(ep.Committee())) -} - func TestTimeoutQCVerifyChecksEpochBinding(t *testing.T) { rng := utils.TestRng() ep, keys := makeEpoch(rng) @@ -211,22 +162,11 @@ func TestTimeoutQCVerifyChecksWeight(t *testing.T) { heavyOnly := NewTimeoutQC([]*FullTimeoutVote{ NewFullTimeoutVote(keys[0], view, utils.None[*PrepareQC]()), }) - if err := heavyOnly.Verify(ep, prev); err != nil { - t.Fatalf("heavyOnly.Verify(): %v", err) - } + require.NoError(t, heavyOnly.Verify(ep, prev)) lightMajority := NewTimeoutQC([]*FullTimeoutVote{ NewFullTimeoutVote(keys[1], view, utils.None[*PrepareQC]()), NewFullTimeoutVote(keys[2], view, utils.None[*PrepareQC]()), }) - if err := lightMajority.Verify(ep, prev); err == nil { - t.Fatal("lightMajority.Verify() succeeded, want error") - } -} - -func TestNewCommittee_RejectsEmptyWeights(t *testing.T) { - _, err := NewCommittee(map[PublicKey]uint64{}) - if err == nil { - t.Fatal("NewCommittee() succeeded with empty weights, want error") - } + require.Error(t, lightMajority.Verify(ep, prev)) } diff --git a/sei-tendermint/autobahn/types/lane_id.go b/sei-tendermint/autobahn/types/lane_id.go new file mode 100644 index 0000000000..fca4f5720d --- /dev/null +++ b/sei-tendermint/autobahn/types/lane_id.go @@ -0,0 +1,83 @@ +package types + +import ( + "cmp" + "crypto/ed25519" + "encoding/binary" + "encoding/hex" + "fmt" + + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/pb" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/protoutils" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" +) + +// LaneID is a validator's continuous membership streak; e_join is the join epoch. +type LaneID struct { + utils.ReadOnly + validator PublicKey + eJoin EpochIndex +} + +func NewLaneID(validator PublicKey, eJoin EpochIndex) LaneID { + return LaneID{validator: validator, eJoin: eJoin} +} + +func (l LaneID) Validator() PublicKey { return l.validator } + +func (l LaneID) EJoin() EpochIndex { return l.eJoin } + +// Compare orders by validator, then e_join. +func (l LaneID) Compare(other LaneID) int { + if c := l.validator.Compare(other.validator); c != 0 { + return c + } + return cmp.Compare(l.eJoin, other.eJoin) +} + +// Bytes returns a stable encoding: pubkey bytes || big-endian e_join. +func (l LaneID) Bytes() []byte { + vb := l.validator.Bytes() + b := make([]byte, 0, len(vb)+8) + b = append(b, vb...) + return binary.BigEndian.AppendUint64(b, uint64(l.eJoin)) +} + +// LaneIDFromBytes parses Bytes() encoding (exactly ed25519 pubkey || u64be e_join). +func LaneIDFromBytes(b []byte) (LaneID, error) { + want := ed25519.PublicKeySize + 8 + if len(b) != want { + return LaneID{}, fmt.Errorf("LaneID: got %d bytes, want %d", len(b), want) + } + eJoin := EpochIndex(binary.BigEndian.Uint64(b[ed25519.PublicKeySize:])) + validator, err := PublicKeyFromBytes(b[:ed25519.PublicKeySize]) + if err != nil { + return LaneID{}, fmt.Errorf("LaneID validator: %w", err) + } + return NewLaneID(validator, eJoin), nil +} + +func (l LaneID) String() string { + return fmt.Sprintf("%s@e%d", l.validator.String(), l.eJoin) +} + +func (l LaneID) HexString() string { return hex.EncodeToString(l.Bytes()) } + +var LaneIDConv = protoutils.Conv[LaneID, *pb.LaneID]{ + Encode: func(l LaneID) *pb.LaneID { + return &pb.LaneID{ + Validator: PublicKeyConv.Encode(l.validator), + EJoin: utils.Alloc(uint64(l.eJoin)), + } + }, + Decode: func(p *pb.LaneID) (LaneID, error) { + validator, err := PublicKeyConv.DecodeReq(p.Validator) + if err != nil { + return LaneID{}, fmt.Errorf("validator: %w", err) + } + if p.EJoin == nil { + return LaneID{}, fmt.Errorf("e_join: missing") + } + return NewLaneID(validator, EpochIndex(*p.EJoin)), nil + }, +} diff --git a/sei-tendermint/autobahn/types/lane_id_test.go b/sei-tendermint/autobahn/types/lane_id_test.go new file mode 100644 index 0000000000..3f745d44cd --- /dev/null +++ b/sei-tendermint/autobahn/types/lane_id_test.go @@ -0,0 +1,24 @@ +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 TestLaneID_Bytes(t *testing.T) { + rng := utils.TestRng() + want := GenLaneID(rng) + got, err := LaneIDFromBytes(want.Bytes()) + require.NoError(t, err) + require.Equal(t, want, got) + + raw := want.Bytes() + _, err = LaneIDFromBytes(raw[:len(raw)-1]) + require.Error(t, err) + _, err = LaneIDFromBytes(append(append([]byte{}, raw...), 0)) + require.Error(t, err) + _, err = LaneIDFromBytes(nil) + require.Error(t, err) +} diff --git a/sei-tendermint/autobahn/types/proposal.go b/sei-tendermint/autobahn/types/proposal.go index bb78f668c1..79c3b96b72 100644 --- a/sei-tendermint/autobahn/types/proposal.go +++ b/sei-tendermint/autobahn/types/proposal.go @@ -527,14 +527,14 @@ func (m *FullProposal) Verify(vs ViewSpec) error { var LaneRangeConv = protoutils.Conv[*LaneRange, *pb.LaneRange]{ Encode: func(m *LaneRange) *pb.LaneRange { return &pb.LaneRange{ - Lane: PublicKeyConv.Encode(m.lane), + LaneId: LaneIDConv.Encode(m.lane), First: utils.Alloc(uint64(m.first)), Next: utils.Alloc(uint64(m.next)), LastHash: m.lastHash[:], } }, Decode: func(m *pb.LaneRange) (*LaneRange, error) { - lane, err := PublicKeyConv.DecodeReq(m.Lane) + lane, err := LaneIDConv.DecodeReq(m.LaneId) if err != nil { return nil, fmt.Errorf("Lane: %w", err) } diff --git a/sei-tendermint/autobahn/types/proposal_test.go b/sei-tendermint/autobahn/types/proposal_test.go index 0228ccc1c3..3145d59c22 100644 --- a/sei-tendermint/autobahn/types/proposal_test.go +++ b/sei-tendermint/autobahn/types/proposal_test.go @@ -101,7 +101,7 @@ func TestProposalVerifyFreshWithBlocks(t *testing.T) { proposerKey := leaderKey(committee, keys, vs.View()) // Produce a LaneQC for the proposer's lane. - lane := proposerKey.Public() + lane := committee.Lane(proposerKey.Public()).OrPanic("missing lane") laneQC := makeLaneQC(rng, committee, keys, lane, 0, GenBlockHeaderHash(rng)) fp := utils.OrPanic1(NewProposal(proposerKey, vs, time.Now(), @@ -115,7 +115,7 @@ func TestNewProposalRejectsLaneRangeLongerThanMaxLaneRangeInProposal(t *testing. ep := genFreshEpoch(rng, committee) vs := ViewSpec{Epoch: ep} proposerKey := leaderKey(committee, keys, vs.View()) - lane := proposerKey.Public() + lane := committee.Lane(proposerKey.Public()).OrPanic("missing lane") laneQC := makeLaneQC(rng, committee, keys, lane, MaxLaneRangeInProposal, GenBlockHeaderHash(rng)) _, err := NewProposal( @@ -135,7 +135,7 @@ func TestProposalBlockTimestampStrictlyMonotone(t *testing.T) { firstBlock := ep.FirstBlock() vs0 := ViewSpec{Epoch: ep} proposer0 := leaderKey(committee, keys, vs0.View()) - lane := proposer0.Public() + lane := committee.Lane(proposer0.Public()).OrPanic("missing lane") firstProposal := utils.OrPanic1(NewProposal( proposer0, @@ -197,7 +197,7 @@ func TestProposalVerifyRejectsNonMonotoneTimestamp(t *testing.T) { ep := genFreshEpoch(rng, committee) vs0 := ViewSpec{Epoch: ep} proposer0 := leaderKey(committee, keys, vs0.View()) - lane := proposer0.Public() + lane := committee.Lane(proposer0.Public()).OrPanic("missing lane") lQC := makeLaneQC(rng, committee, keys, lane, 0, GenBlockHeaderHash(rng)) fp0a := utils.OrPanic1(NewProposal( @@ -327,7 +327,7 @@ func TestProposalVerifyRejectsNonCommitteeLane(t *testing.T) { // Keep the non-empty committee tipcut and add a non-committee lane. // LaneRange.Verify rejects X because it's not a committee lane. - extraLane := GenSecretKey(rng).Public() + extraLane := NewLaneID(GenSecretKey(rng).Public(), GenEpochIndex(rng)) require.False(t, committee.HasLane(extraLane)) origProposal := fp.Proposal().Msg() @@ -457,7 +457,7 @@ func TestProposalVerifyRejectsMissingLaneQC(t *testing.T) { vs := ViewSpec{Epoch: ep} proposerKey := leaderKey(committee, keys, vs.View()) - lane := keys[0].Public() + lane := committee.Lane(keys[0].Public()).OrPanic("missing lane") laneQC := makeLaneQC(rng, committee, keys, lane, 0, GenBlockHeaderHash(rng)) // Build a valid proposal with a block, then strip the laneQC. @@ -479,7 +479,7 @@ func TestProposalVerifyRejectsLaneQCBlockNumberMismatch(t *testing.T) { vs := ViewSpec{Epoch: ep} proposerKey := leaderKey(committee, keys, vs.View()) - lane := keys[0].Public() + lane := committee.Lane(keys[0].Public()).OrPanic("missing lane") // Build a valid proposal with a QC certifying block 1 (range [0, 2)). goodQC := makeLaneQC(rng, committee, keys, lane, 1, GenBlockHeaderHash(rng)) @@ -503,7 +503,7 @@ func TestProposalVerifyRejectsInvalidLaneQCSignature(t *testing.T) { vs := ViewSpec{Epoch: ep} proposerKey := leaderKey(committee, keys, vs.View()) - lane := keys[0].Public() + lane := committee.Lane(keys[0].Public()).OrPanic("missing lane") block := NewBlock(lane, 0, GenBlockHeaderHash(rng), GenPayload(rng)) header := block.Header() @@ -541,7 +541,7 @@ func TestProposalVerifyRejectsLaneRangeLongerThanMaxLaneRangeInProposal(t *testi rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) ep := genFreshEpoch(rng, committee) - lane := leaderKey(committee, keys, View{}).Public() + lane := committee.Lane(leaderKey(committee, keys, View{}).Public()).OrPanic("missing lane") // Bypass NewProposal's check by constructing the proposal directly. oversized := newProposal( View{}, @@ -587,7 +587,7 @@ func TestProposalVerifyRejectsAppProposalLowerThanPrevious(t *testing.T) { // Construct commitQC for index 1 with AppProposal // and Proposal for index 2 without any app proposal. // Such a proposal should fail validation, because app proposals need to be monotone. - l := keys[0].Public() + l := committee.Lane(keys[0].Public()).OrPanic("missing lane") lQCs := map[LaneID]*LaneQC{l: makeLaneQC(rng, committee, keys, l, 0, GenBlockHeaderHash(rng))} commitQC0 := makeCommitQC(keys, makeFullProposal(ep, keys, utils.None[*CommitQC](), lQCs, utils.None[*AppQC]())) appQC0 := makeAppQCFor(keys, commitQC0.GlobalRange().First, 0, GenAppHash(rng), ep.EpochIndex()) @@ -724,7 +724,7 @@ func TestProposalVerifyRejectsLaneQCHeaderHashMismatch(t *testing.T) { vs := ViewSpec{Epoch: ep} proposerKey := leaderKey(committee, keys, vs.View()) - lane := proposerKey.Public() + lane := committee.Lane(proposerKey.Public()).OrPanic("missing lane") // Build a valid proposal with a QC for block 0. realQC := makeLaneQC(rng, committee, keys, lane, 0, GenBlockHeaderHash(rng)) @@ -752,7 +752,7 @@ func TestProposalVerifyValidReproposal(t *testing.T) { ep := genFreshEpoch(rng, committee) vs0 := ViewSpec{Epoch: ep} leader0 := leaderKey(committee, keys, vs0.View()) - lane := committee.Leader(vs0.View()) + lane := committee.Lane(committee.Leader(vs0.View())).OrPanic("missing lane") laneQC0 := makeLaneQC(rng, committee, keys, lane, 0, GenBlockHeaderHash(rng)) fp0 := utils.OrPanic1(NewProposal(leader0, vs0, time.Now(), map[LaneID]*LaneQC{lane: laneQC0}, utils.None[*AppQC]())) @@ -810,7 +810,7 @@ func TestProposalVerifyRejectsReproposalWithUnnecessaryData(t *testing.T) { // Create a valid reproposal, then tamper it with unnecessary laneQCs. reproposal := utils.OrPanic1(NewProposal(leader1, vs1, time.Now(), oneLaneQCMap(rng, committee, keys, vs1), utils.None[*AppQC]())) - lane := keys[0].Public() + lane := committee.Lane(keys[0].Public()).OrPanic("missing lane") laneQC := makeLaneQC(rng, committee, keys, lane, 0, GenBlockHeaderHash(rng)) tamperedFP := &FullProposal{ proposal: reproposal.proposal, diff --git a/sei-tendermint/autobahn/types/testonly.go b/sei-tendermint/autobahn/types/testonly.go index ffc3d5a73d..0129b1f5d4 100644 --- a/sei-tendermint/autobahn/types/testonly.go +++ b/sei-tendermint/autobahn/types/testonly.go @@ -3,6 +3,7 @@ package types import ( "cmp" "fmt" + "maps" "slices" "time" @@ -73,6 +74,7 @@ func GenSecretKey(rng utils.Rng) SecretKey { } // GenCommittee generates a random Committee of the given size. +// Each member gets an independent random e_join (via GenEpochIndex). // Returns the generated secret keys as well. func GenCommittee(rng utils.Rng, size int) (*Committee, []SecretKey) { sks := utils.GenSliceN(rng, size, GenSecretKey) @@ -83,7 +85,17 @@ func GenCommittee(rng utils.Rng, size int) (*Committee, []SecretKey) { slices.SortStableFunc(sks, func(a, b SecretKey) int { return -cmp.Compare(pks[a.Public()], pks[b.Public()]) }) - return utils.OrPanic1(NewCommittee(pks)), sks + weights, total, err := normalizeWeights(pks) + if err != nil { + panic(err) + } + lanes := make([]LaneID, 0, len(weights)) + vs := slices.Collect(maps.Keys(weights)) + slices.SortFunc(vs, PublicKey.Compare) + for _, v := range vs { + lanes = append(lanes, NewLaneID(v, GenEpochIndex(rng))) + } + return utils.OrPanic1(finalizeCommittee(lanes, weights, total)), sks } // TestKeysWithWeight returns a deterministic subset of keys whose committee weight reaches the requested threshold. @@ -107,9 +119,9 @@ func TestSecretKey(nodeID NodeID) SecretKey { return SecretKey{key: ed25519.TestSecretKey([]byte(nodeID))} } -// GenLaneID generates a random LaneID. +// GenLaneID generates a random LaneID (random validator, random e_join). func GenLaneID(rng utils.Rng) LaneID { - return TestSecretKey(GenNodeID(rng)).Public() + return NewLaneID(TestSecretKey(GenNodeID(rng)).Public(), GenEpochIndex(rng)) } // GenSignature generates a random Signature. diff --git a/sei-tendermint/autobahn/types/types_test.go b/sei-tendermint/autobahn/types/types_test.go index b82de8462c..017f13478e 100644 --- a/sei-tendermint/autobahn/types/types_test.go +++ b/sei-tendermint/autobahn/types/types_test.go @@ -41,6 +41,7 @@ func TestConv(t *testing.T) { TimeConv.Test(utils.GenTimestamp(rng)), DurationConv.Test(time.Duration(int64(rng.Uint64()))), PublicKeyConv.Test(GenPublicKey(rng)), + LaneIDConv.Test(GenLaneID(rng)), SignatureConv.Test(GenSignature(rng)), BlockHeaderConv.Test(GenBlockHeader(rng)), PayloadConv.Test(GenPayload(rng)), diff --git a/sei-tendermint/autobahn/types/wireguard_test.go b/sei-tendermint/autobahn/types/wireguard_test.go index cbebd32535..e389917c0b 100644 --- a/sei-tendermint/autobahn/types/wireguard_test.go +++ b/sei-tendermint/autobahn/types/wireguard_test.go @@ -82,7 +82,7 @@ func TestPayloadWireguardRejectsTooManyTxs(t *testing.T) { func TestLaneQCWireguardAcceptsMaxValidators(t *testing.T) { committee, keys := maxValidatorCommittee(t) rng := utils.TestRng() - lane := committee.Leader(View{}) + lane := committee.Lane(committee.Leader(View{})).OrPanic("missing lane") vote := NewLaneVote(NewBlock(lane, 0, GenBlockHeaderHash(rng), GenPayload(rng)).Header()) votes := make([]*Signed[*LaneVote], len(keys)) for i, key := range keys { @@ -191,7 +191,7 @@ func TestFullProposalWireguardAcceptsMaxValidators(t *testing.T) { rng := utils.TestRng() laneQCs := map[LaneID]*LaneQC{} for lane := range committee.Lanes().All() { - key := secretKeyFor(keys, lane) + key := secretKeyFor(keys, lane.Validator()) vote := NewLaneVote(NewBlock(lane, 0, GenBlockHeaderHash(rng), GenPayload(rng)).Header()) laneQCs[lane] = NewLaneQC([]*Signed[*LaneVote]{Sign(key, vote)}) } diff --git a/sei-tendermint/internal/autobahn/autobahn.proto b/sei-tendermint/internal/autobahn/autobahn.proto index 3b04dd242f..195d5bc703 100644 --- a/sei-tendermint/internal/autobahn/autobahn.proto +++ b/sei-tendermint/internal/autobahn/autobahn.proto @@ -62,6 +62,15 @@ message PublicKey { optional bytes ed25519 = 1 [(wireguard.max_size) = 32]; // required } +// LaneID identifies a validator's lane for a continuous committee membership +// streak. e_join is the epoch in which the validator most recently joined. +message LaneID { + option (hashable.hashable) = true; + option (wireguard.sized) = true; + optional PublicKey validator = 1; // required + optional uint64 e_join = 2; // required +} + message Signature { option (hashable.hashable) = true; option (wireguard.sized) = true; @@ -72,10 +81,14 @@ message Signature { message BlockHeader { option (hashable.hashable) = true; option (wireguard.sized) = true; - optional PublicKey lane = 1; // required + // Field 1 was PublicKey "lane"; LaneID is additive on a new number/name so + // WIRE / WIRE_JSON stay compatible (must reserve the old name). + reserved 1; + reserved "lane"; optional uint64 block_number = 2; // required optional bytes parent_hash = 3 [(wireguard.max_size) = 32]; // required optional bytes payload_hash = 4 [(wireguard.max_size) = 32]; // required + optional LaneID lane_id = 5; // required } message Payload { @@ -108,10 +121,14 @@ message LaneQC { message LaneRange { option (hashable.hashable) = true; option (wireguard.sized) = true; - optional PublicKey lane = 1; // required + // Field 1 was PublicKey "lane"; LaneID is additive on a new number/name so + // WIRE / WIRE_JSON stay compatible (must reserve the old name). + reserved 1; + reserved "lane"; optional uint64 first = 2; // required optional uint64 next = 3; // required optional bytes last_hash = 4 [(wireguard.max_size) = 32]; // required + optional LaneID lane_id = 5; // required } message View { diff --git a/sei-tendermint/internal/autobahn/avail/conv_test.go b/sei-tendermint/internal/autobahn/avail/conv_test.go index ca753e5ee5..be2cf6f85d 100644 --- a/sei-tendermint/internal/autobahn/avail/conv_test.go +++ b/sei-tendermint/internal/autobahn/avail/conv_test.go @@ -14,7 +14,7 @@ func TestPruneAnchorConv(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane := keys[0].Public() + lane := types.NewLaneID(keys[0].Public(), 0) block := types.NewBlock(lane, 0, types.BlockHeaderHash{}, types.GenPayload(rng)) laneQCs := map[types.LaneID]*types.LaneQC{ lane: types.NewLaneQC(makeLaneVotes(keys, block.Header())), diff --git a/sei-tendermint/internal/autobahn/avail/inner.go b/sei-tendermint/internal/autobahn/avail/inner.go index d24eec2a2a..c82de40b9c 100644 --- a/sei-tendermint/internal/autobahn/avail/inner.go +++ b/sei-tendermint/internal/autobahn/avail/inner.go @@ -7,16 +7,14 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/avail/metrics" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/consensus/persist" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/epoch" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" ) -// TODO: when dynamic committee changes are supported, newly joined members -// must be added to blocks, votes, nextBlockToPersist, and persistedBlockStart. -// Currently all four are initialized once in newInner from c.Lanes().All(). -// BlockPersister creates lane WALs lazily inside MaybePruneAndPersistLane, but the new -// member must also appear in inner.blocks before the next persist cycle. +// Lane maps: joiners at ApplyEpoch; leavers until tipEpoch omits, then drop + DeleteLane. +// Restart re-attaches leave WALs; tipEpoch omit cleans them up. type inner struct { - epoch *types.Epoch + epoch utils.AtomicSend[*types.Epoch] latestAppQC utils.Option[*types.AppQC] latestCommitQC utils.AtomicSend[utils.Option[*types.CommitQC]] appVotes *queue[types.GlobalBlockNumber, appVotes] @@ -60,16 +58,17 @@ type loadedAvailState struct { blocks map[types.LaneID][]persist.LoadedBlock } -func newInner(epoch *types.Epoch, loaded utils.Option[*loadedAvailState]) (*inner, error) { +func newInner(registry *epoch.Registry, loaded utils.Option[*loadedAvailState]) (*inner, error) { + ep := registry.LatestEpoch() votes := map[types.LaneID]*queue[types.BlockNumber, blockVotes]{} blocks := map[types.LaneID]*queue[types.BlockNumber, *types.Signed[*types.LaneProposal]]{} - for lane := range epoch.Committee().Lanes().All() { + for lane := range ep.Committee().Lanes().All() { votes[lane] = newQueue[types.BlockNumber, blockVotes]() blocks[lane] = newQueue[types.BlockNumber, *types.Signed[*types.LaneProposal]]() } i := &inner{ - epoch: epoch, + epoch: utils.NewAtomicSend(ep), latestAppQC: utils.None[*types.AppQC](), latestCommitQC: utils.NewAtomicSend(utils.None[*types.CommitQC]()), appVotes: newQueue[types.GlobalBlockNumber, appVotes](), @@ -79,13 +78,38 @@ func newInner(epoch *types.Epoch, loaded utils.Option[*loadedAvailState]) (*inne nextBlockToPersist: make(map[types.LaneID]types.BlockNumber, len(votes)), persistedBlockStart: make(map[types.LaneID]types.BlockNumber, len(votes)), } - i.appVotes.prune(epoch.FirstBlock()) + i.appVotes.prune(ep.FirstBlock()) l, ok := loaded.Get() if !ok { return i, nil } + // Re-attach persisted WALs before prune. Skip e_join<=N absent from anchor + // committee (those LaneIDs never rejoin; proposal ranges may omit empty lanes). + var anchorEpoch types.EpochIndex + var anchorCommittee *types.Committee + if anchor, ok := l.pruneAnchor.Get(); ok { + anchorEpoch = anchor.CommitQC.Proposal().EpochIndex() + ep, ok := registry.EpochByIndex(anchorEpoch) + if !ok { + return nil, fmt.Errorf("unknown epoch_index %d for prune anchor", anchorEpoch) + } + anchorCommittee = ep.Committee() + } + for lane := range l.blocks { + if anchorCommittee != nil && lane.EJoin() <= anchorEpoch && !anchorCommittee.HasLane(lane) { + continue + } + if _, ok := i.blocks[lane]; ok { + continue + } + i.blocks[lane] = newQueue[types.BlockNumber, *types.Signed[*types.LaneProposal]]() + i.votes[lane] = newQueue[types.BlockNumber, blockVotes]() + i.nextBlockToPersist[lane] = 0 + i.persistedBlockStart[lane] = 0 + } + // Apply the persisted prune anchor first: prune() positions all queues // (commitQCs, blocks, votes) so that subsequent pushBack calls insert // at the correct indices without needing reset(). @@ -94,8 +118,7 @@ func newInner(epoch *types.Epoch, loaded utils.Option[*loadedAvailState]) (*inne slog.Uint64("roadIndex", uint64(anchor.AppQC.Proposal().RoadIndex())), slog.Uint64("globalNumber", uint64(anchor.AppQC.Proposal().GlobalNumber())), ) - // TODO: use the committee of the anchor's epoch once epoch transitions are wired up. - if _, err := i.prune(epoch.Committee(), anchor.AppQC, anchor.CommitQC); err != nil { + if _, err := i.prune(anchorCommittee, anchor.AppQC, anchor.CommitQC); err != nil { return nil, fmt.Errorf("prune: %w", err) } for lane := range i.blocks { @@ -118,14 +141,16 @@ func newInner(epoch *types.Epoch, loaded utils.Option[*loadedAvailState]) (*inne i.latestCommitQC.Store(utils.Some(i.commitQCs.q[i.commitQCs.next-1])) } - // Restore persisted blocks. Since the anchor is persisted first and - // blocks are written sequentially per lane, gaps, parent-hash - // mismatches, and over-capacity indicate corruption or a bug. + // Restore persisted blocks for re-attached lanes. Gaps / bad parent / over-cap → error. for lane, bs := range l.blocks { q, ok := i.blocks[lane] if !ok || len(bs) == 0 { continue } + // Unnamed-by-tipEpoch tips start at First=0; advance to WAL start before load. + if bs[0].Number > q.next { + q.prune(bs[0].Number) + } var lastHash types.BlockHeaderHash for j, b := range bs { if q.Len() >= BlocksPerLane { @@ -150,10 +175,47 @@ func newInner(epoch *types.Epoch, loaded utils.Option[*loadedAvailState]) (*inne return i, nil } +// addCommitteeLanes adds empty queues for new committee LaneIDs. +func (i *inner) addCommitteeLanes(c *types.Committee) { + for lane := range c.Lanes().All() { + if _, ok := i.blocks[lane]; ok { + continue + } + i.blocks[lane] = newQueue[types.BlockNumber, *types.Signed[*types.LaneProposal]]() + i.votes[lane] = newQueue[types.BlockNumber, blockVotes]() + i.nextBlockToPersist[lane] = 0 + i.persistedBlockStart[lane] = 0 + } +} + +// dropLanes removes block/vote maps for the given LaneIDs (tipEpoch leave prune). +func (i *inner) dropLanes(lanes []types.LaneID) int { + n := 0 + for _, lane := range lanes { + if _, ok := i.blocks[lane]; !ok { + continue + } + delete(i.blocks, lane) + delete(i.votes, lane) + delete(i.nextBlockToPersist, lane) + delete(i.persistedBlockStart, lane) + n++ + } + return n +} + // TODO: filter votes per-epoch committee once epoch transitions are wired up. func (i *inner) laneQC(lane types.LaneID, n types.BlockNumber) (*types.LaneQC, bool) { - c := i.epoch.Committee() - for _, byHash := range i.votes[lane].q[n].byHash { + c := i.epoch.Load().Committee() + votes, ok := i.votes[lane] + if !ok { + return nil, false + } + entry, ok := votes.q[n] + if !ok { + return nil, false + } + for _, byHash := range entry.byHash { if byHash.weight >= c.LaneQuorum() { return types.NewLaneQC(byHash.votes[:]), true } diff --git a/sei-tendermint/internal/autobahn/avail/inner_test.go b/sei-tendermint/internal/autobahn/avail/inner_test.go index a6deb0a076..9c95c58124 100644 --- a/sei-tendermint/internal/autobahn/avail/inner_test.go +++ b/sei-tendermint/internal/autobahn/avail/inner_test.go @@ -2,6 +2,7 @@ package avail import ( "testing" + "time" "github.com/sei-protocol/sei-chain/sei-db/ledger_db/block/memblock" "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" @@ -22,7 +23,7 @@ func TestPruneMismatchedIndices(t *testing.T) { registry, keys := epoch.GenRegistry(rng, 4) makeCommitQC := func(prev utils.Option[*types.CommitQC]) *types.CommitQC { - l := keys[0].Public() + l := types.NewLaneID(keys[0].Public(), 0) lr := types.LaneRangeOpt(prev, l) b := types.NewBlock(l, lr.Next(), lr.LastHash(), types.GenPayload(rng)) lqcs := map[types.LaneID]*types.LaneQC{ @@ -72,7 +73,7 @@ func TestNewInnerFreshStart(t *testing.T) { rng := utils.TestRng() registry, _ := epoch.GenRegistry(rng, 4) - i, err := newInner(registry.LatestEpoch(), utils.None[*loadedAvailState]()) + i, err := newInner(registry, utils.None[*loadedAvailState]()) require.NoError(t, err) require.False(t, i.latestAppQC.IsPresent()) @@ -109,7 +110,7 @@ func TestNewInnerLoadedNoAnchor(t *testing.T) { loaded := &loadedAvailState{} - i, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + i, err := newInner(registry, utils.Some(loaded)) require.NoError(t, err) // No anchor loaded, app votes should start at the registry's first block. @@ -121,7 +122,7 @@ func TestNewInnerLoadedNoAnchor(t *testing.T) { func TestNewInnerLoadedBlocksContiguous(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane := keys[0].Public() + lane := types.NewLaneID(keys[0].Public(), 0) // Build 3 contiguous blocks: 0, 1, 2. var parent types.BlockHeaderHash @@ -136,7 +137,7 @@ func TestNewInnerLoadedBlocksContiguous(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - i, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + i, err := newInner(registry, utils.Some(loaded)) require.NoError(t, err) q := i.blocks[lane] @@ -159,13 +160,13 @@ func TestNewInnerLoadedBlocksContiguous(t *testing.T) { func TestNewInnerLoadedBlocksEmptySlice(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane := keys[0].Public() + lane := types.NewLaneID(keys[0].Public(), 0) loaded := &loadedAvailState{ blocks: map[types.LaneID][]persist.LoadedBlock{lane: {}}, } - i, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + i, err := newInner(registry, utils.Some(loaded)) require.NoError(t, err) q := i.blocks[lane] @@ -178,14 +179,14 @@ func TestNewInnerLoadedBlocksUnknownLane(t *testing.T) { registry, keys := epoch.GenRegistry(rng, 4) unknownKey := types.GenSecretKey(rng) - unknownLane := unknownKey.Public() + unknownLane := types.NewLaneID(unknownKey.Public(), 0) b := testSignedBlock(unknownKey, unknownLane, 0, types.BlockHeaderHash{}, rng) loaded := &loadedAvailState{ blocks: map[types.LaneID][]persist.LoadedBlock{unknownLane: {{Number: 0, Proposal: b}}}, } - i, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + i, err := newInner(registry, utils.Some(loaded)) require.NoError(t, err) for lane := range registry.LatestEpoch().Committee().Lanes().All() { @@ -199,8 +200,8 @@ func TestNewInnerLoadedBlocksUnknownLane(t *testing.T) { func TestNewInnerLoadedBlocksMultipleLanes(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane0 := keys[0].Public() - lane1 := keys[1].Public() + lane0 := types.NewLaneID(keys[0].Public(), 0) + lane1 := types.NewLaneID(keys[1].Public(), 0) var parent0 types.BlockHeaderHash var bs0 []persist.LoadedBlock @@ -222,7 +223,7 @@ func TestNewInnerLoadedBlocksMultipleLanes(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane0: bs0, lane1: bs1}, } - i, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + i, err := newInner(registry, utils.Some(loaded)) require.NoError(t, err) q0 := i.blocks[lane0] @@ -259,7 +260,7 @@ func TestNewInnerLoadedCommitQCsNoAppQC(t *testing.T) { commitQCs: loadedQCs, } - inner, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + inner, err := newInner(registry, utils.Some(loaded)) require.NoError(t, err) // Without anchor, commitQCs.first = 0. All 3 should be restored. @@ -305,7 +306,7 @@ func TestNewInnerLoadedCommitQCsWithAppQC(t *testing.T) { commitQCs: loadedQCs, } - inner, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + inner, err := newInner(registry, utils.Some(loaded)) require.NoError(t, err) // latestAppQC should be set by prune. @@ -330,7 +331,7 @@ func TestNewInnerLoadedCommitQCsWithAppQC(t *testing.T) { func TestNewInnerLoadedAllThree(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane := keys[0].Public() + lane := types.NewLaneID(keys[0].Public(), 0) // AppQC at road index 2. roadIdx := types.RoadIndex(2) @@ -366,7 +367,7 @@ func TestNewInnerLoadedAllThree(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - inner, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + inner, err := newInner(registry, utils.Some(loaded)) require.NoError(t, err) // AppQC restored. @@ -393,9 +394,9 @@ func TestNewInnerLoadedAllThree(t *testing.T) { func TestPruneAdvancesNextBlockToPersist(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane := keys[0].Public() + lane := types.NewLaneID(keys[0].Public(), 0) - i, err := newInner(registry.LatestEpoch(), utils.None[*loadedAvailState]()) + i, err := newInner(registry, utils.None[*loadedAvailState]()) require.NoError(t, err) // Push blocks 0-4 on one lane. @@ -470,7 +471,7 @@ func TestNewInnerLoadedCommitQCsAllBeforeAppQCArePruned(t *testing.T) { pruneAnchor: utils.Some(&PruneAnchor{AppQC: appQC, CommitQC: qcs[5]}), } - inner, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + inner, err := newInner(registry, utils.Some(loaded)) require.NoError(t, err) // prune() pushes the anchor's CommitQC into the queue. @@ -499,7 +500,7 @@ func TestNewInnerAnchorWithNoCommitQCFiles(t *testing.T) { pruneAnchor: utils.Some(&PruneAnchor{AppQC: appQC, CommitQC: qcs[3]}), } - inner, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + inner, err := newInner(registry, utils.Some(loaded)) require.NoError(t, err) // prune() should push the anchor's CommitQC into the queue. @@ -542,7 +543,7 @@ func TestNewInnerLoadedCommitQCsGapReturnsError(t *testing.T) { commitQCs: loadedQCs, } - _, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + _, err := newInner(registry, utils.Some(loaded)) require.Error(t, err) require.Contains(t, err.Error(), "non-contiguous") } @@ -555,7 +556,7 @@ func TestNewInnerLoadedCommitQCsEmpty(t *testing.T) { commitQCs: nil, } - inner, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + inner, err := newInner(registry, utils.Some(loaded)) require.NoError(t, err) require.Equal(t, types.RoadIndex(0), inner.commitQCs.first) @@ -590,7 +591,7 @@ func TestNewInnerLoadedCommitQCsGapWithAppQCAnchor(t *testing.T) { commitQCs: loadedQCs, } - inner, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + inner, err := newInner(registry, utils.Some(loaded)) require.NoError(t, err) // Only QC@10 loaded. @@ -639,7 +640,7 @@ func TestNewInnerLoadedCommitQCsBelowAnchorSkipped(t *testing.T) { commitQCs: loadedQCs, } - inner, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + inner, err := newInner(registry, utils.Some(loaded)) require.NoError(t, err) // prune(3) pushes QC@3 (next=4). Indices 1,2,3 are skipped. 4,5 pushed. @@ -678,7 +679,7 @@ func TestNewInnerLoadedCommitQCsGapAfterAnchorReturnsError(t *testing.T) { commitQCs: loadedQCs, } - _, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + _, err := newInner(registry, utils.Some(loaded)) require.Error(t, err) require.Contains(t, err.Error(), "non-contiguous") } @@ -686,7 +687,7 @@ func TestNewInnerLoadedCommitQCsGapAfterAnchorReturnsError(t *testing.T) { func TestNewInnerLoadedBlocksGapReturnsError(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane := keys[0].Public() + lane := types.NewLaneID(keys[0].Public(), 0) // Blocks 3, 4, 6, 7 with no anchor — queue starts at 0, so block 3 // fails the contiguity check immediately (expected 0, got 3). @@ -702,7 +703,7 @@ func TestNewInnerLoadedBlocksGapReturnsError(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - _, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + _, err := newInner(registry, utils.Some(loaded)) require.Error(t, err) require.Contains(t, err.Error(), "non-contiguous") } @@ -710,7 +711,7 @@ func TestNewInnerLoadedBlocksGapReturnsError(t *testing.T) { func TestNewInnerLoadedBlocksParentHashMismatchReturnsError(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane := keys[0].Public() + lane := types.NewLaneID(keys[0].Public(), 0) // Build blocks 0, 1 with correct chaining, then block 2 with wrong parent. var parent types.BlockHeaderHash @@ -730,7 +731,7 @@ func TestNewInnerLoadedBlocksParentHashMismatchReturnsError(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - _, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + _, err := newInner(registry, utils.Some(loaded)) require.Error(t, err) require.Contains(t, err.Error(), "parent hash mismatch") } @@ -738,7 +739,7 @@ func TestNewInnerLoadedBlocksParentHashMismatchReturnsError(t *testing.T) { func TestNewInnerLoadedBlocksOverCapacityReturnsError(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane := keys[0].Public() + lane := types.NewLaneID(keys[0].Public(), 0) // Build BlocksPerLane + 5 contiguous blocks — more than the lane capacity. // Since runtime enforces the capacity limit, exceeding it on disk indicates @@ -756,7 +757,7 @@ func TestNewInnerLoadedBlocksOverCapacityReturnsError(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - _, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + _, err := newInner(registry, utils.Some(loaded)) require.Error(t, err) require.Contains(t, err.Error(), "exceeds capacity") } @@ -779,7 +780,7 @@ func TestNewInnerPruneAnchorPrunesBlockQueues(t *testing.T) { appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) pruneQC := qcs[2] - lane := keys[0].Public() + lane := types.NewLaneID(keys[0].Public(), 0) // Persist some blocks starting at the lane range for the prune CommitQC. lrFirst := pruneQC.LaneRange(lane).First() @@ -799,7 +800,7 @@ func TestNewInnerPruneAnchorPrunesBlockQueues(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - i, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + i, err := newInner(registry, utils.Some(loaded)) require.NoError(t, err) // prune() should advance block queue first to the prune anchor's lane range. @@ -835,7 +836,7 @@ func TestNewInnerPruneAnchorCommitQCUsedForPrune(t *testing.T) { }, } - i, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) + i, err := newInner(registry, utils.Some(loaded)) require.NoError(t, err) // prune(appQC@1, pruneQC@1) should advance commitQCs.first to 1. @@ -843,3 +844,112 @@ func TestNewInnerPruneAnchorCommitQCUsedForPrune(t *testing.T) { // CommitQCs 1 and 2 should still be loaded. require.Equal(t, types.RoadIndex(3), i.commitQCs.next) } + +// Leave-lane WALs are re-attached on restart even when LatestEpoch omits them +// (kept until tipEpoch prune while the node is running). +func TestNewInnerRestoresLeaveLaneWAL(t *testing.T) { + rng := utils.TestRng() + registry, keys := epoch.GenRegistry(rng, 3) + a, b := keys[0], keys[1] + cKey := types.GenSecretKey(rng) + ep0 := registry.LatestEpoch() + laneB := ep0.Committee().Lane(b.Public()).OrPanic("b") + + b0 := testSignedBlock(b, laneB, 0, types.BlockHeaderHash{}, rng) + loaded := &loadedAvailState{ + blocks: map[types.LaneID][]persist.LoadedBlock{ + laneB: {{Number: 0, Proposal: b0}}, + }, + } + + ep1, err := registry.ActivateEpoch( + map[types.PublicKey]uint64{a.Public(): 1, cKey.Public(): 1}, + types.OpenRoadRange(), time.Time{}, registry.FirstBlock(), + ) + require.NoError(t, err) + require.False(t, ep1.Committee().HasLane(laneB)) + + i, err := newInner(registry, utils.Some(loaded)) + require.NoError(t, err) + require.Contains(t, i.blocks, laneB) + require.Equal(t, types.BlockNumber(1), i.blocks[laneB].next) + require.Contains(t, i.votes, laneB) +} + +// Anchor at epoch N that still names a leave lane: restore and position via prune. +func TestNewInnerRestoresLeaveLaneNamedByAnchor(t *testing.T) { + rng := utils.TestRng() + registry, keys := epoch.GenRegistry(rng, 3) + a, b := keys[0], keys[1] + cKey := types.GenSecretKey(rng) + ep0 := registry.LatestEpoch() + laneB := ep0.Committee().Lane(b.Public()).OrPanic("b") + + qc0 := makeCommitQC(ep0, keys, utils.None[*types.CommitQC](), nil, utils.None[*types.AppQC]()) + require.True(t, ep0.Committee().HasLane(laneB)) + appProposal := types.NewAppProposal(qc0.GlobalRange().First, qc0.Index(), types.GenAppHash(rng), 0) + appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) + + lrFirst := qc0.LaneRange(laneB).First() + b0 := testSignedBlock(b, laneB, lrFirst, types.BlockHeaderHash{}, rng) + loaded := &loadedAvailState{ + pruneAnchor: utils.Some(&PruneAnchor{AppQC: appQC, CommitQC: qc0}), + commitQCs: []persist.LoadedCommitQC{{Index: qc0.Index(), QC: qc0}}, + blocks: map[types.LaneID][]persist.LoadedBlock{ + laneB: {{Number: lrFirst, Proposal: b0}}, + }, + } + + _, err := registry.ActivateEpoch( + map[types.PublicKey]uint64{a.Public(): 1, cKey.Public(): 1}, + types.OpenRoadRange(), time.Time{}, registry.FirstBlock(), + ) + require.NoError(t, err) + + i, err := newInner(registry, utils.Some(loaded)) + require.NoError(t, err) + require.Contains(t, i.blocks, laneB) + require.Equal(t, lrFirst, i.blocks[laneB].first) + require.Equal(t, lrFirst+1, i.blocks[laneB].next) +} + +// With anchor epoch N, lanes with e_join <= N absent from that epoch's committee +// are skipped (left for good; orphan WAL dirs may remain unused on disk). +func TestNewInnerSkipsStaleLaneAbsentFromAnchor(t *testing.T) { + rng := utils.TestRng() + registry, keys := epoch.GenRegistry(rng, 3) + a := keys[0] + cKey := types.GenSecretKey(rng) + + ep1, err := registry.ActivateEpoch( + map[types.PublicKey]uint64{a.Public(): 1, cKey.Public(): 1}, + types.OpenRoadRange(), time.Time{}, registry.FirstBlock(), + ) + require.NoError(t, err) + qc1 := makeCommitQC(ep1, []types.SecretKey{a, cKey}, utils.None[*types.CommitQC](), nil, utils.None[*types.AppQC]()) + require.Equal(t, types.EpochIndex(1), qc1.Proposal().EpochIndex()) + + // e_join == N covers the <= bound (not only e_join < N). + orphan := types.NewLaneID(types.GenSecretKey(rng).Public(), qc1.Proposal().EpochIndex()) + require.Equal(t, orphan.EJoin(), qc1.Proposal().EpochIndex()) + require.False(t, ep1.Committee().HasLane(orphan)) + + app1 := types.NewAppProposal(qc1.GlobalRange().First, qc1.Index(), types.GenAppHash(rng), 1) + appQC1 := types.NewAppQC([]*types.Signed[*types.AppVote]{ + types.Sign(a, types.NewAppVote(app1)), + types.Sign(cKey, types.NewAppVote(app1)), + }) + + ob := testSignedBlock(types.GenSecretKey(rng), orphan, 0, types.BlockHeaderHash{}, rng) + loaded := &loadedAvailState{ + pruneAnchor: utils.Some(&PruneAnchor{AppQC: appQC1, CommitQC: qc1}), + commitQCs: []persist.LoadedCommitQC{{Index: qc1.Index(), QC: qc1}}, + blocks: map[types.LaneID][]persist.LoadedBlock{ + orphan: {{Number: 0, Proposal: ob}}, + }, + } + + i, err := newInner(registry, utils.Some(loaded)) + require.NoError(t, err) + require.NotContains(t, i.blocks, orphan) +} diff --git a/sei-tendermint/internal/autobahn/avail/state.go b/sei-tendermint/internal/autobahn/avail/state.go index 8a2e172c44..6691a6df87 100644 --- a/sei-tendermint/internal/autobahn/avail/state.go +++ b/sei-tendermint/internal/autobahn/avail/state.go @@ -10,6 +10,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/avail/metrics" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/consensus/persist" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/data" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/epoch" pb "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/pb" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/protoutils" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" @@ -19,6 +20,10 @@ import ( // ErrBadLane . var ErrBadLane = errors.New("bad lane") +// ErrLanePruned: SubscribeLaneProposals.Recv after tipEpoch drop of the bound leave map. +// Leave alone keeps serving until prune; rejoin needs a new Subscribe. +var ErrLanePruned = errors.New("lane pruned") + const BlocksPerLane = 3 * types.MaxLaneRangeInProposal // State represents the Data Availability Plane and Ordered Event Log. @@ -35,13 +40,113 @@ type State struct { data *data.State inner utils.Watch[*inner] + // Mirror of inner.epoch (AtomicRecv). Stay (same LaneID) must not end a produce session. + epoch utils.AtomicRecv[*types.Epoch] + // persisters groups all disk persistence components. // Always initialized: real when stateDir is set, no-op otherwise. persisters persisters } -func (s *State) PublicKey() types.PublicKey { - return s.key.Public() +// LocalLane is this node's applied-committee LaneID, if any. +func (s *State) LocalLane() utils.Option[types.LaneID] { + return s.epoch.Load().Committee().Lane(s.key.Public()) +} + +// WaitLocalLane waits until pred(LocalLane()). Stay does not satisfy a "changed" pred. +func (s *State) WaitLocalLane(ctx context.Context, pred func(utils.Option[types.LaneID]) bool) (utils.Option[types.LaneID], error) { + pk := s.key.Public() + var lane utils.Option[types.LaneID] + _, err := s.epoch.Wait(ctx, func(ep *types.Epoch) bool { + lane = ep.Committee().Lane(pk) + return pred(lane) + }) + if err != nil { + return utils.None[types.LaneID](), err + } + return lane, nil +} + +// WaitProduce waits until LocalLane is Some (produce session start). +func (s *State) WaitProduce(ctx context.Context) (types.LaneID, error) { + laneOpt, err := s.WaitLocalLane(ctx, func(opt utils.Option[types.LaneID]) bool { + return opt.IsPresent() + }) + if err != nil { + return types.LaneID{}, err + } + return laneOpt.OrPanic("present"), nil +} + +// WaitMustStop waits until LocalLane is None or != lane (produce session stop). +func (s *State) WaitMustStop(ctx context.Context, lane types.LaneID) error { + _, err := s.WaitLocalLane(ctx, func(opt utils.Option[types.LaneID]) bool { + got, ok := opt.Get() + return !ok || got != lane + }) + return err +} + +// ApplyEpoch installs the applied committee: add joiner maps, then Store ep under +// the inner lock so waiters never observe the new committee before those maps exist. +// Leavers stay until tipEpoch omits them (persist path). Registry ActivateEpoch is separate. +func (s *State) ApplyEpoch(ep *types.Epoch) { + for inner, ctrl := range s.inner.Lock() { + inner.addCommitteeLanes(ep.Committee()) + inner.epoch.Store(ep) + ctrl.Updated() + } +} + +// tipEpochOf is the registry epoch of the first retained CommitQC. +func tipEpochOf(inner *inner, registry *epoch.Registry) (utils.Option[*types.Epoch], error) { + if inner.commitQCs.first >= inner.commitQCs.next { + return utils.None[*types.Epoch](), nil + } + idx := inner.commitQCs.q[inner.commitQCs.first].Proposal().EpochIndex() + ep, found := registry.EpochByIndex(idx) + if !found { + return utils.None[*types.Epoch](), fmt.Errorf("unknown epoch_index %d for tipEpoch CommitQC", idx) + } + return utils.Some(ep), nil +} + +// staleLaneDisposable: tipEpoch omits lane and e_join < tip (joiners at/after tip stay). +// None tipEpoch → false. +func staleLaneDisposable(lane types.LaneID, tipEpoch utils.Option[*types.Epoch]) bool { + ep, ok := tipEpoch.Get() + if !ok { + return false + } + return lane.EJoin() < ep.EpochIndex() && !ep.Committee().HasLane(lane) +} + +// deleteStaleLaneWAL Deletes WALs for tip-stale leave maps. +// DeleteLane no-ops if a lane never opened a WAL (empty leave). +func (s *State) deleteStaleLaneWAL(lanes []types.LaneID) error { + for _, lane := range lanes { + if err := s.persisters.blocks.DeleteLane(lane); err != nil { + return fmt.Errorf("DeleteLane(%s): %w", lane, err) + } + } + return nil +} + +// pruneStaleLeave Deletes WALs then drops maps for tip-stale leave LaneIDs +// (same tick as runPersist after Parallel). +func (s *State) pruneStaleLeave(staleLeave []types.LaneID) error { + if err := s.deleteStaleLaneWAL(staleLeave); err != nil { + return err + } + if len(staleLeave) == 0 { + return nil + } + for inner, ctrl := range s.inner.Lock() { + if inner.dropLanes(staleLeave) > 0 { + ctrl.Updated() + } + } + return nil } // persisters holds all disk persistence components. Either all are present @@ -194,17 +299,19 @@ func NewState(key types.SecretKey, data *data.State, stateDir utils.Option[strin }() ep := data.Registry().LatestEpoch() - inner, err := newInner(ep, loaded) + inner, err := newInner(data.Registry(), loaded) if err != nil { return nil, err } // Truncate WAL entries below the prune anchor that were filtered out by - // loadPersistedState. + // loadPersistedState. Includes restored leave lanes; tipEpoch prune deletes + // leave WALs once their maps become staleLeave. if ls, ok := loaded.Get(); ok { if anchor, ok := ls.pruneAnchor.Get(); ok { - for lane := range ep.Committee().Lanes().All() { - if err := pers.blocks.MaybePruneAndPersistLane(lane, utils.Some(anchor.CommitQC), nil, utils.None[func(*types.Signed[*types.LaneProposal])]()); err != nil { + c := ep.Committee() + for lane := range inner.blocks { + if err := pers.blocks.MaybePruneAndPersistLane(lane, c, utils.Some(anchor.CommitQC), nil, utils.None[func(*types.Signed[*types.LaneProposal])]()); err != nil { return nil, fmt.Errorf("prune stale block WAL entries: %w", err) } } @@ -218,6 +325,7 @@ func NewState(key types.SecretKey, data *data.State, stateDir utils.Option[strin key: key, data: data, inner: utils.NewWatch(inner), + epoch: inner.epoch.Subscribe(), persisters: pers, }, nil } @@ -324,8 +432,8 @@ func (s *State) PushCommitQC(ctx context.Context, qc *types.CommitQC) error { if idx != inner.commitQCs.next { return nil } - if qc.Proposal().EpochIndex() != inner.epoch.EpochIndex() { - return fmt.Errorf("commitQC epoch_index %d != current epoch %d", qc.Proposal().EpochIndex(), inner.epoch.EpochIndex()) + if got, want := qc.Proposal().EpochIndex(), inner.epoch.Load().EpochIndex(); got != want { + return fmt.Errorf("commitQC epoch_index %d != current epoch %d", got, want) } inner.commitQCs.pushBack(qc) metrics.ObserveCommitQC(qc) @@ -439,15 +547,19 @@ func (s *State) NextBlock(lane types.LaneID) types.BlockNumber { // Block returns block n of the given lane. // Waits until the block is available. // Returns ErrPruned if the block has been already pruned. +// Returns ErrBadLane if the lane map is gone (tipEpoch leave prune). func (s *State) Block(ctx context.Context, lane types.LaneID, n types.BlockNumber) (*types.Signed[*types.LaneProposal], error) { for inner, ctrl := range s.inner.Lock() { + if err := ctrl.WaitUntil(ctx, func() bool { + q, ok := inner.blocks[lane] + return !ok || n < q.next + }); err != nil { + return nil, err + } q, ok := inner.blocks[lane] if !ok { return nil, ErrBadLane } - if err := ctrl.WaitUntil(ctx, func() bool { return n < q.next }); err != nil { - return nil, err - } if n < q.first { return nil, types.ErrPruned } @@ -460,7 +572,7 @@ func (s *State) Block(ctx context.Context, lane types.LaneID, n types.BlockNumbe // Waits until all previous blocks are available. func (s *State) PushBlock(ctx context.Context, p *types.Signed[*types.LaneProposal]) error { h := p.Msg().Block().Header() - if p.Key() != h.Lane() { + if p.Key() != h.Lane().Validator() { return fmt.Errorf("signer %v does not match lane %v", p.Key(), h.Lane()) } if _, err := s.data.Registry().VerifyInWindow(func(c *types.Committee) error { @@ -540,7 +652,7 @@ func (s *State) PushVote(ctx context.Context, vote *types.Signed[*types.LaneVote for q.next <= h.BlockNumber() { q.pushBack(newBlockVotes()) } - if _, ok := q.q[h.BlockNumber()].pushVote(inner.epoch, vote); ok { + if _, ok := q.q[h.BlockNumber()].pushVote(inner.epoch.Load(), vote); ok { ctrl.Updated() } } @@ -548,6 +660,8 @@ func (s *State) PushVote(ctx context.Context, vote *types.Signed[*types.LaneVote } // headers collects headers for the given range. +// Missing vote queue (leave map dropped past AppQC floor) → ErrPruned so PushQC can skip; +// ErrBadLane would kill avail.Run. func (s *State) headers(ctx context.Context, lr *types.LaneRange) ([]*types.BlockHeader, error) { // Empty range is always available. if lr.First() == lr.Next() { @@ -556,10 +670,14 @@ func (s *State) headers(ctx context.Context, lr *types.LaneRange) ([]*types.Bloc want := lr.LastHash() headers := make([]*types.BlockHeader, lr.Next()-lr.First()) for inner, ctrl := range s.inner.Lock() { - q := inner.votes[lr.Lane()] for i := range headers { n := lr.Next() - types.BlockNumber(i) - 1 //nolint:gosec // i is bounded by len(headers) which is a small block range; no overflow risk for { + // Re-check after Wait: tipEpoch may drop the leave map mid-assembly. + q, ok := inner.votes[lr.Lane()] + if !ok { + return nil, types.ErrPruned + } // If pruned, then give up. if q.first > lr.First() { return nil, types.ErrPruned @@ -603,15 +721,27 @@ func (s *State) fullCommitQC(ctx context.Context, n types.RoadIndex) (*types.Ful return types.NewFullCommitQC(qc, commitHeaders), nil } -// WaitForLocalCapacity waits until the lane owned by this node has capacity for toProduce block. -func (s *State) WaitForLocalCapacity(ctx context.Context, toProduce types.BlockNumber) error { - lane := s.key.Public() +// WaitForLocalCapacity waits until the lane has capacity for toProduce. +// ErrBadLane if the lane left committee or its map was tipEpoch-pruned while waiting. +func (s *State) WaitForLocalCapacity(ctx context.Context, lane types.LaneID, toProduce types.BlockNumber) error { for inner, ctrl := range s.inner.Lock() { if err := ctrl.WaitUntil(ctx, func() bool { + if !inner.epoch.Load().Committee().HasLane(lane) { + return true + } + if _, ok := inner.blocks[lane]; !ok { + return true + } return toProduce < inner.persistedBlockStart[lane]+BlocksPerLane }); err != nil { return err } + if !inner.epoch.Load().Committee().HasLane(lane) { + return ErrBadLane + } + if _, ok := inner.blocks[lane]; !ok { + return ErrBadLane + } } return nil } @@ -645,17 +775,16 @@ func (s *State) WaitForLaneQCs( panic("unreachable") } -// ProduceLocalBlock appends a new block to the producers lane. -// Fails in case there is not enough capacity in the lane, or it is not the next block expected. -func (s *State) ProduceLocalBlock(n types.BlockNumber, payload *types.Payload) (*types.Signed[*types.LaneProposal], error) { - return s.produceLocalBlock(n, s.key, payload) -} - -// TODO: produceLocalBlock is a separate function for testing - consider improving the tests to use ProduceBlock only. -func (s *State) produceLocalBlock(n types.BlockNumber, key types.SecretKey, payload *types.Payload) (*types.Signed[*types.LaneProposal], error) { - lane := key.Public() +// ProduceLocalBlock appends block n on the WaitProduce session lane. +func (s *State) ProduceLocalBlock(lane types.LaneID, n types.BlockNumber, payload *types.Payload) (*types.Signed[*types.LaneProposal], error) { + if s.key.Public() != lane.Validator() { + return nil, ErrBadLane + } var result *types.Signed[*types.LaneProposal] for inner, ctrl := range s.inner.Lock() { + if !inner.epoch.Load().Committee().HasLane(lane) { + return nil, ErrBadLane + } q, ok := inner.blocks[lane] if !ok { return nil, ErrBadLane @@ -670,7 +799,7 @@ func (s *State) produceLocalBlock(n types.BlockNumber, key types.SecretKey, payl if q.first < q.next { parent = q.q[q.next-1].Msg().Block().Header().Hash() } - result = types.Sign(key, types.NewLaneProposal(types.NewBlock(lane, q.next, parent, payload))) + result = types.Sign(s.key, types.NewLaneProposal(types.NewBlock(lane, q.next, parent, payload))) q.pushBack(result) ctrl.Updated() } @@ -710,9 +839,14 @@ func (s *State) Run(ctx context.Context) error { for inner := range s.inner.Lock() { for lane := range c.Lanes().All() { lr := qc.QC().LaneRange(lane) + q, ok := inner.blocks[lr.Lane()] + if !ok { + // Leave map gone after AppQC floor (headers already ErrPruned). + continue + } for n := lr.First(); n < lr.Next(); n++ { // We are not expected to have all the blocks locally - only the available ones. - if b, ok := inner.blocks[lr.Lane()].q[n]; ok { + if b, ok := q.q[n]; ok { // We don't need to check the blocks against the headers, // as bad blocks will be filtered out by PushQC anyway. blocks = append(blocks, b.Msg().Block()) @@ -776,6 +910,8 @@ func (s *State) runPersist(ctx context.Context, pers persisters) error { blocksByLane[lane] = append(blocksByLane[lane], proposal) } + active := s.epoch.Load().Committee() + // 2. Persist commit-QCs and per-lane blocks in parallel. // Callees handle empty inputs gracefully (no-op when nothing to write/truncate). if err := scope.Parallel(func(ps scope.ParallelScope) error { @@ -805,13 +941,17 @@ func (s *State) runPersist(ctx context.Context, pers persisters) error { for lane := range batchLanes { proposals := blocksByLane[lane] ps.Spawn(func() error { - return pers.blocks.MaybePruneAndPersistLane(lane, anchorQC, proposals, utils.Some(markBlock)) + // allowCreate if active or proposals non-empty (leave flush before first WAL). + return pers.blocks.MaybePruneAndPersistLane(lane, active, anchorQC, proposals, utils.Some(markBlock)) }) } return nil }); err != nil { return err } + if err := s.pruneStaleLeave(batch.staleLeave); err != nil { + return err + } } } @@ -820,6 +960,9 @@ type persistBatch struct { blocks []*types.Signed[*types.LaneProposal] commitQCs []*types.CommitQC pruneAnchor utils.Option[*PruneAnchor] + // staleLeave: tipEpoch-disposable map keys skipped for append this tick. + // WAL deleted then maps dropped after Parallel (same iteration). + staleLeave []types.LaneID } // advancePersistedBlockStart updates the per-lane block admission watermark @@ -858,7 +1001,12 @@ func (s *State) markCommitQCsPersisted(qc *types.CommitQC) { } // collectPersistBatch waits for new blocks or commitQCs and collects them under lock. -func (s *State) collectPersistBatch(ctx context.Context, lastPersistedAppQCNext types.RoadIndex) (persistBatch, error) { +// TipEpoch-stale leave maps are listed in staleLeave (not appended); runPersist +// Deletes their WALs then drops the maps in the same iteration. +func (s *State) collectPersistBatch( + ctx context.Context, + lastPersistedAppQCNext types.RoadIndex, +) (persistBatch, error) { var b persistBatch for inner, ctrl := range s.inner.Lock() { // Derive the CommitQC persist cursor from latestCommitQC. This is @@ -881,7 +1029,15 @@ func (s *State) collectPersistBatch(ctx context.Context, lastPersistedAppQCNext }); err != nil { return b, err } + tipEpoch, err := tipEpochOf(inner, s.data.Registry()) + if err != nil { + return b, err + } for lane, q := range inner.blocks { + if staleLaneDisposable(lane, tipEpoch) { + b.staleLeave = append(b.staleLeave, lane) + continue + } start := max(inner.nextBlockToPersist[lane], q.first) for n := start; n < q.next; n++ { b.blocks = append(b.blocks, q.q[n]) diff --git a/sei-tendermint/internal/autobahn/avail/state_test.go b/sei-tendermint/internal/autobahn/avail/state_test.go index 7411cd0c45..c39b1abd6f 100644 --- a/sei-tendermint/internal/autobahn/avail/state_test.go +++ b/sei-tendermint/internal/autobahn/avail/state_test.go @@ -36,6 +36,26 @@ func makeAppVotes(keys []types.SecretKey, proposal *types.AppProposal) []*types. return votes } +// pushPeerLaneBlock admits a block signed by key onto state via PushBlock +// (foreign keys; production ProduceLocalBlock only signs with the State's key). +func pushPeerLaneBlock(ctx context.Context, state *State, key types.SecretKey, payload *types.Payload) (*types.Signed[*types.LaneProposal], error) { + lane := types.NewLaneID(key.Public(), 0) + n := state.NextBlock(lane) + var parent types.BlockHeaderHash + if n > 0 { + prev, err := state.Block(ctx, lane, n-1) + if err != nil { + return nil, err + } + parent = prev.Msg().Block().Header().Hash() + } + b := types.Sign(key, types.NewLaneProposal(types.NewBlock(lane, n, parent, payload))) + if err := state.PushBlock(ctx, b); err != nil { + return nil, err + } + return b, nil +} + func TestSubscribeAppVotesJumpsToDataFloor(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 3) @@ -146,12 +166,12 @@ func testState(t *testing.T, stateDir utils.Option[string]) { want := byLane[types.PayloadHash]{} for range 10 { key := keys[rng.Intn(len(keys))] - lane := key.Public() + lane := types.NewLaneID(key.Public(), 0) p := types.GenPayload(rng) want[lane] = append(want[lane], p.Hash()) - b, err := state.produceLocalBlock(state.NextBlock(lane), key, p) + b, err := pushPeerLaneBlock(ctx, state, key, p) if err != nil { - return fmt.Errorf("state.produceLocalBlock(): %w", err) + return fmt.Errorf("pushPeerLaneBlock(): %w", err) } if err := utils.TestDiff(b.Msg().Block().Payload(), p); err != nil { return fmt.Errorf("snapshot: %w", err) @@ -280,8 +300,8 @@ func TestStateRestartFromPersisted(t *testing.T) { for range 5 { key := keys[rng.Intn(len(keys))] - if _, err := state.produceLocalBlock(state.NextBlock(key.Public()), key, types.GenPayload(rng)); err != nil { - return fmt.Errorf("produceLocalBlock: %w", err) + if _, err := pushPeerLaneBlock(ctx, state, key, types.GenPayload(rng)); err != nil { + return fmt.Errorf("pushPeerLaneBlock: %w", err) } } @@ -388,9 +408,9 @@ func TestStateMismatchedQCs(t *testing.T) { } // 1. Produce a block so we have a non-empty range - lane := keys[0].Public() + lane := types.NewLaneID(keys[0].Public(), 0) p := types.GenPayload(rng) - b, err := state.ProduceLocalBlock(state.NextBlock(lane), p) + b, err := state.ProduceLocalBlock(lane, state.NextBlock(lane), p) require.NoError(t, err) // 2. Form a LaneQC for it @@ -424,11 +444,11 @@ func TestPushBlockRejectsBadParentHash(t *testing.T) { state := utils.OrPanic1(NewState(keys[0], ds, utils.Some(t.TempDir()))) // Produce a valid first block on our lane. - _, err := state.ProduceLocalBlock(state.NextBlock(keys[0].Public()), types.GenPayload(rng)) + lane := types.NewLaneID(keys[0].Public(), 0) + _, err := state.ProduceLocalBlock(lane, state.NextBlock(lane), types.GenPayload(rng)) require.NoError(t, err) // Create a second block with a fake parentHash. - lane := keys[0].Public() fakeBlock := types.NewBlock(lane, 1, types.GenBlockHeaderHash(rng), types.GenPayload(rng)) fakeProp := types.Sign(keys[0], types.NewLaneProposal(fakeBlock)) @@ -447,7 +467,7 @@ func TestPushBlockRejectsWrongSigner(t *testing.T) { state := utils.OrPanic1(NewState(keys[0], ds, utils.Some(t.TempDir()))) // Create a block on keys[0]'s lane but sign it with keys[1]. - lane := keys[0].Public() + lane := types.NewLaneID(keys[0].Public(), 0) block := types.NewBlock(lane, 0, types.GenBlockHeaderHash(rng), types.GenPayload(rng)) prop := types.Sign(keys[1], types.NewLaneProposal(block)) @@ -520,7 +540,7 @@ func TestNewStateWithPersistence(t *testing.T) { t.Run("loads persisted blocks", func(t *testing.T) { dir := t.TempDir() ds := newTestDataState(&data.Config{Registry: registry}) - lane := keys[0].Public() + lane := types.NewLaneID(keys[0].Public(), 0) // Persist blocks using BlockPersister. bp, _, err := persist.NewBlockPersister(utils.Some(dir)) @@ -531,7 +551,7 @@ func TestNewStateWithPersistence(t *testing.T) { block := types.NewBlock(lane, n, parent, types.GenPayload(rng)) signed := types.Sign(keys[0], types.NewLaneProposal(block)) parent = block.Header().Hash() - require.NoError(t, bp.MaybePruneAndPersistLane(lane, utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{signed}, noBlockCB)) + require.NoError(t, bp.MaybePruneAndPersistLane(lane, utils.OrPanic1(types.NewCommittee(map[types.PublicKey]uint64{lane.Validator(): 1})), utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{signed}, noBlockCB)) } // Release the seeding persister's WAL locks before NewState opens the same directory. @@ -547,7 +567,7 @@ func TestNewStateWithPersistence(t *testing.T) { t.Run("loads persisted AppQC and blocks together", func(t *testing.T) { dir := t.TempDir() ds := newTestDataState(&data.Config{Registry: registry}) - lane := keys[0].Public() + lane := types.NewLaneID(keys[0].Public(), 0) roadIdx := types.RoadIndex(2) globalNum := types.GlobalBlockNumber(5) @@ -583,7 +603,7 @@ func TestNewStateWithPersistence(t *testing.T) { block := types.NewBlock(lane, n, parent, types.GenPayload(rng)) signed := types.Sign(keys[0], types.NewLaneProposal(block)) parent = block.Header().Hash() - require.NoError(t, bp.MaybePruneAndPersistLane(lane, utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{signed}, noBlockCB)) + require.NoError(t, bp.MaybePruneAndPersistLane(lane, utils.OrPanic1(types.NewCommittee(map[types.PublicKey]uint64{lane.Validator(): 1})), utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{signed}, noBlockCB)) } // Release the seeding persisters' WAL locks before NewState opens the same directory. @@ -751,7 +771,7 @@ func TestNewStateWithPersistence(t *testing.T) { t.Run("anchor past all persisted blocks truncates lane WAL", func(t *testing.T) { dir := t.TempDir() ds := newTestDataState(&data.Config{Registry: registry}) - lane := keys[0].Public() + lane := types.NewLaneID(keys[0].Public(), 0) // Persist commitQCs 0-9 and blocks 0-2 for one lane. qcs := make([]*types.CommitQC, 10) @@ -772,7 +792,7 @@ func TestNewStateWithPersistence(t *testing.T) { block := types.NewBlock(lane, n, parent, types.GenPayload(rng)) signed := types.Sign(keys[0], types.NewLaneProposal(block)) parent = block.Header().Hash() - require.NoError(t, bp.MaybePruneAndPersistLane(lane, utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{signed}, noBlockCB)) + require.NoError(t, bp.MaybePruneAndPersistLane(lane, utils.OrPanic1(types.NewCommittee(map[types.PublicKey]uint64{lane.Validator(): 1})), utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{signed}, noBlockCB)) } // Persist a prune anchor at index 9 with a laneRange that starts past @@ -823,7 +843,7 @@ func TestNewStateWithPersistence(t *testing.T) { t.Run("failed NewState releases WAL locks", func(t *testing.T) { dir := t.TempDir() ds := newTestDataState(&data.Config{Registry: registry}) - lane := keys[0].Public() + lane := types.NewLaneID(keys[0].Public(), 0) // Seed one lane so the failing NewState below has a lane WAL to leak, then release the seeder. bp, _, err := persist.NewBlockPersister(utils.Some(dir)) @@ -831,8 +851,9 @@ func TestNewStateWithPersistence(t *testing.T) { var parent types.BlockHeaderHash block := types.NewBlock(lane, 0, parent, types.GenPayload(rng)) proposals := []*types.Signed[*types.LaneProposal]{types.Sign(keys[0], types.NewLaneProposal(block))} + active := utils.OrPanic1(types.NewCommittee(map[types.PublicKey]uint64{keys[0].Public(): 1})) require.NoError(t, bp.MaybePruneAndPersistLane( - lane, utils.None[*types.CommitQC](), proposals, noBlockCB)) + lane, active, utils.None[*types.CommitQC](), proposals, noBlockCB)) require.NoError(t, bp.Close()) // A prune anchor missing its CommitQC unmarshals as proto but fails PruneAnchorConv.Decode, so diff --git a/sei-tendermint/internal/autobahn/avail/subscriptions.go b/sei-tendermint/internal/autobahn/avail/subscriptions.go index 76c6634409..24436ba86f 100644 --- a/sei-tendermint/internal/autobahn/avail/subscriptions.go +++ b/sei-tendermint/internal/autobahn/avail/subscriptions.go @@ -8,8 +8,17 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" ) -func (s *State) SubscribeLaneProposals(first types.BlockNumber) *LaneProposalsRecv { - return &LaneProposalsRecv{s, s.key.Public(), first} +// SubscribeLaneProposals binds LocalLane at subscribe time. After leave, serves +// the leave map until tipEpoch prune → ErrLanePruned; rejoin needs a new Subscribe. +// +// Back-leash (AppQC in prior epoch before ActivateEpoch) means tipEpoch prune +// drops the leave map before rejoin, so Recv ends before LocalLane is Some(new). +func (s *State) SubscribeLaneProposals(first types.BlockNumber) (*LaneProposalsRecv, error) { + lane, ok := s.LocalLane().Get() + if !ok { + return nil, ErrBadLane + } + return &LaneProposalsRecv{s, lane, first}, nil } type LaneProposalsRecv struct { @@ -26,6 +35,10 @@ func (r *LaneProposalsRecv) Recv(ctx context.Context) (*types.Signed[*types.Lane r.next += 1 continue } + if errors.Is(err, ErrBadLane) { + // TipEpoch pruned leave map (or DeleteLane race). + return nil, ErrLanePruned + } return nil, fmt.Errorf("x.avail.Block(): %w", err) } r.next += 1 diff --git a/sei-tendermint/internal/autobahn/avail/subscriptions_test.go b/sei-tendermint/internal/autobahn/avail/subscriptions_test.go new file mode 100644 index 0000000000..303631fb8b --- /dev/null +++ b/sei-tendermint/internal/autobahn/avail/subscriptions_test.go @@ -0,0 +1,49 @@ +package avail + +import ( + "testing" + "time" + + "github.com/sei-protocol/sei-chain/sei-db/ledger_db/block/memblock" + "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/data" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/epoch" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" +) + +func TestSubscribeLaneProposals_ErrLanePrunedAfterMapDrop(t *testing.T) { + rng := utils.TestRng() + registry, keys := epoch.GenRegistry(rng, 2) + a, b := keys[0], keys[1] + db := memblock.NewBlockDB() + t.Cleanup(func() { require.NoError(t, db.Close()) }) + ds := utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, db)) + state := utils.OrPanic1(NewState(a, ds, utils.None[string]())) + + lane0 := state.LocalLane().OrPanic("genesis") + want, err := state.ProduceLocalBlock(lane0, 0, types.GenPayload(rng)) + require.NoError(t, err) + sub, err := state.SubscribeLaneProposals(0) + require.NoError(t, err) + + ep, err := registry.ActivateEpoch( + map[types.PublicKey]uint64{b.Public(): 1}, + types.OpenRoadRange(), time.Time{}, registry.FirstBlock(), + ) + require.NoError(t, err) + state.ApplyEpoch(ep) + _, err = state.SubscribeLaneProposals(0) + require.ErrorIs(t, err, ErrBadLane) + + got, err := sub.Recv(t.Context()) + require.NoError(t, err) + require.Equal(t, want.Msg().Block().Header().Hash(), got.Msg().Block().Header().Hash()) + + for inner, ctrl := range state.inner.Lock() { + inner.dropLanes([]types.LaneID{lane0}) + ctrl.Updated() + } + _, err = sub.Recv(t.Context()) + require.ErrorIs(t, err, ErrLanePruned) +} diff --git a/sei-tendermint/internal/autobahn/avail/testonly.go b/sei-tendermint/internal/autobahn/avail/testonly.go index aee8eead1d..6f8f446f8b 100644 --- a/sei-tendermint/internal/autobahn/avail/testonly.go +++ b/sei-tendermint/internal/autobahn/avail/testonly.go @@ -13,7 +13,10 @@ func RunTestNetwork(ctx context.Context, states []*State) error { for _, from := range states { for _, to := range states { s.Spawn(func() error { - sub := from.SubscribeLaneProposals(0) + sub, err := from.SubscribeLaneProposals(0) + if err != nil { + return err + } for { p, err := sub.Recv(ctx) if err != nil { diff --git a/sei-tendermint/internal/autobahn/consensus/persist/blocks.go b/sei-tendermint/internal/autobahn/consensus/persist/blocks.go index 5500e2b410..bf1f499bb9 100644 --- a/sei-tendermint/internal/autobahn/consensus/persist/blocks.go +++ b/sei-tendermint/internal/autobahn/consensus/persist/blocks.go @@ -173,18 +173,14 @@ func (lw *laneWAL) close() error { // MaybePruneAndPersistLane holds the per-lane lock for the entire // truncate-then-append sequence, so concurrent calls on the same lane // serialize correctly. Different lanes are fully parallel. -// -// NOTE: MaybePruneAndPersistLane releases the map RLock before acquiring -// the per-lane lock. This is safe because lanes are only added, never -// removed. If lane deletion is added in the future, the map RLock must be -// held through the WAL write. +// Lanes may be removed via DeleteLane once tipEpoch omits them. type BlockPersister struct { dir utils.Option[string] // immutable after construction lanes utils.RWMutex[map[types.LaneID]*laneWAL] } func laneDir(lane types.LaneID) string { - return hex.EncodeToString(lane.Bytes()) + return lane.HexString() } func newLaneWALState(dir string) (*laneWALState, error) { @@ -235,9 +231,9 @@ func NewBlockPersister(stateDir utils.Option[string]) (*BlockPersister, map[type logger.Warn("skipping unexpected entry in blocks dir", "name", e.Name()) continue } - lane, err := types.PublicKeyFromBytes(laneBytes) + lane, err := types.LaneIDFromBytes(laneBytes) if err != nil { - logger.Warn("skipping lane dir with invalid key", "name", e.Name(), "err", err) + logger.Warn("skipping lane dir with invalid LaneID", "name", e.Name(), "err", err) continue } lanePath := filepath.Join(dir, e.Name()) @@ -261,35 +257,34 @@ func NewBlockPersister(stateDir utils.Option[string]) (*BlockPersister, map[type return bp, allBlocks, nil } -// getOrCreateLane returns the laneWAL for the given lane, creating it if -// necessary. Uses double-checked locking: fast path reads under RLock; -// slow path (lane creation) promotes to a write Lock. -// The returned pointer is safe to use after the lock is released because -// lanes are only ever added, never removed (see BlockPersister doc). -// Returns an error if called on a no-op persister (caller should check first). -func (bp *BlockPersister) getOrCreateLane(lane types.LaneID) (*laneWAL, error) { - dir, ok := bp.dir.Get() - if !ok { - return nil, fmt.Errorf("getOrCreateLane called on no-op persister") +// getLane returns (or optionally creates) the lane WAL. allowCreate=false after +// DeleteLane so truncate-only no-ops. Do not DeleteLane while using a pointer from here. +func (bp *BlockPersister) getLane(lane types.LaneID, allowCreate bool) (lw *laneWAL, ok bool, err error) { + dir, hasDir := bp.dir.Get() + if !hasDir { + return nil, false, fmt.Errorf("getLane called on no-op persister") } - // Fast path: read-only check. for lanes := range bp.lanes.RLock() { + // Fast path: read-only check. if lw, ok := lanes[lane]; ok { - return lw, nil + return lw, true, nil } } - // Slow path: create under write lock (double-checked). + if !allowCreate { + return nil, false, nil + } for lanes := range bp.lanes.Lock() { + // Slow path: create under write lock (double-checked). if lw, ok := lanes[lane]; ok { - return lw, nil + return lw, true, nil } s, err := newLaneWALState(filepath.Join(dir, laneDir(lane))) if err != nil { - return nil, fmt.Errorf("create lane WAL for %s: %w", lane, err) + return nil, false, fmt.Errorf("create lane WAL for %s: %w", lane, err) } lw := &laneWAL{state: utils.NewMutex(s)} lanes[lane] = lw - return lw, nil + return lw, true, nil } panic("unreachable") } @@ -302,6 +297,10 @@ func (bp *BlockPersister) getOrCreateLane(lane types.LaneID) (*laneWAL, error) { // - anchor empty, proposals non-empty: append only, no truncation. // - anchor empty, proposals empty: no-op. // +// active: open WALs for HasLane; leavers flush if already open. Non-empty +// proposals still allowCreate so a leave before first open flushes tips +// (post-DeleteLane batches omit the lane, so prune does not recreate). +// // afterEach, when present, is called once per appended proposal in order, after the whole batch has // been flushed — never before, because an append is not durable until then and afterEach is what // releases a block to the rest of consensus. It is invoked while the per-lane lock is held, so it must @@ -313,6 +312,7 @@ func (bp *BlockPersister) getOrCreateLane(lane types.LaneID) (*laneWAL, error) { // so concurrent calls on the same lane serialize correctly. func (bp *BlockPersister) MaybePruneAndPersistLane( lane types.LaneID, + active *types.Committee, anchor utils.Option[*types.CommitQC], proposals []*types.Signed[*types.LaneProposal], afterEach utils.Option[func(*types.Signed[*types.LaneProposal])], @@ -326,19 +326,52 @@ func (bp *BlockPersister) MaybePruneAndPersistLane( return nil } - lw, err := bp.getOrCreateLane(lane) + allowCreate := active.HasLane(lane) || len(proposals) > 0 + lw, ok, err := bp.getLane(lane, allowCreate) if err != nil { return err } + if !ok { + return nil + } return lw.maybePruneAndPersist(lane, anchor, proposals, afterEach) } +// NOTE: MaybePruneAndPersistLane releases the map RLock before acquiring +// the per-lane lock. DeleteLane must not overlap an in-flight +// MaybePruneAndPersistLane on the same lane. Avail calls DeleteLane after +// runPersist's Parallel batch returns for tip-stale leave maps. +// +// No-op if the lane WAL is not open (never created, or already deleted). +func (bp *BlockPersister) DeleteLane(lane types.LaneID) error { + dir, ok := bp.dir.Get() + if !ok { + return nil + } + for lanes := range bp.lanes.Lock() { + lw, ok := lanes[lane] + if !ok { + return nil + } + delete(lanes, lane) + if err := lw.close(); err != nil { + return fmt.Errorf("close lane %s WAL: %w", lane, err) + } + path := filepath.Join(dir, laneDir(lane)) + if err := os.RemoveAll(path); err != nil { + return fmt.Errorf("remove lane dir %s: %w", path, err) + } + logger.Info("deleted inactive lane WAL", "lane", lane.String()) + return nil + } + panic("unreachable") +} + // Close shuts down all per-lane WALs, releasing the exclusive lock each one holds on its directory. // // Production does not call this: a node exits by rugpull and the OS reclaims everything. It exists so // that a process which opens the same state directory more than once in its lifetime — a test // simulating a restart — can release the first owner before the second opens. -// // Safe for concurrent use. func (bp *BlockPersister) Close() error { if _, ok := bp.dir.Get(); !ok { diff --git a/sei-tendermint/internal/autobahn/consensus/persist/blocks_test.go b/sei-tendermint/internal/autobahn/consensus/persist/blocks_test.go index 8c52225f6e..4414659e33 100644 --- a/sei-tendermint/internal/autobahn/consensus/persist/blocks_test.go +++ b/sei-tendermint/internal/autobahn/consensus/persist/blocks_test.go @@ -12,11 +12,15 @@ import ( ) func testSignedProposal(rng utils.Rng, key types.SecretKey, n types.BlockNumber) *types.Signed[*types.LaneProposal] { - lane := key.Public() + lane := types.NewLaneID(key.Public(), 0) block := types.NewBlock(lane, n, types.GenBlockHeaderHash(rng), types.GenPayload(rng)) return types.Sign(key, types.NewLaneProposal(block)) } +func committeeForLane(lane types.LaneID) *types.Committee { + return utils.OrPanic1(types.NewCommittee(map[types.PublicKey]uint64{lane.Validator(): 1})) +} + var noBlockCB = utils.None[func(*types.Signed[*types.LaneProposal])]() // liveBlocks drops blocks the prune anchor has moved past, mirroring the filter loadPersistedState @@ -33,8 +37,10 @@ func liveBlocks(loaded []LoadedBlock, first types.BlockNumber) []LoadedBlock { func testPersistBlock(t *testing.T, bp *BlockPersister, p *types.Signed[*types.LaneProposal]) { t.Helper() + lane := p.Msg().Block().Header().Lane() require.NoError(t, bp.MaybePruneAndPersistLane( - p.Msg().Block().Header().Lane(), + lane, + committeeForLane(lane), utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{p}, noBlockCB, @@ -81,7 +87,7 @@ func TestPersistBlockAndLoad(t *testing.T) { dir := t.TempDir() key := types.GenSecretKey(rng) - lane := key.Public() + lane := types.NewLaneID(key.Public(), 0) bp, _, err := NewBlockPersister(utils.Some(dir)) require.NoError(t, err) @@ -109,8 +115,8 @@ func TestPersistBlockMultipleLanes(t *testing.T) { key1 := types.GenSecretKey(rng) key2 := types.GenSecretKey(rng) - lane1 := key1.Public() - lane2 := key2.Public() + lane1 := types.NewLaneID(key1.Public(), 0) + lane2 := types.NewLaneID(key2.Public(), 0) bp, _, err := NewBlockPersister(utils.Some(dir)) require.NoError(t, err) @@ -134,7 +140,7 @@ func TestDeleteBeforeRemovesOldKeepsNew(t *testing.T) { dir := t.TempDir() key := types.GenSecretKey(rng) - lane := key.Public() + lane := types.NewLaneID(key.Public(), 0) bp, _, err := NewBlockPersister(utils.Some(dir)) require.NoError(t, err) @@ -160,9 +166,9 @@ func TestDeleteBeforeAndRestart(t *testing.T) { key1 := types.GenSecretKey(rng) key2 := types.GenSecretKey(rng) key3 := types.GenSecretKey(rng) - lane1 := key1.Public() - lane2 := key2.Public() - lane3 := key3.Public() // never persisted — exercises the "no WAL yet" path + lane1 := types.NewLaneID(key1.Public(), 0) + lane2 := types.NewLaneID(key2.Public(), 0) + lane3 := types.NewLaneID(key3.Public(), 0) // never persisted — exercises the "no WAL yet" path bp, _, err := NewBlockPersister(utils.Some(dir)) require.NoError(t, err) @@ -206,7 +212,7 @@ func TestNoOpBlockPersister(t *testing.T) { rng := utils.TestRng() key := types.GenSecretKey(rng) - lane := key.Public() + lane := types.NewLaneID(key.Public(), 0) proposals := make([]*types.Signed[*types.LaneProposal], 5) for i := range proposals { @@ -217,11 +223,11 @@ func TestNoOpBlockPersister(t *testing.T) { // Verify afterEach is still invoked for every proposal. var called int cb := utils.Some(func(_ *types.Signed[*types.LaneProposal]) { called++ }) - require.NoError(t, bp.MaybePruneAndPersistLane(lane, utils.None[*types.CommitQC](), proposals[:3], cb)) + require.NoError(t, bp.MaybePruneAndPersistLane(lane, committeeForLane(lane), utils.None[*types.CommitQC](), proposals[:3], cb)) require.Equal(t, 3, called) called = 0 - require.NoError(t, bp.MaybePruneAndPersistLane(lane, utils.None[*types.CommitQC](), proposals[3:], cb)) + require.NoError(t, bp.MaybePruneAndPersistLane(lane, committeeForLane(lane), utils.None[*types.CommitQC](), proposals[3:], cb)) require.Equal(t, 2, called) require.NoError(t, bp.Close()) @@ -232,7 +238,7 @@ func TestDeleteBeforeThenPersistMore(t *testing.T) { dir := t.TempDir() key := types.GenSecretKey(rng) - lane := key.Public() + lane := types.NewLaneID(key.Public(), 0) bp, _, err := NewBlockPersister(utils.Some(dir)) require.NoError(t, err) @@ -258,7 +264,7 @@ func TestDeleteBeforePastAllBlocks(t *testing.T) { dir := t.TempDir() key := types.GenSecretKey(rng) - lane := key.Public() + lane := types.NewLaneID(key.Public(), 0) bp, _, err := NewBlockPersister(utils.Some(dir)) require.NoError(t, err) @@ -287,7 +293,7 @@ func TestDeleteBeforePastAllRejectsStaleBlock(t *testing.T) { dir := t.TempDir() key := types.GenSecretKey(rng) - lane := key.Public() + lane := types.NewLaneID(key.Public(), 0) bp, _, err := NewBlockPersister(utils.Some(dir)) require.NoError(t, err) @@ -300,7 +306,7 @@ func TestDeleteBeforePastAllRejectsStaleBlock(t *testing.T) { // Writing a stale block number (0) should be rejected. stale := testSignedProposal(rng, key, 0) - err = bp.MaybePruneAndPersistLane(lane, utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{stale}, noBlockCB) + err = bp.MaybePruneAndPersistLane(lane, committeeForLane(lane), utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{stale}, noBlockCB) require.Error(t, err) require.Contains(t, err.Error(), "out of sequence") @@ -314,7 +320,7 @@ func TestTruncateOnEmptyWALAdvancesCursor(t *testing.T) { dir := t.TempDir() key := types.GenSecretKey(rng) - lane := key.Public() + lane := types.NewLaneID(key.Public(), 0) bp, _, err := NewBlockPersister(utils.Some(dir)) require.NoError(t, err) @@ -339,7 +345,7 @@ func TestEmptyLaneWALSurvivesReopen(t *testing.T) { dir := t.TempDir() key := types.GenSecretKey(rng) - lane := key.Public() + lane := types.NewLaneID(key.Public(), 0) // Simulate a crash after lazy lane directory creation but before any write: // create the lane subdirectory so NewBlockPersister discovers it on open. @@ -399,18 +405,18 @@ func TestPersistBlockOutOfSequence(t *testing.T) { bp, _, err := NewBlockPersister(utils.Some(dir)) require.NoError(t, err) - lane := key.Public() + lane := types.NewLaneID(key.Public(), 0) testPersistBlock(t, bp, testSignedProposal(rng, key, 0)) // Gap: skip block 1, try block 2. gap := testSignedProposal(rng, key, 2) - err = bp.MaybePruneAndPersistLane(lane, utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{gap}, noBlockCB) + err = bp.MaybePruneAndPersistLane(lane, committeeForLane(lane), utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{gap}, noBlockCB) require.Error(t, err) require.Contains(t, err.Error(), "out of sequence") // Duplicate: try block 0 again. dup := testSignedProposal(rng, key, 0) - err = bp.MaybePruneAndPersistLane(lane, utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{dup}, noBlockCB) + err = bp.MaybePruneAndPersistLane(lane, committeeForLane(lane), utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{dup}, noBlockCB) require.Error(t, err) require.Contains(t, err.Error(), "out of sequence") @@ -424,7 +430,7 @@ func TestLoadAllDropsBlocksBehindGap(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() key := types.GenSecretKey(rng) - lane := key.Public() + lane := types.NewLaneID(key.Public(), 0) // Write straight to a lane WAL, bypassing the contiguity check, to lay down blocks 0 and 2 with // no block 1 between them. @@ -453,7 +459,7 @@ func TestPersistBlockAutoCreatesLane(t *testing.T) { require.Equal(t, 0, len(entries)) key := types.GenSecretKey(rng) - lane := key.Public() + lane := types.NewLaneID(key.Public(), 0) testPersistBlock(t, bp, testSignedProposal(rng, key, 0)) entries, _ = os.ReadDir(filepath.Join(dir, blocksDir)) @@ -474,7 +480,7 @@ func TestPersistBlockAutoCreatesLane(t *testing.T) { func TestPruneReclaimsSealedFiles(t *testing.T) { rng := utils.TestRng() key := types.GenSecretKey(rng) - lane := key.Public() + lane := types.NewLaneID(key.Public(), 0) dir := t.TempDir() const total = 40 @@ -513,7 +519,7 @@ func TestPersistBlockInvokesAfterEachOncePerBlock(t *testing.T) { dir := t.TempDir() key := types.GenSecretKey(rng) - lane := key.Public() + lane := types.NewLaneID(key.Public(), 0) bp, _, err := NewBlockPersister(utils.Some(dir)) require.NoError(t, err) @@ -526,7 +532,7 @@ func TestPersistBlockInvokesAfterEachOncePerBlock(t *testing.T) { cb := utils.Some(func(p *types.Signed[*types.LaneProposal]) { seen = append(seen, p.Msg().Block().Header().BlockNumber()) }) - require.NoError(t, bp.MaybePruneAndPersistLane(lane, utils.None[*types.CommitQC](), proposals, cb)) + require.NoError(t, bp.MaybePruneAndPersistLane(lane, committeeForLane(lane), utils.None[*types.CommitQC](), proposals, cb)) require.NoError(t, bp.Close()) require.Equal(t, len(proposals), len(seen)) @@ -560,9 +566,9 @@ func TestPersistBlockConcurrentDistinctLanes(t *testing.T) { require.NoError(t, scope.Parallel(func(ps scope.ParallelScope) error { for i := range numLanes { - lane := keys[i].Public() + lane := types.NewLaneID(keys[i].Public(), 0) ps.Spawn(func() error { - return bp.MaybePruneAndPersistLane(lane, utils.None[*types.CommitQC](), proposals[i], noBlockCB) + return bp.MaybePruneAndPersistLane(lane, committeeForLane(lane), utils.None[*types.CommitQC](), proposals[i], noBlockCB) }) } return nil @@ -574,10 +580,82 @@ func TestPersistBlockConcurrentDistinctLanes(t *testing.T) { require.NoError(t, err) require.Equal(t, numLanes, len(blocks)) for i := range numLanes { - lane := keys[i].Public() + lane := types.NewLaneID(keys[i].Public(), 0) require.Equal(t, blocksPerLane, len(blocks[lane])) for j := range blocksPerLane { require.Equal(t, types.BlockNumber(j), blocks[lane][j].Number) } } } + +// After DeleteLane, truncate-only persist (empty proposals) must not recreate +// the WAL. Production runPersist never includes pruned lanes in a block batch. +func TestMaybePruneAndPersistLane_InactiveDoesNotRecreateAfterDelete(t *testing.T) { + rng := utils.TestRng() + dir := t.TempDir() + bp, _, err := NewBlockPersister(utils.Some(dir)) + require.NoError(t, err) + t.Cleanup(func() { _ = bp.Close() }) + + leaver := types.GenSecretKey(rng) + stayer := types.GenSecretKey(rng) + lane := types.NewLaneID(leaver.Public(), 0) + proposal := types.Sign(leaver, types.NewLaneProposal( + types.NewBlock(lane, 0, types.BlockHeaderHash{}, types.GenPayload(rng)), + )) + + require.NoError(t, bp.MaybePruneAndPersistLane( + lane, + committeeForLane(lane), + utils.None[*types.CommitQC](), + []*types.Signed[*types.LaneProposal]{proposal}, + noBlockCB, + )) + lanePath := filepath.Join(dir, blocksDir, laneDir(lane)) + require.NoError(t, bp.DeleteLane(lane)) + _, err = os.Stat(lanePath) + require.True(t, os.IsNotExist(err)) + require.NoError(t, bp.DeleteLane(lane)) // idempotent + + active := utils.OrPanic1(types.NewCommittee(map[types.PublicKey]uint64{stayer.Public(): 1})) + require.False(t, active.HasLane(lane)) + require.NoError(t, bp.MaybePruneAndPersistLane( + lane, + active, + utils.None[*types.CommitQC](), + nil, + noBlockCB, + )) + _, err = os.Stat(lanePath) + require.True(t, os.IsNotExist(err)) +} + +// Leave before the first WAL open still flushes in-memory tips (allowCreate +// when proposals are non-empty even if the lane is inactive). +func TestMaybePruneAndPersistLane_InactiveWithProposalsCreatesWAL(t *testing.T) { + rng := utils.TestRng() + dir := t.TempDir() + bp, _, err := NewBlockPersister(utils.Some(dir)) + require.NoError(t, err) + t.Cleanup(func() { _ = bp.Close() }) + + leaver := types.GenSecretKey(rng) + stayer := types.GenSecretKey(rng) + lane := types.NewLaneID(leaver.Public(), 0) + proposal := types.Sign(leaver, types.NewLaneProposal( + types.NewBlock(lane, 0, types.BlockHeaderHash{}, types.GenPayload(rng)), + )) + + active := utils.OrPanic1(types.NewCommittee(map[types.PublicKey]uint64{stayer.Public(): 1})) + require.False(t, active.HasLane(lane)) + require.NoError(t, bp.MaybePruneAndPersistLane( + lane, + active, + utils.None[*types.CommitQC](), + []*types.Signed[*types.LaneProposal]{proposal}, + noBlockCB, + )) + lanePath := filepath.Join(dir, blocksDir, laneDir(lane)) + _, err = os.Stat(lanePath) + require.NoError(t, err) +} diff --git a/sei-tendermint/internal/autobahn/epoch/registry.go b/sei-tendermint/internal/autobahn/epoch/registry.go index 75191764d1..beebfb38aa 100644 --- a/sei-tendermint/internal/autobahn/epoch/registry.go +++ b/sei-tendermint/internal/autobahn/epoch/registry.go @@ -1,6 +1,7 @@ package epoch import ( + "fmt" "time" "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" @@ -15,7 +16,7 @@ type registryState struct { // Registry is the authoritative source of epoch and committee information. // All layers (consensus, data, avail) read from it. type Registry struct { - state utils.RWMutex[registryState] + state utils.RWMutex[*registryState] } // NewRegistry creates a Registry with the genesis committee. @@ -26,7 +27,7 @@ func NewRegistry( ) (*Registry, error) { ep := types.NewEpoch(0, types.OpenRoadRange(), genesisTimestamp, committee, firstBlock) return &Registry{ - state: utils.NewRWMutex(registryState{ + state: utils.NewRWMutex(®istryState{ m: map[types.EpochIndex]*types.Epoch{0: ep}, latest: 0, }), @@ -67,6 +68,34 @@ func (r *Registry) LatestEpoch() *types.Epoch { panic("unreachable") } +// ActivateEpoch appends latest+1 via ActivateCommittee. +// +// Scaffolding for #3736: does not validate roads.First vs prior RoadRange; prior +// range left as stored. Tests may pass OpenRoadRange() until multi-epoch roads wire up. +func (r *Registry) ActivateEpoch( + weights map[types.PublicKey]uint64, + roads types.RoadRange, + firstTimestamp time.Time, + firstBlock types.GlobalBlockNumber, +) (*types.Epoch, error) { + for s := range r.state.Lock() { + prev := s.m[s.latest] + next := s.latest + 1 + if _, exists := s.m[next]; exists { + return nil, fmt.Errorf("epoch %d already exists", next) + } + committee, err := types.ActivateCommittee(prev.Committee(), weights, next) + if err != nil { + return nil, err + } + ep := types.NewEpoch(next, roads, firstTimestamp, committee, firstBlock) + s.m[next] = ep + s.latest = next + return ep, nil + } + panic("unreachable") +} + // VerifyInWindow calls fn against the latest epoch's committee and returns it if accepted. // Returns a slice of all matching epochs so callers can skip re-verification for any // epoch already checked here. diff --git a/sei-tendermint/internal/autobahn/pb/autobahn.hashable.go b/sei-tendermint/internal/autobahn/pb/autobahn.hashable.go index 73b708c7f6..f876857b42 100644 --- a/sei-tendermint/internal/autobahn/pb/autobahn.hashable.go +++ b/sei-tendermint/internal/autobahn/pb/autobahn.hashable.go @@ -4,6 +4,7 @@ package pb func (*Timestamp) IsHashable() {} func (*Duration) IsHashable() {} func (*PublicKey) IsHashable() {} +func (*LaneID) IsHashable() {} func (*Signature) IsHashable() {} func (*BlockHeader) IsHashable() {} func (*Payload) IsHashable() {} diff --git a/sei-tendermint/internal/autobahn/pb/autobahn.pb.go b/sei-tendermint/internal/autobahn/pb/autobahn.pb.go index 1348b18d6b..5605608386 100644 --- a/sei-tendermint/internal/autobahn/pb/autobahn.pb.go +++ b/sei-tendermint/internal/autobahn/pb/autobahn.pb.go @@ -421,6 +421,60 @@ func (x *PublicKey) GetEd25519() []byte { return nil } +// LaneID identifies a validator's lane for a continuous committee membership +// streak. e_join is the epoch in which the validator most recently joined. +type LaneID struct { + state protoimpl.MessageState `protogen:"open.v1"` + Validator *PublicKey `protobuf:"bytes,1,opt,name=validator,proto3,oneof" json:"validator,omitempty"` // required + EJoin *uint64 `protobuf:"varint,2,opt,name=e_join,json=eJoin,proto3,oneof" json:"e_join,omitempty"` // required + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LaneID) Reset() { + *x = LaneID{} + mi := &file_autobahn_autobahn_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LaneID) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LaneID) ProtoMessage() {} + +func (x *LaneID) ProtoReflect() protoreflect.Message { + mi := &file_autobahn_autobahn_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LaneID.ProtoReflect.Descriptor instead. +func (*LaneID) Descriptor() ([]byte, []int) { + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{7} +} + +func (x *LaneID) GetValidator() *PublicKey { + if x != nil { + return x.Validator + } + return nil +} + +func (x *LaneID) GetEJoin() uint64 { + if x != nil && x.EJoin != nil { + return *x.EJoin + } + return 0 +} + type Signature struct { state protoimpl.MessageState `protogen:"open.v1"` Key *PublicKey `protobuf:"bytes,1,opt,name=key,proto3,oneof" json:"key,omitempty"` // required @@ -431,7 +485,7 @@ type Signature struct { func (x *Signature) Reset() { *x = Signature{} - mi := &file_autobahn_autobahn_proto_msgTypes[7] + mi := &file_autobahn_autobahn_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -443,7 +497,7 @@ func (x *Signature) String() string { func (*Signature) ProtoMessage() {} func (x *Signature) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[7] + mi := &file_autobahn_autobahn_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -456,7 +510,7 @@ func (x *Signature) ProtoReflect() protoreflect.Message { // Deprecated: Use Signature.ProtoReflect.Descriptor instead. func (*Signature) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{7} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{8} } func (x *Signature) GetKey() *PublicKey { @@ -475,17 +529,17 @@ func (x *Signature) GetSig() []byte { type BlockHeader struct { state protoimpl.MessageState `protogen:"open.v1"` - Lane *PublicKey `protobuf:"bytes,1,opt,name=lane,proto3,oneof" json:"lane,omitempty"` // required BlockNumber *uint64 `protobuf:"varint,2,opt,name=block_number,json=blockNumber,proto3,oneof" json:"block_number,omitempty"` // required ParentHash []byte `protobuf:"bytes,3,opt,name=parent_hash,json=parentHash,proto3,oneof" json:"parent_hash,omitempty"` // required PayloadHash []byte `protobuf:"bytes,4,opt,name=payload_hash,json=payloadHash,proto3,oneof" json:"payload_hash,omitempty"` // required + LaneId *LaneID `protobuf:"bytes,5,opt,name=lane_id,json=laneId,proto3,oneof" json:"lane_id,omitempty"` // required unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BlockHeader) Reset() { *x = BlockHeader{} - mi := &file_autobahn_autobahn_proto_msgTypes[8] + mi := &file_autobahn_autobahn_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -497,7 +551,7 @@ func (x *BlockHeader) String() string { func (*BlockHeader) ProtoMessage() {} func (x *BlockHeader) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[8] + mi := &file_autobahn_autobahn_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -510,14 +564,7 @@ func (x *BlockHeader) ProtoReflect() protoreflect.Message { // Deprecated: Use BlockHeader.ProtoReflect.Descriptor instead. func (*BlockHeader) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{8} -} - -func (x *BlockHeader) GetLane() *PublicKey { - if x != nil { - return x.Lane - } - return nil + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{9} } func (x *BlockHeader) GetBlockNumber() uint64 { @@ -541,6 +588,13 @@ func (x *BlockHeader) GetPayloadHash() []byte { return nil } +func (x *BlockHeader) GetLaneId() *LaneID { + if x != nil { + return x.LaneId + } + return nil +} + type Payload struct { state protoimpl.MessageState `protogen:"open.v1"` CreatedAt *Timestamp `protobuf:"bytes,1,opt,name=created_at,json=createdAt,proto3,oneof" json:"created_at,omitempty"` // required @@ -553,7 +607,7 @@ type Payload struct { func (x *Payload) Reset() { *x = Payload{} - mi := &file_autobahn_autobahn_proto_msgTypes[9] + mi := &file_autobahn_autobahn_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -565,7 +619,7 @@ func (x *Payload) String() string { func (*Payload) ProtoMessage() {} func (x *Payload) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[9] + mi := &file_autobahn_autobahn_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -578,7 +632,7 @@ func (x *Payload) ProtoReflect() protoreflect.Message { // Deprecated: Use Payload.ProtoReflect.Descriptor instead. func (*Payload) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{9} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{10} } func (x *Payload) GetCreatedAt() *Timestamp { @@ -619,7 +673,7 @@ type Block struct { func (x *Block) Reset() { *x = Block{} - mi := &file_autobahn_autobahn_proto_msgTypes[10] + mi := &file_autobahn_autobahn_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -631,7 +685,7 @@ func (x *Block) String() string { func (*Block) ProtoMessage() {} func (x *Block) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[10] + mi := &file_autobahn_autobahn_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -644,7 +698,7 @@ func (x *Block) ProtoReflect() protoreflect.Message { // Deprecated: Use Block.ProtoReflect.Descriptor instead. func (*Block) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{10} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{11} } func (x *Block) GetHeader() *BlockHeader { @@ -671,7 +725,7 @@ type LaneQC struct { func (x *LaneQC) Reset() { *x = LaneQC{} - mi := &file_autobahn_autobahn_proto_msgTypes[11] + mi := &file_autobahn_autobahn_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -683,7 +737,7 @@ func (x *LaneQC) String() string { func (*LaneQC) ProtoMessage() {} func (x *LaneQC) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[11] + mi := &file_autobahn_autobahn_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -696,7 +750,7 @@ func (x *LaneQC) ProtoReflect() protoreflect.Message { // Deprecated: Use LaneQC.ProtoReflect.Descriptor instead. func (*LaneQC) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{11} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{12} } func (x *LaneQC) GetVote() *BlockHeader { @@ -715,17 +769,17 @@ func (x *LaneQC) GetSigs() []*Signature { type LaneRange struct { state protoimpl.MessageState `protogen:"open.v1"` - Lane *PublicKey `protobuf:"bytes,1,opt,name=lane,proto3,oneof" json:"lane,omitempty"` // required First *uint64 `protobuf:"varint,2,opt,name=first,proto3,oneof" json:"first,omitempty"` // required Next *uint64 `protobuf:"varint,3,opt,name=next,proto3,oneof" json:"next,omitempty"` // required LastHash []byte `protobuf:"bytes,4,opt,name=last_hash,json=lastHash,proto3,oneof" json:"last_hash,omitempty"` // required + LaneId *LaneID `protobuf:"bytes,5,opt,name=lane_id,json=laneId,proto3,oneof" json:"lane_id,omitempty"` // required unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *LaneRange) Reset() { *x = LaneRange{} - mi := &file_autobahn_autobahn_proto_msgTypes[12] + mi := &file_autobahn_autobahn_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -737,7 +791,7 @@ func (x *LaneRange) String() string { func (*LaneRange) ProtoMessage() {} func (x *LaneRange) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[12] + mi := &file_autobahn_autobahn_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -750,14 +804,7 @@ func (x *LaneRange) ProtoReflect() protoreflect.Message { // Deprecated: Use LaneRange.ProtoReflect.Descriptor instead. func (*LaneRange) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{12} -} - -func (x *LaneRange) GetLane() *PublicKey { - if x != nil { - return x.Lane - } - return nil + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{13} } func (x *LaneRange) GetFirst() uint64 { @@ -781,6 +828,13 @@ func (x *LaneRange) GetLastHash() []byte { return nil } +func (x *LaneRange) GetLaneId() *LaneID { + if x != nil { + return x.LaneId + } + return nil +} + type View struct { state protoimpl.MessageState `protogen:"open.v1"` Index *uint64 `protobuf:"varint,1,opt,name=index,proto3,oneof" json:"index,omitempty"` // required @@ -792,7 +846,7 @@ type View struct { func (x *View) Reset() { *x = View{} - mi := &file_autobahn_autobahn_proto_msgTypes[13] + mi := &file_autobahn_autobahn_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -804,7 +858,7 @@ func (x *View) String() string { func (*View) ProtoMessage() {} func (x *View) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[13] + mi := &file_autobahn_autobahn_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -817,7 +871,7 @@ func (x *View) ProtoReflect() protoreflect.Message { // Deprecated: Use View.ProtoReflect.Descriptor instead. func (*View) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{13} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{14} } func (x *View) GetIndex() uint64 { @@ -854,7 +908,7 @@ type Proposal struct { func (x *Proposal) Reset() { *x = Proposal{} - mi := &file_autobahn_autobahn_proto_msgTypes[14] + mi := &file_autobahn_autobahn_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -866,7 +920,7 @@ func (x *Proposal) String() string { func (*Proposal) ProtoMessage() {} func (x *Proposal) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[14] + mi := &file_autobahn_autobahn_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -879,7 +933,7 @@ func (x *Proposal) ProtoReflect() protoreflect.Message { // Deprecated: Use Proposal.ProtoReflect.Descriptor instead. func (*Proposal) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{14} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{15} } func (x *Proposal) GetView() *View { @@ -929,7 +983,7 @@ type FullProposal struct { func (x *FullProposal) Reset() { *x = FullProposal{} - mi := &file_autobahn_autobahn_proto_msgTypes[15] + mi := &file_autobahn_autobahn_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -941,7 +995,7 @@ func (x *FullProposal) String() string { func (*FullProposal) ProtoMessage() {} func (x *FullProposal) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[15] + mi := &file_autobahn_autobahn_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -954,7 +1008,7 @@ func (x *FullProposal) ProtoReflect() protoreflect.Message { // Deprecated: Use FullProposal.ProtoReflect.Descriptor instead. func (*FullProposal) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{15} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{16} } func (x *FullProposal) GetProposalV2() *SignedProposal { @@ -995,7 +1049,7 @@ type PrepareQC struct { func (x *PrepareQC) Reset() { *x = PrepareQC{} - mi := &file_autobahn_autobahn_proto_msgTypes[16] + mi := &file_autobahn_autobahn_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1007,7 +1061,7 @@ func (x *PrepareQC) String() string { func (*PrepareQC) ProtoMessage() {} func (x *PrepareQC) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[16] + mi := &file_autobahn_autobahn_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1020,7 +1074,7 @@ func (x *PrepareQC) ProtoReflect() protoreflect.Message { // Deprecated: Use PrepareQC.ProtoReflect.Descriptor instead. func (*PrepareQC) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{16} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{17} } func (x *PrepareQC) GetVote() *Proposal { @@ -1047,7 +1101,7 @@ type CommitQC struct { func (x *CommitQC) Reset() { *x = CommitQC{} - mi := &file_autobahn_autobahn_proto_msgTypes[17] + mi := &file_autobahn_autobahn_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1059,7 +1113,7 @@ func (x *CommitQC) String() string { func (*CommitQC) ProtoMessage() {} func (x *CommitQC) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[17] + mi := &file_autobahn_autobahn_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1072,7 +1126,7 @@ func (x *CommitQC) ProtoReflect() protoreflect.Message { // Deprecated: Use CommitQC.ProtoReflect.Descriptor instead. func (*CommitQC) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{17} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{18} } func (x *CommitQC) GetVote() *Proposal { @@ -1099,7 +1153,7 @@ type FullCommitQC struct { func (x *FullCommitQC) Reset() { *x = FullCommitQC{} - mi := &file_autobahn_autobahn_proto_msgTypes[18] + mi := &file_autobahn_autobahn_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1111,7 +1165,7 @@ func (x *FullCommitQC) String() string { func (*FullCommitQC) ProtoMessage() {} func (x *FullCommitQC) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[18] + mi := &file_autobahn_autobahn_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1124,7 +1178,7 @@ func (x *FullCommitQC) ProtoReflect() protoreflect.Message { // Deprecated: Use FullCommitQC.ProtoReflect.Descriptor instead. func (*FullCommitQC) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{18} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{19} } func (x *FullCommitQC) GetQc() *CommitQC { @@ -1151,7 +1205,7 @@ type TimeoutVote struct { func (x *TimeoutVote) Reset() { *x = TimeoutVote{} - mi := &file_autobahn_autobahn_proto_msgTypes[19] + mi := &file_autobahn_autobahn_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1163,7 +1217,7 @@ func (x *TimeoutVote) String() string { func (*TimeoutVote) ProtoMessage() {} func (x *TimeoutVote) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[19] + mi := &file_autobahn_autobahn_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1176,7 +1230,7 @@ func (x *TimeoutVote) ProtoReflect() protoreflect.Message { // Deprecated: Use TimeoutVote.ProtoReflect.Descriptor instead. func (*TimeoutVote) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{19} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{20} } func (x *TimeoutVote) GetView() *View { @@ -1203,7 +1257,7 @@ type TimeoutQC struct { func (x *TimeoutQC) Reset() { *x = TimeoutQC{} - mi := &file_autobahn_autobahn_proto_msgTypes[20] + mi := &file_autobahn_autobahn_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1215,7 +1269,7 @@ func (x *TimeoutQC) String() string { func (*TimeoutQC) ProtoMessage() {} func (x *TimeoutQC) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[20] + mi := &file_autobahn_autobahn_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1228,7 +1282,7 @@ func (x *TimeoutQC) ProtoReflect() protoreflect.Message { // Deprecated: Use TimeoutQC.ProtoReflect.Descriptor instead. func (*TimeoutQC) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{20} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{21} } func (x *TimeoutQC) GetVotesV2() []*SignedTimeoutVote { @@ -1255,7 +1309,7 @@ type FullTimeoutVote struct { func (x *FullTimeoutVote) Reset() { *x = FullTimeoutVote{} - mi := &file_autobahn_autobahn_proto_msgTypes[21] + mi := &file_autobahn_autobahn_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1267,7 +1321,7 @@ func (x *FullTimeoutVote) String() string { func (*FullTimeoutVote) ProtoMessage() {} func (x *FullTimeoutVote) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[21] + mi := &file_autobahn_autobahn_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1280,7 +1334,7 @@ func (x *FullTimeoutVote) ProtoReflect() protoreflect.Message { // Deprecated: Use FullTimeoutVote.ProtoReflect.Descriptor instead. func (*FullTimeoutVote) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{21} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{22} } func (x *FullTimeoutVote) GetVoteV2() *SignedTimeoutVote { @@ -1315,7 +1369,7 @@ type PersistedInner struct { func (x *PersistedInner) Reset() { *x = PersistedInner{} - mi := &file_autobahn_autobahn_proto_msgTypes[22] + mi := &file_autobahn_autobahn_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1327,7 +1381,7 @@ func (x *PersistedInner) String() string { func (*PersistedInner) ProtoMessage() {} func (x *PersistedInner) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[22] + mi := &file_autobahn_autobahn_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1340,7 +1394,7 @@ func (x *PersistedInner) ProtoReflect() protoreflect.Message { // Deprecated: Use PersistedInner.ProtoReflect.Descriptor instead. func (*PersistedInner) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{22} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{23} } func (x *PersistedInner) GetCommitQc() *CommitQC { @@ -1397,7 +1451,7 @@ type PersistedAvailPruneAnchor struct { func (x *PersistedAvailPruneAnchor) Reset() { *x = PersistedAvailPruneAnchor{} - mi := &file_autobahn_autobahn_proto_msgTypes[23] + mi := &file_autobahn_autobahn_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1409,7 +1463,7 @@ func (x *PersistedAvailPruneAnchor) String() string { func (*PersistedAvailPruneAnchor) ProtoMessage() {} func (x *PersistedAvailPruneAnchor) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[23] + mi := &file_autobahn_autobahn_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1422,7 +1476,7 @@ func (x *PersistedAvailPruneAnchor) ProtoReflect() protoreflect.Message { // Deprecated: Use PersistedAvailPruneAnchor.ProtoReflect.Descriptor instead. func (*PersistedAvailPruneAnchor) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{23} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{24} } func (x *PersistedAvailPruneAnchor) GetAppQc() *AppQC { @@ -1449,7 +1503,7 @@ type AppQC struct { func (x *AppQC) Reset() { *x = AppQC{} - mi := &file_autobahn_autobahn_proto_msgTypes[24] + mi := &file_autobahn_autobahn_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1461,7 +1515,7 @@ func (x *AppQC) String() string { func (*AppQC) ProtoMessage() {} func (x *AppQC) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[24] + mi := &file_autobahn_autobahn_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1474,7 +1528,7 @@ func (x *AppQC) ProtoReflect() protoreflect.Message { // Deprecated: Use AppQC.ProtoReflect.Descriptor instead. func (*AppQC) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{24} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{25} } func (x *AppQC) GetVote() *AppProposal { @@ -1507,7 +1561,7 @@ type AppProposal struct { func (x *AppProposal) Reset() { *x = AppProposal{} - mi := &file_autobahn_autobahn_proto_msgTypes[25] + mi := &file_autobahn_autobahn_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1519,7 +1573,7 @@ func (x *AppProposal) String() string { func (*AppProposal) ProtoMessage() {} func (x *AppProposal) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[25] + mi := &file_autobahn_autobahn_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1532,7 +1586,7 @@ func (x *AppProposal) ProtoReflect() protoreflect.Message { // Deprecated: Use AppProposal.ProtoReflect.Descriptor instead. func (*AppProposal) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{25} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{26} } func (x *AppProposal) GetGlobalNumber() uint64 { @@ -1583,7 +1637,7 @@ type Msg struct { func (x *Msg) Reset() { *x = Msg{} - mi := &file_autobahn_autobahn_proto_msgTypes[26] + mi := &file_autobahn_autobahn_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1595,7 +1649,7 @@ func (x *Msg) String() string { func (*Msg) ProtoMessage() {} func (x *Msg) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[26] + mi := &file_autobahn_autobahn_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1608,7 +1662,7 @@ func (x *Msg) ProtoReflect() protoreflect.Message { // Deprecated: Use Msg.ProtoReflect.Descriptor instead. func (*Msg) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{26} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{27} } func (x *Msg) GetT() isMsg_T { @@ -1740,7 +1794,7 @@ type SignedProposal struct { func (x *SignedProposal) Reset() { *x = SignedProposal{} - mi := &file_autobahn_autobahn_proto_msgTypes[27] + mi := &file_autobahn_autobahn_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1752,7 +1806,7 @@ func (x *SignedProposal) String() string { func (*SignedProposal) ProtoMessage() {} func (x *SignedProposal) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[27] + mi := &file_autobahn_autobahn_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1765,7 +1819,7 @@ func (x *SignedProposal) ProtoReflect() protoreflect.Message { // Deprecated: Use SignedProposal.ProtoReflect.Descriptor instead. func (*SignedProposal) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{27} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{28} } func (x *SignedProposal) GetMsg() *Proposal { @@ -1792,7 +1846,7 @@ type SignedTimeoutVote struct { func (x *SignedTimeoutVote) Reset() { *x = SignedTimeoutVote{} - mi := &file_autobahn_autobahn_proto_msgTypes[28] + mi := &file_autobahn_autobahn_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1804,7 +1858,7 @@ func (x *SignedTimeoutVote) String() string { func (*SignedTimeoutVote) ProtoMessage() {} func (x *SignedTimeoutVote) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[28] + mi := &file_autobahn_autobahn_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1817,7 +1871,7 @@ func (x *SignedTimeoutVote) ProtoReflect() protoreflect.Message { // Deprecated: Use SignedTimeoutVote.ProtoReflect.Descriptor instead. func (*SignedTimeoutVote) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{28} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{29} } func (x *SignedTimeoutVote) GetMsg() *TimeoutVote { @@ -1844,7 +1898,7 @@ type SignedAppVote struct { func (x *SignedAppVote) Reset() { *x = SignedAppVote{} - mi := &file_autobahn_autobahn_proto_msgTypes[29] + mi := &file_autobahn_autobahn_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1856,7 +1910,7 @@ func (x *SignedAppVote) String() string { func (*SignedAppVote) ProtoMessage() {} func (x *SignedAppVote) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[29] + mi := &file_autobahn_autobahn_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1869,7 +1923,7 @@ func (x *SignedAppVote) ProtoReflect() protoreflect.Message { // Deprecated: Use SignedAppVote.ProtoReflect.Descriptor instead. func (*SignedAppVote) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{29} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{30} } func (x *SignedAppVote) GetMsg() *AppProposal { @@ -1896,7 +1950,7 @@ type SignedBlock struct { func (x *SignedBlock) Reset() { *x = SignedBlock{} - mi := &file_autobahn_autobahn_proto_msgTypes[30] + mi := &file_autobahn_autobahn_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1908,7 +1962,7 @@ func (x *SignedBlock) String() string { func (*SignedBlock) ProtoMessage() {} func (x *SignedBlock) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[30] + mi := &file_autobahn_autobahn_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1921,7 +1975,7 @@ func (x *SignedBlock) ProtoReflect() protoreflect.Message { // Deprecated: Use SignedBlock.ProtoReflect.Descriptor instead. func (*SignedBlock) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{30} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{31} } func (x *SignedBlock) GetMsg() *Block { @@ -1948,7 +2002,7 @@ type SignedBlockHeader struct { func (x *SignedBlockHeader) Reset() { *x = SignedBlockHeader{} - mi := &file_autobahn_autobahn_proto_msgTypes[31] + mi := &file_autobahn_autobahn_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1960,7 +2014,7 @@ func (x *SignedBlockHeader) String() string { func (*SignedBlockHeader) ProtoMessage() {} func (x *SignedBlockHeader) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[31] + mi := &file_autobahn_autobahn_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1973,7 +2027,7 @@ func (x *SignedBlockHeader) ProtoReflect() protoreflect.Message { // Deprecated: Use SignedBlockHeader.ProtoReflect.Descriptor instead. func (*SignedBlockHeader) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{31} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{32} } func (x *SignedBlockHeader) GetMsg() *BlockHeader { @@ -2000,7 +2054,7 @@ type SignedAppProposal struct { func (x *SignedAppProposal) Reset() { *x = SignedAppProposal{} - mi := &file_autobahn_autobahn_proto_msgTypes[32] + mi := &file_autobahn_autobahn_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2012,7 +2066,7 @@ func (x *SignedAppProposal) String() string { func (*SignedAppProposal) ProtoMessage() {} func (x *SignedAppProposal) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[32] + mi := &file_autobahn_autobahn_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2025,7 +2079,7 @@ func (x *SignedAppProposal) ProtoReflect() protoreflect.Message { // Deprecated: Use SignedAppProposal.ProtoReflect.Descriptor instead. func (*SignedAppProposal) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{32} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{33} } func (x *SignedAppProposal) GetMsg() *AppProposal { @@ -2059,7 +2113,7 @@ type ConsensusReq struct { func (x *ConsensusReq) Reset() { *x = ConsensusReq{} - mi := &file_autobahn_autobahn_proto_msgTypes[33] + mi := &file_autobahn_autobahn_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2071,7 +2125,7 @@ func (x *ConsensusReq) String() string { func (*ConsensusReq) ProtoMessage() {} func (x *ConsensusReq) ProtoReflect() protoreflect.Message { - mi := &file_autobahn_autobahn_proto_msgTypes[33] + mi := &file_autobahn_autobahn_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2084,7 +2138,7 @@ func (x *ConsensusReq) ProtoReflect() protoreflect.Message { // Deprecated: Use ConsensusReq.ProtoReflect.Descriptor instead. func (*ConsensusReq) Descriptor() ([]byte, []int) { - return file_autobahn_autobahn_proto_rawDescGZIP(), []int{33} + return file_autobahn_autobahn_proto_rawDescGZIP(), []int{34} } func (x *ConsensusReq) GetT() isConsensusReq_T { @@ -2218,22 +2272,29 @@ const file_autobahn_autobahn_proto_rawDesc = "" + "\tPublicKey\x12%\n" + "\aed25519\x18\x01 \x01(\fB\x06؈\xe2\xab\f H\x00R\aed25519\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\n" + "\n" + - "\b_ed25519\"t\n" + + "\b_ed25519\"\x83\x01\n" + + "\x06LaneID\x126\n" + + "\tvalidator\x18\x01 \x01(\v2\x13.autobahn.PublicKeyH\x00R\tvalidator\x88\x01\x01\x12\x1a\n" + + "\x06e_join\x18\x02 \x01(\x04H\x01R\x05eJoin\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\f\n" + + "\n" + + "_validatorB\t\n" + + "\a_e_join\"t\n" + "\tSignature\x12*\n" + "\x03key\x18\x01 \x01(\v2\x13.autobahn.PublicKeyH\x00R\x03key\x88\x01\x01\x12\x1d\n" + "\x03sig\x18\x02 \x01(\fB\x06؈\xe2\xab\f@H\x01R\x03sig\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\x06\n" + "\x04_keyB\x06\n" + - "\x04_sig\"\x8a\x02\n" + - "\vBlockHeader\x12,\n" + - "\x04lane\x18\x01 \x01(\v2\x13.autobahn.PublicKeyH\x00R\x04lane\x88\x01\x01\x12&\n" + - "\fblock_number\x18\x02 \x01(\x04H\x01R\vblockNumber\x88\x01\x01\x12,\n" + - "\vparent_hash\x18\x03 \x01(\fB\x06؈\xe2\xab\f H\x02R\n" + + "\x04_sig\"\x9b\x02\n" + + "\vBlockHeader\x12&\n" + + "\fblock_number\x18\x02 \x01(\x04H\x00R\vblockNumber\x88\x01\x01\x12,\n" + + "\vparent_hash\x18\x03 \x01(\fB\x06؈\xe2\xab\f H\x01R\n" + "parentHash\x88\x01\x01\x12.\n" + - "\fpayload_hash\x18\x04 \x01(\fB\x06؈\xe2\xab\f H\x03R\vpayloadHash\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\a\n" + - "\x05_laneB\x0f\n" + + "\fpayload_hash\x18\x04 \x01(\fB\x06؈\xe2\xab\f H\x02R\vpayloadHash\x88\x01\x01\x12.\n" + + "\alane_id\x18\x05 \x01(\v2\x10.autobahn.LaneIDH\x03R\x06laneId\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\x0f\n" + "\r_block_numberB\x0e\n" + "\f_parent_hashB\x0f\n" + - "\r_payload_hash\"\xd5\x02\n" + + "\r_payload_hashB\n" + + "\n" + + "\b_lane_idJ\x04\b\x01\x10\x02R\x04lane\"\xd5\x02\n" + "\aPayload\x127\n" + "\n" + "created_at\x18\x01 \x01(\v2\x13.autobahn.TimestampH\x00R\tcreatedAt\x88\x01\x01\x12-\n" + @@ -2252,17 +2313,18 @@ const file_autobahn_autobahn_proto_rawDesc = "" + "\b_payload\"l\n" + "\x06LaneQC\x12)\n" + "\x04vote\x18\x01 \x01(\v2\x15.autobahn.BlockHeaderR\x04vote\x12/\n" + - "\x04sigs\x18\x02 \x03(\v2\x13.autobahn.SignatureB\x06Ј\xe2\xab\fdR\x04sigs:\x06\xe8\x88\xe2\xab\f\x01\"\xcf\x01\n" + - "\tLaneRange\x12,\n" + - "\x04lane\x18\x01 \x01(\v2\x13.autobahn.PublicKeyH\x00R\x04lane\x88\x01\x01\x12\x19\n" + - "\x05first\x18\x02 \x01(\x04H\x01R\x05first\x88\x01\x01\x12\x17\n" + - "\x04next\x18\x03 \x01(\x04H\x02R\x04next\x88\x01\x01\x12(\n" + - "\tlast_hash\x18\x04 \x01(\fB\x06؈\xe2\xab\f H\x03R\blastHash\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\a\n" + - "\x05_laneB\b\n" + + "\x04sigs\x18\x02 \x03(\v2\x13.autobahn.SignatureB\x06Ј\xe2\xab\fdR\x04sigs:\x06\xe8\x88\xe2\xab\f\x01\"\xe0\x01\n" + + "\tLaneRange\x12\x19\n" + + "\x05first\x18\x02 \x01(\x04H\x00R\x05first\x88\x01\x01\x12\x17\n" + + "\x04next\x18\x03 \x01(\x04H\x01R\x04next\x88\x01\x01\x12(\n" + + "\tlast_hash\x18\x04 \x01(\fB\x06؈\xe2\xab\f H\x02R\blastHash\x88\x01\x01\x12.\n" + + "\alane_id\x18\x05 \x01(\v2\x10.autobahn.LaneIDH\x03R\x06laneId\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\b\n" + "\x06_firstB\a\n" + "\x05_nextB\f\n" + "\n" + - "_last_hash\"\x97\x01\n" + + "_last_hashB\n" + + "\n" + + "\b_lane_idJ\x04\b\x01\x10\x02R\x04lane\"\x97\x01\n" + "\x04View\x12\x19\n" + "\x05index\x18\x01 \x01(\x04H\x00R\x05index\x88\x01\x01\x12\x1b\n" + "\x06number\x18\x02 \x01(\x04H\x01R\x06number\x88\x01\x01\x12$\n" + @@ -2400,7 +2462,7 @@ func file_autobahn_autobahn_proto_rawDescGZIP() []byte { return file_autobahn_autobahn_proto_rawDescData } -var file_autobahn_autobahn_proto_msgTypes = make([]protoimpl.MessageInfo, 35) +var file_autobahn_autobahn_proto_msgTypes = make([]protoimpl.MessageInfo, 36) var file_autobahn_autobahn_proto_goTypes = []any{ (*Timestamp)(nil), // 0: autobahn.Timestamp (*Duration)(nil), // 1: autobahn.Duration @@ -2409,105 +2471,107 @@ var file_autobahn_autobahn_proto_goTypes = []any{ (*Transaction)(nil), // 4: autobahn.Transaction (*TransactionResp)(nil), // 5: autobahn.TransactionResp (*PublicKey)(nil), // 6: autobahn.PublicKey - (*Signature)(nil), // 7: autobahn.Signature - (*BlockHeader)(nil), // 8: autobahn.BlockHeader - (*Payload)(nil), // 9: autobahn.Payload - (*Block)(nil), // 10: autobahn.Block - (*LaneQC)(nil), // 11: autobahn.LaneQC - (*LaneRange)(nil), // 12: autobahn.LaneRange - (*View)(nil), // 13: autobahn.View - (*Proposal)(nil), // 14: autobahn.Proposal - (*FullProposal)(nil), // 15: autobahn.FullProposal - (*PrepareQC)(nil), // 16: autobahn.PrepareQC - (*CommitQC)(nil), // 17: autobahn.CommitQC - (*FullCommitQC)(nil), // 18: autobahn.FullCommitQC - (*TimeoutVote)(nil), // 19: autobahn.TimeoutVote - (*TimeoutQC)(nil), // 20: autobahn.TimeoutQC - (*FullTimeoutVote)(nil), // 21: autobahn.FullTimeoutVote - (*PersistedInner)(nil), // 22: autobahn.PersistedInner - (*PersistedAvailPruneAnchor)(nil), // 23: autobahn.PersistedAvailPruneAnchor - (*AppQC)(nil), // 24: autobahn.AppQC - (*AppProposal)(nil), // 25: autobahn.AppProposal - (*Msg)(nil), // 26: autobahn.Msg - (*SignedProposal)(nil), // 27: autobahn.SignedProposal - (*SignedTimeoutVote)(nil), // 28: autobahn.SignedTimeoutVote - (*SignedAppVote)(nil), // 29: autobahn.SignedAppVote - (*SignedBlock)(nil), // 30: autobahn.SignedBlock - (*SignedBlockHeader)(nil), // 31: autobahn.SignedBlockHeader - (*SignedAppProposal)(nil), // 32: autobahn.SignedAppProposal - (*ConsensusReq)(nil), // 33: autobahn.ConsensusReq - nil, // 34: autobahn.TransactionHeader.PropertiesEntry + (*LaneID)(nil), // 7: autobahn.LaneID + (*Signature)(nil), // 8: autobahn.Signature + (*BlockHeader)(nil), // 9: autobahn.BlockHeader + (*Payload)(nil), // 10: autobahn.Payload + (*Block)(nil), // 11: autobahn.Block + (*LaneQC)(nil), // 12: autobahn.LaneQC + (*LaneRange)(nil), // 13: autobahn.LaneRange + (*View)(nil), // 14: autobahn.View + (*Proposal)(nil), // 15: autobahn.Proposal + (*FullProposal)(nil), // 16: autobahn.FullProposal + (*PrepareQC)(nil), // 17: autobahn.PrepareQC + (*CommitQC)(nil), // 18: autobahn.CommitQC + (*FullCommitQC)(nil), // 19: autobahn.FullCommitQC + (*TimeoutVote)(nil), // 20: autobahn.TimeoutVote + (*TimeoutQC)(nil), // 21: autobahn.TimeoutQC + (*FullTimeoutVote)(nil), // 22: autobahn.FullTimeoutVote + (*PersistedInner)(nil), // 23: autobahn.PersistedInner + (*PersistedAvailPruneAnchor)(nil), // 24: autobahn.PersistedAvailPruneAnchor + (*AppQC)(nil), // 25: autobahn.AppQC + (*AppProposal)(nil), // 26: autobahn.AppProposal + (*Msg)(nil), // 27: autobahn.Msg + (*SignedProposal)(nil), // 28: autobahn.SignedProposal + (*SignedTimeoutVote)(nil), // 29: autobahn.SignedTimeoutVote + (*SignedAppVote)(nil), // 30: autobahn.SignedAppVote + (*SignedBlock)(nil), // 31: autobahn.SignedBlock + (*SignedBlockHeader)(nil), // 32: autobahn.SignedBlockHeader + (*SignedAppProposal)(nil), // 33: autobahn.SignedAppProposal + (*ConsensusReq)(nil), // 34: autobahn.ConsensusReq + nil, // 35: autobahn.TransactionHeader.PropertiesEntry } var file_autobahn_autobahn_proto_depIdxs = []int32{ - 34, // 0: autobahn.TransactionHeader.properties:type_name -> autobahn.TransactionHeader.PropertiesEntry + 35, // 0: autobahn.TransactionHeader.properties:type_name -> autobahn.TransactionHeader.PropertiesEntry 2, // 1: autobahn.TransactionHeader.timestamps:type_name -> autobahn.TransactionTimestamps 3, // 2: autobahn.Transaction.header:type_name -> autobahn.TransactionHeader - 6, // 3: autobahn.Signature.key:type_name -> autobahn.PublicKey - 6, // 4: autobahn.BlockHeader.lane:type_name -> autobahn.PublicKey - 0, // 5: autobahn.Payload.created_at:type_name -> autobahn.Timestamp - 8, // 6: autobahn.Block.header:type_name -> autobahn.BlockHeader - 9, // 7: autobahn.Block.payload:type_name -> autobahn.Payload - 8, // 8: autobahn.LaneQC.vote:type_name -> autobahn.BlockHeader - 7, // 9: autobahn.LaneQC.sigs:type_name -> autobahn.Signature - 6, // 10: autobahn.LaneRange.lane:type_name -> autobahn.PublicKey - 13, // 11: autobahn.Proposal.view:type_name -> autobahn.View - 0, // 12: autobahn.Proposal.timestamp:type_name -> autobahn.Timestamp - 12, // 13: autobahn.Proposal.lane_ranges:type_name -> autobahn.LaneRange - 25, // 14: autobahn.Proposal.app:type_name -> autobahn.AppProposal - 27, // 15: autobahn.FullProposal.proposal_v2:type_name -> autobahn.SignedProposal - 11, // 16: autobahn.FullProposal.lane_qcs:type_name -> autobahn.LaneQC - 24, // 17: autobahn.FullProposal.app_qc:type_name -> autobahn.AppQC - 20, // 18: autobahn.FullProposal.timeout_qc:type_name -> autobahn.TimeoutQC - 14, // 19: autobahn.PrepareQC.vote:type_name -> autobahn.Proposal - 7, // 20: autobahn.PrepareQC.sigs:type_name -> autobahn.Signature - 14, // 21: autobahn.CommitQC.vote:type_name -> autobahn.Proposal - 7, // 22: autobahn.CommitQC.sigs:type_name -> autobahn.Signature - 17, // 23: autobahn.FullCommitQC.qc:type_name -> autobahn.CommitQC - 8, // 24: autobahn.FullCommitQC.headers:type_name -> autobahn.BlockHeader - 13, // 25: autobahn.TimeoutVote.view:type_name -> autobahn.View - 28, // 26: autobahn.TimeoutQC.votes_v2:type_name -> autobahn.SignedTimeoutVote - 16, // 27: autobahn.TimeoutQC.latest_prepare_qc:type_name -> autobahn.PrepareQC - 28, // 28: autobahn.FullTimeoutVote.vote_v2:type_name -> autobahn.SignedTimeoutVote - 16, // 29: autobahn.FullTimeoutVote.latest_prepare_qc:type_name -> autobahn.PrepareQC - 17, // 30: autobahn.PersistedInner.commit_qc:type_name -> autobahn.CommitQC - 16, // 31: autobahn.PersistedInner.prepare_qc:type_name -> autobahn.PrepareQC - 20, // 32: autobahn.PersistedInner.timeout_qc:type_name -> autobahn.TimeoutQC - 27, // 33: autobahn.PersistedInner.commit_vote_v2:type_name -> autobahn.SignedProposal - 27, // 34: autobahn.PersistedInner.prepare_vote_v2:type_name -> autobahn.SignedProposal - 21, // 35: autobahn.PersistedInner.timeout_vote:type_name -> autobahn.FullTimeoutVote - 24, // 36: autobahn.PersistedAvailPruneAnchor.app_qc:type_name -> autobahn.AppQC - 17, // 37: autobahn.PersistedAvailPruneAnchor.commit_qc:type_name -> autobahn.CommitQC - 25, // 38: autobahn.AppQC.vote:type_name -> autobahn.AppProposal - 7, // 39: autobahn.AppQC.sigs:type_name -> autobahn.Signature - 10, // 40: autobahn.Msg.lane_proposal:type_name -> autobahn.Block - 8, // 41: autobahn.Msg.lane_vote:type_name -> autobahn.BlockHeader - 14, // 42: autobahn.Msg.proposal:type_name -> autobahn.Proposal - 14, // 43: autobahn.Msg.prepare_vote:type_name -> autobahn.Proposal - 14, // 44: autobahn.Msg.commit_vote:type_name -> autobahn.Proposal - 19, // 45: autobahn.Msg.timeout_vote:type_name -> autobahn.TimeoutVote - 25, // 46: autobahn.Msg.app_vote:type_name -> autobahn.AppProposal - 14, // 47: autobahn.SignedProposal.msg:type_name -> autobahn.Proposal - 7, // 48: autobahn.SignedProposal.sig:type_name -> autobahn.Signature - 19, // 49: autobahn.SignedTimeoutVote.msg:type_name -> autobahn.TimeoutVote - 7, // 50: autobahn.SignedTimeoutVote.sig:type_name -> autobahn.Signature - 25, // 51: autobahn.SignedAppVote.msg:type_name -> autobahn.AppProposal - 7, // 52: autobahn.SignedAppVote.sig:type_name -> autobahn.Signature - 10, // 53: autobahn.SignedBlock.msg:type_name -> autobahn.Block - 7, // 54: autobahn.SignedBlock.sig:type_name -> autobahn.Signature - 8, // 55: autobahn.SignedBlockHeader.msg:type_name -> autobahn.BlockHeader - 7, // 56: autobahn.SignedBlockHeader.sig:type_name -> autobahn.Signature - 25, // 57: autobahn.SignedAppProposal.msg:type_name -> autobahn.AppProposal - 7, // 58: autobahn.SignedAppProposal.sig:type_name -> autobahn.Signature - 15, // 59: autobahn.ConsensusReq.proposal:type_name -> autobahn.FullProposal - 27, // 60: autobahn.ConsensusReq.prepare_vote_v2:type_name -> autobahn.SignedProposal - 27, // 61: autobahn.ConsensusReq.commit_vote_v2:type_name -> autobahn.SignedProposal - 21, // 62: autobahn.ConsensusReq.timeout_vote:type_name -> autobahn.FullTimeoutVote - 20, // 63: autobahn.ConsensusReq.timeout_qc:type_name -> autobahn.TimeoutQC - 64, // [64:64] is the sub-list for method output_type - 64, // [64:64] is the sub-list for method input_type - 64, // [64:64] is the sub-list for extension type_name - 64, // [64:64] is the sub-list for extension extendee - 0, // [0:64] is the sub-list for field type_name + 6, // 3: autobahn.LaneID.validator:type_name -> autobahn.PublicKey + 6, // 4: autobahn.Signature.key:type_name -> autobahn.PublicKey + 7, // 5: autobahn.BlockHeader.lane_id:type_name -> autobahn.LaneID + 0, // 6: autobahn.Payload.created_at:type_name -> autobahn.Timestamp + 9, // 7: autobahn.Block.header:type_name -> autobahn.BlockHeader + 10, // 8: autobahn.Block.payload:type_name -> autobahn.Payload + 9, // 9: autobahn.LaneQC.vote:type_name -> autobahn.BlockHeader + 8, // 10: autobahn.LaneQC.sigs:type_name -> autobahn.Signature + 7, // 11: autobahn.LaneRange.lane_id:type_name -> autobahn.LaneID + 14, // 12: autobahn.Proposal.view:type_name -> autobahn.View + 0, // 13: autobahn.Proposal.timestamp:type_name -> autobahn.Timestamp + 13, // 14: autobahn.Proposal.lane_ranges:type_name -> autobahn.LaneRange + 26, // 15: autobahn.Proposal.app:type_name -> autobahn.AppProposal + 28, // 16: autobahn.FullProposal.proposal_v2:type_name -> autobahn.SignedProposal + 12, // 17: autobahn.FullProposal.lane_qcs:type_name -> autobahn.LaneQC + 25, // 18: autobahn.FullProposal.app_qc:type_name -> autobahn.AppQC + 21, // 19: autobahn.FullProposal.timeout_qc:type_name -> autobahn.TimeoutQC + 15, // 20: autobahn.PrepareQC.vote:type_name -> autobahn.Proposal + 8, // 21: autobahn.PrepareQC.sigs:type_name -> autobahn.Signature + 15, // 22: autobahn.CommitQC.vote:type_name -> autobahn.Proposal + 8, // 23: autobahn.CommitQC.sigs:type_name -> autobahn.Signature + 18, // 24: autobahn.FullCommitQC.qc:type_name -> autobahn.CommitQC + 9, // 25: autobahn.FullCommitQC.headers:type_name -> autobahn.BlockHeader + 14, // 26: autobahn.TimeoutVote.view:type_name -> autobahn.View + 29, // 27: autobahn.TimeoutQC.votes_v2:type_name -> autobahn.SignedTimeoutVote + 17, // 28: autobahn.TimeoutQC.latest_prepare_qc:type_name -> autobahn.PrepareQC + 29, // 29: autobahn.FullTimeoutVote.vote_v2:type_name -> autobahn.SignedTimeoutVote + 17, // 30: autobahn.FullTimeoutVote.latest_prepare_qc:type_name -> autobahn.PrepareQC + 18, // 31: autobahn.PersistedInner.commit_qc:type_name -> autobahn.CommitQC + 17, // 32: autobahn.PersistedInner.prepare_qc:type_name -> autobahn.PrepareQC + 21, // 33: autobahn.PersistedInner.timeout_qc:type_name -> autobahn.TimeoutQC + 28, // 34: autobahn.PersistedInner.commit_vote_v2:type_name -> autobahn.SignedProposal + 28, // 35: autobahn.PersistedInner.prepare_vote_v2:type_name -> autobahn.SignedProposal + 22, // 36: autobahn.PersistedInner.timeout_vote:type_name -> autobahn.FullTimeoutVote + 25, // 37: autobahn.PersistedAvailPruneAnchor.app_qc:type_name -> autobahn.AppQC + 18, // 38: autobahn.PersistedAvailPruneAnchor.commit_qc:type_name -> autobahn.CommitQC + 26, // 39: autobahn.AppQC.vote:type_name -> autobahn.AppProposal + 8, // 40: autobahn.AppQC.sigs:type_name -> autobahn.Signature + 11, // 41: autobahn.Msg.lane_proposal:type_name -> autobahn.Block + 9, // 42: autobahn.Msg.lane_vote:type_name -> autobahn.BlockHeader + 15, // 43: autobahn.Msg.proposal:type_name -> autobahn.Proposal + 15, // 44: autobahn.Msg.prepare_vote:type_name -> autobahn.Proposal + 15, // 45: autobahn.Msg.commit_vote:type_name -> autobahn.Proposal + 20, // 46: autobahn.Msg.timeout_vote:type_name -> autobahn.TimeoutVote + 26, // 47: autobahn.Msg.app_vote:type_name -> autobahn.AppProposal + 15, // 48: autobahn.SignedProposal.msg:type_name -> autobahn.Proposal + 8, // 49: autobahn.SignedProposal.sig:type_name -> autobahn.Signature + 20, // 50: autobahn.SignedTimeoutVote.msg:type_name -> autobahn.TimeoutVote + 8, // 51: autobahn.SignedTimeoutVote.sig:type_name -> autobahn.Signature + 26, // 52: autobahn.SignedAppVote.msg:type_name -> autobahn.AppProposal + 8, // 53: autobahn.SignedAppVote.sig:type_name -> autobahn.Signature + 11, // 54: autobahn.SignedBlock.msg:type_name -> autobahn.Block + 8, // 55: autobahn.SignedBlock.sig:type_name -> autobahn.Signature + 9, // 56: autobahn.SignedBlockHeader.msg:type_name -> autobahn.BlockHeader + 8, // 57: autobahn.SignedBlockHeader.sig:type_name -> autobahn.Signature + 26, // 58: autobahn.SignedAppProposal.msg:type_name -> autobahn.AppProposal + 8, // 59: autobahn.SignedAppProposal.sig:type_name -> autobahn.Signature + 16, // 60: autobahn.ConsensusReq.proposal:type_name -> autobahn.FullProposal + 28, // 61: autobahn.ConsensusReq.prepare_vote_v2:type_name -> autobahn.SignedProposal + 28, // 62: autobahn.ConsensusReq.commit_vote_v2:type_name -> autobahn.SignedProposal + 22, // 63: autobahn.ConsensusReq.timeout_vote:type_name -> autobahn.FullTimeoutVote + 21, // 64: autobahn.ConsensusReq.timeout_qc:type_name -> autobahn.TimeoutQC + 65, // [65:65] is the sub-list for method output_type + 65, // [65:65] is the sub-list for method input_type + 65, // [65:65] is the sub-list for extension type_name + 65, // [65:65] is the sub-list for extension extendee + 0, // [0:65] is the sub-list for field type_name } func init() { file_autobahn_autobahn_proto_init() } @@ -2522,17 +2586,18 @@ func file_autobahn_autobahn_proto_init() { file_autobahn_autobahn_proto_msgTypes[8].OneofWrappers = []any{} file_autobahn_autobahn_proto_msgTypes[9].OneofWrappers = []any{} file_autobahn_autobahn_proto_msgTypes[10].OneofWrappers = []any{} - file_autobahn_autobahn_proto_msgTypes[12].OneofWrappers = []any{} + file_autobahn_autobahn_proto_msgTypes[11].OneofWrappers = []any{} file_autobahn_autobahn_proto_msgTypes[13].OneofWrappers = []any{} file_autobahn_autobahn_proto_msgTypes[14].OneofWrappers = []any{} file_autobahn_autobahn_proto_msgTypes[15].OneofWrappers = []any{} - file_autobahn_autobahn_proto_msgTypes[19].OneofWrappers = []any{} + file_autobahn_autobahn_proto_msgTypes[16].OneofWrappers = []any{} file_autobahn_autobahn_proto_msgTypes[20].OneofWrappers = []any{} file_autobahn_autobahn_proto_msgTypes[21].OneofWrappers = []any{} file_autobahn_autobahn_proto_msgTypes[22].OneofWrappers = []any{} file_autobahn_autobahn_proto_msgTypes[23].OneofWrappers = []any{} - file_autobahn_autobahn_proto_msgTypes[25].OneofWrappers = []any{} - file_autobahn_autobahn_proto_msgTypes[26].OneofWrappers = []any{ + file_autobahn_autobahn_proto_msgTypes[24].OneofWrappers = []any{} + file_autobahn_autobahn_proto_msgTypes[26].OneofWrappers = []any{} + file_autobahn_autobahn_proto_msgTypes[27].OneofWrappers = []any{ (*Msg_LaneProposal)(nil), (*Msg_LaneVote)(nil), (*Msg_Proposal)(nil), @@ -2541,7 +2606,7 @@ func file_autobahn_autobahn_proto_init() { (*Msg_TimeoutVote)(nil), (*Msg_AppVote)(nil), } - file_autobahn_autobahn_proto_msgTypes[33].OneofWrappers = []any{ + file_autobahn_autobahn_proto_msgTypes[34].OneofWrappers = []any{ (*ConsensusReq_Proposal)(nil), (*ConsensusReq_PrepareVoteV2)(nil), (*ConsensusReq_CommitVoteV2)(nil), @@ -2554,7 +2619,7 @@ func file_autobahn_autobahn_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_autobahn_autobahn_proto_rawDesc), len(file_autobahn_autobahn_proto_rawDesc)), NumEnums: 0, - NumMessages: 35, + NumMessages: 36, NumExtensions: 0, NumServices: 0, }, diff --git a/sei-tendermint/internal/autobahn/pb/autobahn.wireguard.go b/sei-tendermint/internal/autobahn/pb/autobahn.wireguard.go index 99b476ba5d..d60d837174 100644 --- a/sei-tendermint/internal/autobahn/pb/autobahn.wireguard.go +++ b/sei-tendermint/internal/autobahn/pb/autobahn.wireguard.go @@ -15,12 +15,16 @@ func (*PublicKey) MaxSize() int { return 34 } +func (*LaneID) MaxSize() int { + return 47 +} + func (*Signature) MaxSize() int { return 102 } func (*BlockHeader) MaxSize() int { - return 115 + return 128 } func (*Payload) MaxSize() int { @@ -28,15 +32,15 @@ func (*Payload) MaxSize() int { } func (*Block) MaxSize() int { - return 2056167 + return 2056181 } func (*LaneQC) MaxSize() int { - return 10517 + return 10531 } func (*LaneRange) MaxSize() int { - return 92 + return 105 } func (*View) MaxSize() int { @@ -44,23 +48,23 @@ func (*View) MaxSize() int { } func (*Proposal) MaxSize() int { - return 9539 + return 10839 } func (*FullProposal) MaxSize() int { - return 1107571 + return 1111571 } func (*PrepareQC) MaxSize() int { - return 19942 + return 21242 } func (*CommitQC) MaxSize() int { - return 19942 + return 21242 } func (*FullCommitQC) MaxSize() int { - return 136946 + return 152246 } func (*TimeoutVote) MaxSize() int { @@ -68,11 +72,11 @@ func (*TimeoutVote) MaxSize() int { } func (*TimeoutQC) MaxSize() int { - return 35446 + return 36746 } func (*FullTimeoutVote) MaxSize() int { - return 20101 + return 21401 } func (*AppQC) MaxSize() int { @@ -84,11 +88,11 @@ func (*AppProposal) MaxSize() int { } func (*Msg) MaxSize() int { - return 2056171 + return 2056185 } func (*SignedProposal) MaxSize() int { - return 9646 + return 10946 } func (*SignedTimeoutVote) MaxSize() int { @@ -100,11 +104,11 @@ func (*SignedAppVote) MaxSize() int { } func (*SignedBlock) MaxSize() int { - return 2056275 + return 2056289 } func (*SignedBlockHeader) MaxSize() int { - return 221 + return 235 } func (*SignedAppProposal) MaxSize() int { @@ -112,7 +116,7 @@ func (*SignedAppProposal) MaxSize() int { } func (*ConsensusReq) MaxSize() int { - return 1107575 + return 1111575 } func init() { @@ -161,6 +165,12 @@ func init() { 1: {MaxCount: 1, MaxSize: 32}, }) + // Register the wireguard.Schema generated for autobahn.LaneID. + runtime.MustRegister[*LaneID](runtime.Schema{ + 1: {MaxCount: 1, Nested: utils.Some(reflect.TypeFor[*PublicKey]())}, + 2: {MaxCount: 1}, + }) + // Register the wireguard.Schema generated for autobahn.Signature. runtime.MustRegister[*Signature](runtime.Schema{ 1: {MaxCount: 1, Nested: utils.Some(reflect.TypeFor[*PublicKey]())}, @@ -169,10 +179,10 @@ func init() { // Register the wireguard.Schema generated for autobahn.BlockHeader. runtime.MustRegister[*BlockHeader](runtime.Schema{ - 1: {MaxCount: 1, Nested: utils.Some(reflect.TypeFor[*PublicKey]())}, 2: {MaxCount: 1}, 3: {MaxCount: 1, MaxSize: 32}, 4: {MaxCount: 1, MaxSize: 32}, + 5: {MaxCount: 1, Nested: utils.Some(reflect.TypeFor[*LaneID]())}, }) // Register the wireguard.Schema generated for autobahn.Payload. @@ -197,10 +207,10 @@ func init() { // Register the wireguard.Schema generated for autobahn.LaneRange. runtime.MustRegister[*LaneRange](runtime.Schema{ - 1: {MaxCount: 1, Nested: utils.Some(reflect.TypeFor[*PublicKey]())}, 2: {MaxCount: 1}, 3: {MaxCount: 1}, 4: {MaxCount: 1, MaxSize: 32}, + 5: {MaxCount: 1, Nested: utils.Some(reflect.TypeFor[*LaneID]())}, }) // Register the wireguard.Schema generated for autobahn.View. diff --git a/sei-tendermint/internal/autobahn/producer/mempool.go b/sei-tendermint/internal/autobahn/producer/mempool.go index f269ec3910..d523f71d43 100644 --- a/sei-tendermint/internal/autobahn/producer/mempool.go +++ b/sei-tendermint/internal/autobahn/producer/mempool.go @@ -16,6 +16,9 @@ var errTooLarge = errors.New("transaction too large") var errBadNonce = errors.New("bad nonce") var errMempoolFull = errors.New("mempool is full") +// ErrNotProducing: LocalLane None or mempool not aligned (leave / pre-align gap). +var ErrNotProducing = errors.New("not producing") + type blockSpec struct { gasEstimated uint64 gasWanted uint64 @@ -29,6 +32,7 @@ type blockSpec struct { type mempool struct { capacity uint64 + lane utils.Option[types.LaneID] first types.BlockNumber next types.BlockNumber blocks map[types.BlockNumber]*blockSpec @@ -79,13 +83,6 @@ func (s *State) EvmTxByHash(hash common.Hash) (tmtypes.Tx, bool) { panic("unreachable") } -func (s *State) mempoolFirst() types.BlockNumber { - for m := range s.mempool.Lock() { - return m.first - } - panic("unreachable") -} - // Removes txs from mempool assigned to lane blocks Date: Sun, 9 Aug 2026 08:15:26 -0700 Subject: [PATCH 02/14] fix(autobahn): allowCreate API, waitLaneBound, restore QC tests Drop *Committee from MaybePruneAndPersistLane; wake on tipEpoch map drop in PushBlock/PushVote via waitLaneBound; restore Prepare/App/empty committee coverage; tag multi-epoch follow-ups as TODO(#3736). Co-authored-by: Cursor --- .../autobahn/types/committee_test.go | 64 ++++++++++- .../internal/autobahn/avail/inner.go | 6 +- .../internal/autobahn/avail/state.go | 106 +++++++++++------- .../internal/autobahn/avail/state_test.go | 9 +- .../autobahn/consensus/persist/blocks.go | 13 ++- .../autobahn/consensus/persist/blocks_test.go | 33 ++---- .../internal/autobahn/epoch/registry.go | 2 +- 7 files changed, 155 insertions(+), 78 deletions(-) diff --git a/sei-tendermint/autobahn/types/committee_test.go b/sei-tendermint/autobahn/types/committee_test.go index c1a6a28150..61c1cf3041 100644 --- a/sei-tendermint/autobahn/types/committee_test.go +++ b/sei-tendermint/autobahn/types/committee_test.go @@ -104,22 +104,38 @@ func TestLaneQCVerifyChecksWeight(t *testing.T) { require.Error(t, lightMajority.Verify(ep.Committee())) } -func TestCommitQCVerifyChecksWeight(t *testing.T) { +func TestPrepareQCVerifyChecksWeight(t *testing.T) { rng := utils.TestRng() ep, keys := makeEpoch(rng) - vote := NewCommitVote(ProposalAt(ep, View{EpochIndex: ep.EpochIndex(), Index: ep.RoadRange().First})) + vote := NewPrepareVote(ProposalAt(ep, View{EpochIndex: ep.EpochIndex(), Index: ep.RoadRange().First})) - heavyOnly := NewCommitQC([]*Signed[*CommitVote]{ + heavyOnly := NewPrepareQC([]*Signed[*PrepareVote]{ Sign(keys[0], vote), }) require.NoError(t, heavyOnly.Verify(ep)) - lightMajority := NewCommitQC([]*Signed[*CommitVote]{ + lightMajority := NewPrepareQC([]*Signed[*PrepareVote]{ Sign(keys[1], vote), Sign(keys[2], vote), }) require.Error(t, lightMajority.Verify(ep)) } +func TestPrepareQCVerifyChecksEpochBinding(t *testing.T) { + rng := utils.TestRng() + ep, keys := makeEpoch(rng) + sign := func(p *Proposal) *PrepareQC { + return NewPrepareQC([]*Signed[*PrepareVote]{Sign(keys[0], NewPrepareVote(p))}) + } + + 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()) + 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()) + require.Error(t, sign(outOfRoads).Verify(ep)) +} + func TestCommitQCVerifyChecksEpochBinding(t *testing.T) { rng := utils.TestRng() ep, keys := makeEpoch(rng) @@ -136,6 +152,39 @@ func TestCommitQCVerifyChecksEpochBinding(t *testing.T) { require.Error(t, sign(outOfRoads).Verify(ep)) } +func TestCommitQCVerifyChecksWeight(t *testing.T) { + rng := utils.TestRng() + ep, keys := makeEpoch(rng) + vote := NewCommitVote(ProposalAt(ep, View{EpochIndex: ep.EpochIndex(), Index: ep.RoadRange().First})) + + heavyOnly := NewCommitQC([]*Signed[*CommitVote]{ + Sign(keys[0], vote), + }) + require.NoError(t, heavyOnly.Verify(ep)) + lightMajority := NewCommitQC([]*Signed[*CommitVote]{ + Sign(keys[1], vote), + Sign(keys[2], vote), + }) + require.Error(t, lightMajority.Verify(ep)) +} + +func TestAppQCVerifyChecksWeight(t *testing.T) { + rng := utils.TestRng() + ep, keys := makeEpoch(rng) + vote := NewAppVote(NewAppProposal(0, 0, GenAppHash(rng), ep.EpochIndex())) + + heavyOnly := NewAppQC([]*Signed[*AppVote]{ + Sign(keys[0], vote), + }) + require.NoError(t, heavyOnly.Verify(ep.Committee())) + + lightMajority := NewAppQC([]*Signed[*AppVote]{ + Sign(keys[1], vote), + Sign(keys[2], vote), + }) + require.Error(t, lightMajority.Verify(ep.Committee())) +} + func TestTimeoutQCVerifyChecksEpochBinding(t *testing.T) { rng := utils.TestRng() ep, keys := makeEpoch(rng) @@ -170,3 +219,10 @@ func TestTimeoutQCVerifyChecksWeight(t *testing.T) { }) require.Error(t, lightMajority.Verify(ep, prev)) } + +func TestNewCommittee_RejectsEmptyWeights(t *testing.T) { + _, err := NewCommittee(map[PublicKey]uint64{}) + if err == nil { + t.Fatal("NewCommittee() succeeded with empty weights, want error") + } +} diff --git a/sei-tendermint/internal/autobahn/avail/inner.go b/sei-tendermint/internal/autobahn/avail/inner.go index c82de40b9c..97b6f2c152 100644 --- a/sei-tendermint/internal/autobahn/avail/inner.go +++ b/sei-tendermint/internal/autobahn/avail/inner.go @@ -85,8 +85,10 @@ func newInner(registry *epoch.Registry, loaded utils.Option[*loadedAvailState]) return i, nil } - // Re-attach persisted WALs before prune. Skip e_join<=N absent from anchor - // committee (those LaneIDs never rejoin; proposal ranges may omit empty lanes). + // Re-attach persisted WALs before prune. Skip tip-stale leave WALs: + // e_join <= anchorEpoch and absent from the anchor committee (same rule as + // staleLaneDisposable's e_join < tip: membership at e implies e_join <= e). + // Those LaneIDs never rejoin; proposal ranges may omit empty lanes. var anchorEpoch types.EpochIndex var anchorCommittee *types.Committee if anchor, ok := l.pruneAnchor.Get(); ok { diff --git a/sei-tendermint/internal/autobahn/avail/state.go b/sei-tendermint/internal/autobahn/avail/state.go index 6691a6df87..d8a4010133 100644 --- a/sei-tendermint/internal/autobahn/avail/state.go +++ b/sei-tendermint/internal/autobahn/avail/state.go @@ -90,6 +90,7 @@ func (s *State) WaitMustStop(ctx context.Context, lane types.LaneID) error { // ApplyEpoch installs the applied committee: add joiner maps, then Store ep under // the inner lock so waiters never observe the new committee before those maps exist. // Leavers stay until tipEpoch omits them (persist path). Registry ActivateEpoch is separate. +// Production wiring of ApplyEpoch is #3736; tests call it directly today. func (s *State) ApplyEpoch(ep *types.Epoch) { for inner, ctrl := range s.inner.Lock() { inner.addCommitteeLanes(ep.Committee()) @@ -311,7 +312,8 @@ func NewState(key types.SecretKey, data *data.State, stateDir utils.Option[strin if anchor, ok := ls.pruneAnchor.Get(); ok { c := ep.Committee() for lane := range inner.blocks { - if err := pers.blocks.MaybePruneAndPersistLane(lane, c, utils.Some(anchor.CommitQC), nil, utils.None[func(*types.Signed[*types.LaneProposal])]()); err != nil { + // allowCreate only for lanes still in the latest committee; leave WALs truncate in place. + if err := pers.blocks.MaybePruneAndPersistLane(lane, c.HasLane(lane), utils.Some(anchor.CommitQC), nil, utils.None[func(*types.Signed[*types.LaneProposal])]()); err != nil { return nil, fmt.Errorf("prune stale block WAL entries: %w", err) } } @@ -432,6 +434,7 @@ func (s *State) PushCommitQC(ctx context.Context, qc *types.CommitQC) error { if idx != inner.commitQCs.next { return nil } + // TODO(#3736): accept prior-epoch CommitQCs while tip lags across ApplyEpoch. if got, want := qc.Proposal().EpochIndex(), inner.epoch.Load().EpochIndex(); got != want { return fmt.Errorf("commitQC epoch_index %d != current epoch %d", got, want) } @@ -534,6 +537,30 @@ func (s *State) PushAppQC(appQC *types.AppQC, commitQC *types.CommitQC) error { return nil } +// waitLaneBound waits until m[lane] is present and n < bound(v), or the lane is +// gone (tipEpoch drop → ErrBadLane). bound is an exclusive upper limit. +func waitLaneBound[V any]( + ctx context.Context, + ctrl *utils.WatchCtrl, + m map[types.LaneID]V, + lane types.LaneID, + n types.BlockNumber, + bound func(V) types.BlockNumber, +) (V, error) { + var zero V + if err := ctrl.WaitUntil(ctx, func() bool { + v, ok := m[lane] + return !ok || n < bound(v) + }); err != nil { + return zero, err + } + v, ok := m[lane] + if !ok { + return zero, ErrBadLane + } + return v, nil +} + // NextBlock returns the index of the next missing block in local storage for the given lane. func (s *State) NextBlock(lane types.LaneID) types.BlockNumber { for inner := range s.inner.Lock() { @@ -550,16 +577,14 @@ func (s *State) NextBlock(lane types.LaneID) types.BlockNumber { // Returns ErrBadLane if the lane map is gone (tipEpoch leave prune). func (s *State) Block(ctx context.Context, lane types.LaneID, n types.BlockNumber) (*types.Signed[*types.LaneProposal], error) { for inner, ctrl := range s.inner.Lock() { - if err := ctrl.WaitUntil(ctx, func() bool { - q, ok := inner.blocks[lane] - return !ok || n < q.next - }); err != nil { + q, err := waitLaneBound(ctx, ctrl, inner.blocks, lane, n, + func(q *queue[types.BlockNumber, *types.Signed[*types.LaneProposal]]) types.BlockNumber { + return q.next + }, + ) + if err != nil { return nil, err } - q, ok := inner.blocks[lane] - if !ok { - return nil, ErrBadLane - } if n < q.first { return nil, types.ErrPruned } @@ -570,6 +595,7 @@ func (s *State) Block(ctx context.Context, lane types.LaneID, n types.BlockNumbe // PushBlock pushes a block to the state. // Waits until all previous blocks are available. +// Returns ErrBadLane if tipEpoch drop removes the lane map while waiting. func (s *State) PushBlock(ctx context.Context, p *types.Signed[*types.LaneProposal]) error { h := p.Msg().Block().Header() if p.Key() != h.Lane().Validator() { @@ -583,18 +609,20 @@ func (s *State) PushBlock(ctx context.Context, p *types.Signed[*types.LanePropos }); err != nil { return fmt.Errorf("block.Verify(): %w", err) } + lane := h.Lane() + n := h.BlockNumber() for inner, ctrl := range s.inner.Lock() { - q, ok := inner.blocks[h.Lane()] - if !ok { - return ErrBadLane - } - if err := ctrl.WaitUntil(ctx, func() bool { - return h.BlockNumber() <= min(q.next, inner.persistedBlockStart[h.Lane()]+BlocksPerLane-1) - }); err != nil { + q, err := waitLaneBound(ctx, ctrl, inner.blocks, lane, n, + func(q *queue[types.BlockNumber, *types.Signed[*types.LaneProposal]]) types.BlockNumber { + // exclusive: n <= min(q.next, start+cap-1) + return min(q.next, inner.persistedBlockStart[lane]+BlocksPerLane-1) + 1 + }, + ) + if err != nil { return err } // not needed any more - if q.next != h.BlockNumber() { + if q.next != n { return nil } // Verify parent hash chain to prevent a malicious producer from @@ -610,8 +638,8 @@ func (s *State) PushBlock(ctx context.Context, p *types.Signed[*types.LanePropos prevHash := q.q[q.next-1].Msg().Block().Header().Hash() if h.ParentHash() != prevHash { logger.Error("parent hash mismatch (producer equivocation)", - "lane", h.Lane(), - slog.Uint64("block", uint64(h.BlockNumber())), + "lane", lane, + slog.Uint64("block", uint64(n)), "got", h.ParentHash(), "want", prevHash) return nil @@ -626,6 +654,7 @@ func (s *State) PushBlock(ctx context.Context, p *types.Signed[*types.LanePropos // PushVote pushes a LaneVote to the state. // Waits until the lane has enough capacity for the new vote. // It does NOT wait for the previous votes. +// Returns ErrBadLane if tipEpoch drop removes the lane map while waiting. func (s *State) PushVote(ctx context.Context, vote *types.Signed[*types.LaneVote]) error { if _, err := s.data.Registry().VerifyInWindow(func(c *types.Committee) error { if err := vote.Msg().Verify(c); err != nil { @@ -636,23 +665,25 @@ func (s *State) PushVote(ctx context.Context, vote *types.Signed[*types.LaneVote return fmt.Errorf("vote.Verify(): %w", err) } h := vote.Msg().Header() + lane := h.Lane() + n := h.BlockNumber() for inner, ctrl := range s.inner.Lock() { - q, ok := inner.votes[h.Lane()] - if !ok { - return ErrBadLane - } - if err := ctrl.WaitUntil(ctx, func() bool { - return h.BlockNumber() < inner.persistedBlockStart[h.Lane()]+BlocksPerLane - }); err != nil { + q, err := waitLaneBound(ctx, ctrl, inner.votes, lane, n, + func(*queue[types.BlockNumber, blockVotes]) types.BlockNumber { + // votes and persistedBlockStart are dropped together + return inner.persistedBlockStart[lane] + BlocksPerLane + }, + ) + if err != nil { return err } - if h.BlockNumber() < q.first { + if n < q.first { return nil } - for q.next <= h.BlockNumber() { + for q.next <= n { q.pushBack(newBlockVotes()) } - if _, ok := q.q[h.BlockNumber()].pushVote(inner.epoch.Load(), vote); ok { + if _, ok := q.q[n].pushVote(inner.epoch.Load(), vote); ok { ctrl.Updated() } } @@ -729,17 +760,15 @@ func (s *State) WaitForLocalCapacity(ctx context.Context, lane types.LaneID, toP if !inner.epoch.Load().Committee().HasLane(lane) { return true } - if _, ok := inner.blocks[lane]; !ok { - return true - } - return toProduce < inner.persistedBlockStart[lane]+BlocksPerLane + start, ok := inner.persistedBlockStart[lane] + return !ok || toProduce < start+BlocksPerLane }); err != nil { return err } if !inner.epoch.Load().Committee().HasLane(lane) { return ErrBadLane } - if _, ok := inner.blocks[lane]; !ok { + if _, ok := inner.persistedBlockStart[lane]; !ok { return ErrBadLane } } @@ -922,9 +951,8 @@ func (s *State) runPersist(ctx context.Context, pers persisters) error { }) // Collect lanes: any lane with blocks in this batch, plus all lanes // in the anchor epoch (for WAL pruning). - // TODO: when epoch transitions land, also union in lanes from all - // epochs that appear in batch.commitQCs so new-epoch lanes are - // never skipped in a cross-epoch batch. + // TODO(#3736): only lanes of the latest CommitQC's epoch are + // admitted — do not union earlier epochs from batch.commitQCs. batchLanes := map[types.LaneID]struct{}{} for lane := range blocksByLane { batchLanes[lane] = struct{}{} @@ -941,8 +969,8 @@ func (s *State) runPersist(ctx context.Context, pers persisters) error { for lane := range batchLanes { proposals := blocksByLane[lane] ps.Spawn(func() error { - // allowCreate if active or proposals non-empty (leave flush before first WAL). - return pers.blocks.MaybePruneAndPersistLane(lane, active, anchorQC, proposals, utils.Some(markBlock)) + allowCreate := active.HasLane(lane) || len(proposals) > 0 + return pers.blocks.MaybePruneAndPersistLane(lane, allowCreate, anchorQC, proposals, utils.Some(markBlock)) }) } return nil diff --git a/sei-tendermint/internal/autobahn/avail/state_test.go b/sei-tendermint/internal/autobahn/avail/state_test.go index c39b1abd6f..d954114a24 100644 --- a/sei-tendermint/internal/autobahn/avail/state_test.go +++ b/sei-tendermint/internal/autobahn/avail/state_test.go @@ -551,7 +551,7 @@ func TestNewStateWithPersistence(t *testing.T) { block := types.NewBlock(lane, n, parent, types.GenPayload(rng)) signed := types.Sign(keys[0], types.NewLaneProposal(block)) parent = block.Header().Hash() - require.NoError(t, bp.MaybePruneAndPersistLane(lane, utils.OrPanic1(types.NewCommittee(map[types.PublicKey]uint64{lane.Validator(): 1})), utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{signed}, noBlockCB)) + require.NoError(t, bp.MaybePruneAndPersistLane(lane, true, utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{signed}, noBlockCB)) } // Release the seeding persister's WAL locks before NewState opens the same directory. @@ -603,7 +603,7 @@ func TestNewStateWithPersistence(t *testing.T) { block := types.NewBlock(lane, n, parent, types.GenPayload(rng)) signed := types.Sign(keys[0], types.NewLaneProposal(block)) parent = block.Header().Hash() - require.NoError(t, bp.MaybePruneAndPersistLane(lane, utils.OrPanic1(types.NewCommittee(map[types.PublicKey]uint64{lane.Validator(): 1})), utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{signed}, noBlockCB)) + require.NoError(t, bp.MaybePruneAndPersistLane(lane, true, utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{signed}, noBlockCB)) } // Release the seeding persisters' WAL locks before NewState opens the same directory. @@ -792,7 +792,7 @@ func TestNewStateWithPersistence(t *testing.T) { block := types.NewBlock(lane, n, parent, types.GenPayload(rng)) signed := types.Sign(keys[0], types.NewLaneProposal(block)) parent = block.Header().Hash() - require.NoError(t, bp.MaybePruneAndPersistLane(lane, utils.OrPanic1(types.NewCommittee(map[types.PublicKey]uint64{lane.Validator(): 1})), utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{signed}, noBlockCB)) + require.NoError(t, bp.MaybePruneAndPersistLane(lane, true, utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{signed}, noBlockCB)) } // Persist a prune anchor at index 9 with a laneRange that starts past @@ -851,9 +851,8 @@ func TestNewStateWithPersistence(t *testing.T) { var parent types.BlockHeaderHash block := types.NewBlock(lane, 0, parent, types.GenPayload(rng)) proposals := []*types.Signed[*types.LaneProposal]{types.Sign(keys[0], types.NewLaneProposal(block))} - active := utils.OrPanic1(types.NewCommittee(map[types.PublicKey]uint64{keys[0].Public(): 1})) require.NoError(t, bp.MaybePruneAndPersistLane( - lane, active, utils.None[*types.CommitQC](), proposals, noBlockCB)) + lane, true, utils.None[*types.CommitQC](), proposals, noBlockCB)) require.NoError(t, bp.Close()) // A prune anchor missing its CommitQC unmarshals as proto but fails PruneAnchorConv.Decode, so diff --git a/sei-tendermint/internal/autobahn/consensus/persist/blocks.go b/sei-tendermint/internal/autobahn/consensus/persist/blocks.go index bf1f499bb9..4fb48fb007 100644 --- a/sei-tendermint/internal/autobahn/consensus/persist/blocks.go +++ b/sei-tendermint/internal/autobahn/consensus/persist/blocks.go @@ -233,7 +233,9 @@ func NewBlockPersister(stateDir utils.Option[string]) (*BlockPersister, map[type } lane, err := types.LaneIDFromBytes(laneBytes) if err != nil { - logger.Warn("skipping lane dir with invalid LaneID", "name", e.Name(), "err", err) + // Pre-LaneID hex(pubkey) dirs fail to parse; they leak until the + // operator wipes persistent_state_dir (no migration in this PR). + logger.Warn("skipping lane dir with invalid LaneID (leaks until state wipe)", "name", e.Name(), "err", err) continue } lanePath := filepath.Join(dir, e.Name()) @@ -297,9 +299,9 @@ func (bp *BlockPersister) getLane(lane types.LaneID, allowCreate bool) (lw *lane // - anchor empty, proposals non-empty: append only, no truncation. // - anchor empty, proposals empty: no-op. // -// active: open WALs for HasLane; leavers flush if already open. Non-empty -// proposals still allowCreate so a leave before first open flushes tips -// (post-DeleteLane batches omit the lane, so prune does not recreate). +// allowCreate: open a WAL if missing. Avail passes true for active lanes, or when +// proposals are non-empty so a leave before the first open still flushes tips. +// After DeleteLane, empty-proposal prune passes false so the WAL is not recreated. // // afterEach, when present, is called once per appended proposal in order, after the whole batch has // been flushed — never before, because an append is not durable until then and afterEach is what @@ -312,7 +314,7 @@ func (bp *BlockPersister) getLane(lane types.LaneID, allowCreate bool) (lw *lane // so concurrent calls on the same lane serialize correctly. func (bp *BlockPersister) MaybePruneAndPersistLane( lane types.LaneID, - active *types.Committee, + allowCreate bool, anchor utils.Option[*types.CommitQC], proposals []*types.Signed[*types.LaneProposal], afterEach utils.Option[func(*types.Signed[*types.LaneProposal])], @@ -326,7 +328,6 @@ func (bp *BlockPersister) MaybePruneAndPersistLane( return nil } - allowCreate := active.HasLane(lane) || len(proposals) > 0 lw, ok, err := bp.getLane(lane, allowCreate) if err != nil { return err diff --git a/sei-tendermint/internal/autobahn/consensus/persist/blocks_test.go b/sei-tendermint/internal/autobahn/consensus/persist/blocks_test.go index 4414659e33..9162f4058d 100644 --- a/sei-tendermint/internal/autobahn/consensus/persist/blocks_test.go +++ b/sei-tendermint/internal/autobahn/consensus/persist/blocks_test.go @@ -17,10 +17,6 @@ func testSignedProposal(rng utils.Rng, key types.SecretKey, n types.BlockNumber) return types.Sign(key, types.NewLaneProposal(block)) } -func committeeForLane(lane types.LaneID) *types.Committee { - return utils.OrPanic1(types.NewCommittee(map[types.PublicKey]uint64{lane.Validator(): 1})) -} - var noBlockCB = utils.None[func(*types.Signed[*types.LaneProposal])]() // liveBlocks drops blocks the prune anchor has moved past, mirroring the filter loadPersistedState @@ -40,7 +36,7 @@ func testPersistBlock(t *testing.T, bp *BlockPersister, p *types.Signed[*types.L lane := p.Msg().Block().Header().Lane() require.NoError(t, bp.MaybePruneAndPersistLane( lane, - committeeForLane(lane), + true, utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{p}, noBlockCB, @@ -223,11 +219,11 @@ func TestNoOpBlockPersister(t *testing.T) { // Verify afterEach is still invoked for every proposal. var called int cb := utils.Some(func(_ *types.Signed[*types.LaneProposal]) { called++ }) - require.NoError(t, bp.MaybePruneAndPersistLane(lane, committeeForLane(lane), utils.None[*types.CommitQC](), proposals[:3], cb)) + require.NoError(t, bp.MaybePruneAndPersistLane(lane, true, utils.None[*types.CommitQC](), proposals[:3], cb)) require.Equal(t, 3, called) called = 0 - require.NoError(t, bp.MaybePruneAndPersistLane(lane, committeeForLane(lane), utils.None[*types.CommitQC](), proposals[3:], cb)) + require.NoError(t, bp.MaybePruneAndPersistLane(lane, true, utils.None[*types.CommitQC](), proposals[3:], cb)) require.Equal(t, 2, called) require.NoError(t, bp.Close()) @@ -306,7 +302,7 @@ func TestDeleteBeforePastAllRejectsStaleBlock(t *testing.T) { // Writing a stale block number (0) should be rejected. stale := testSignedProposal(rng, key, 0) - err = bp.MaybePruneAndPersistLane(lane, committeeForLane(lane), utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{stale}, noBlockCB) + err = bp.MaybePruneAndPersistLane(lane, true, utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{stale}, noBlockCB) require.Error(t, err) require.Contains(t, err.Error(), "out of sequence") @@ -410,13 +406,13 @@ func TestPersistBlockOutOfSequence(t *testing.T) { // Gap: skip block 1, try block 2. gap := testSignedProposal(rng, key, 2) - err = bp.MaybePruneAndPersistLane(lane, committeeForLane(lane), utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{gap}, noBlockCB) + err = bp.MaybePruneAndPersistLane(lane, true, utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{gap}, noBlockCB) require.Error(t, err) require.Contains(t, err.Error(), "out of sequence") // Duplicate: try block 0 again. dup := testSignedProposal(rng, key, 0) - err = bp.MaybePruneAndPersistLane(lane, committeeForLane(lane), utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{dup}, noBlockCB) + err = bp.MaybePruneAndPersistLane(lane, true, utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{dup}, noBlockCB) require.Error(t, err) require.Contains(t, err.Error(), "out of sequence") @@ -532,7 +528,7 @@ func TestPersistBlockInvokesAfterEachOncePerBlock(t *testing.T) { cb := utils.Some(func(p *types.Signed[*types.LaneProposal]) { seen = append(seen, p.Msg().Block().Header().BlockNumber()) }) - require.NoError(t, bp.MaybePruneAndPersistLane(lane, committeeForLane(lane), utils.None[*types.CommitQC](), proposals, cb)) + require.NoError(t, bp.MaybePruneAndPersistLane(lane, true, utils.None[*types.CommitQC](), proposals, cb)) require.NoError(t, bp.Close()) require.Equal(t, len(proposals), len(seen)) @@ -568,7 +564,7 @@ func TestPersistBlockConcurrentDistinctLanes(t *testing.T) { for i := range numLanes { lane := types.NewLaneID(keys[i].Public(), 0) ps.Spawn(func() error { - return bp.MaybePruneAndPersistLane(lane, committeeForLane(lane), utils.None[*types.CommitQC](), proposals[i], noBlockCB) + return bp.MaybePruneAndPersistLane(lane, true, utils.None[*types.CommitQC](), proposals[i], noBlockCB) }) } return nil @@ -598,7 +594,6 @@ func TestMaybePruneAndPersistLane_InactiveDoesNotRecreateAfterDelete(t *testing. t.Cleanup(func() { _ = bp.Close() }) leaver := types.GenSecretKey(rng) - stayer := types.GenSecretKey(rng) lane := types.NewLaneID(leaver.Public(), 0) proposal := types.Sign(leaver, types.NewLaneProposal( types.NewBlock(lane, 0, types.BlockHeaderHash{}, types.GenPayload(rng)), @@ -606,7 +601,7 @@ func TestMaybePruneAndPersistLane_InactiveDoesNotRecreateAfterDelete(t *testing. require.NoError(t, bp.MaybePruneAndPersistLane( lane, - committeeForLane(lane), + true, utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{proposal}, noBlockCB, @@ -617,11 +612,9 @@ func TestMaybePruneAndPersistLane_InactiveDoesNotRecreateAfterDelete(t *testing. require.True(t, os.IsNotExist(err)) require.NoError(t, bp.DeleteLane(lane)) // idempotent - active := utils.OrPanic1(types.NewCommittee(map[types.PublicKey]uint64{stayer.Public(): 1})) - require.False(t, active.HasLane(lane)) require.NoError(t, bp.MaybePruneAndPersistLane( lane, - active, + false, // after DeleteLane: truncate-only must not recreate utils.None[*types.CommitQC](), nil, noBlockCB, @@ -640,17 +633,15 @@ func TestMaybePruneAndPersistLane_InactiveWithProposalsCreatesWAL(t *testing.T) t.Cleanup(func() { _ = bp.Close() }) leaver := types.GenSecretKey(rng) - stayer := types.GenSecretKey(rng) lane := types.NewLaneID(leaver.Public(), 0) proposal := types.Sign(leaver, types.NewLaneProposal( types.NewBlock(lane, 0, types.BlockHeaderHash{}, types.GenPayload(rng)), )) - active := utils.OrPanic1(types.NewCommittee(map[types.PublicKey]uint64{stayer.Public(): 1})) - require.False(t, active.HasLane(lane)) + // Inactive leave still flushes when proposals are non-empty (allowCreate=true). require.NoError(t, bp.MaybePruneAndPersistLane( lane, - active, + true, utils.None[*types.CommitQC](), []*types.Signed[*types.LaneProposal]{proposal}, noBlockCB, diff --git a/sei-tendermint/internal/autobahn/epoch/registry.go b/sei-tendermint/internal/autobahn/epoch/registry.go index beebfb38aa..32c0cc63ff 100644 --- a/sei-tendermint/internal/autobahn/epoch/registry.go +++ b/sei-tendermint/internal/autobahn/epoch/registry.go @@ -99,7 +99,7 @@ func (r *Registry) ActivateEpoch( // VerifyInWindow calls fn against the latest epoch's committee and returns it if accepted. // Returns a slice of all matching epochs so callers can skip re-verification for any // epoch already checked here. -// TODO: expand to neighbor epochs (previous and next) once multi-epoch transitions are wired up. +// TODO(#3736): expand to neighbor epochs (previous and next) once multi-epoch transitions are wired up. func (r *Registry) VerifyInWindow(fn func(*types.Committee) error) ([]*types.Epoch, error) { for s := range r.state.RLock() { ep := s.m[s.latest] From d7902613e19c56eaa2c841e08f690b700f3105f2 Mon Sep 17 00:00:00 2001 From: Wen Date: Sun, 9 Aug 2026 08:46:40 -0700 Subject: [PATCH 03/14] docs(autobahn): succinct LaneID/avail lifecycle; drop waitLaneBound Document stay/leave/rejoin on LaneID and map/tip dispose on avail. Inline tipEpoch-aware waits; clarify restart tipcut <= vs live <. Co-authored-by: Cursor --- sei-tendermint/autobahn/types/lane_id.go | 7 +- .../internal/autobahn/avail/inner.go | 12 +-- .../internal/autobahn/avail/state.go | 99 ++++++++++--------- 3 files changed, 63 insertions(+), 55 deletions(-) diff --git a/sei-tendermint/autobahn/types/lane_id.go b/sei-tendermint/autobahn/types/lane_id.go index fca4f5720d..50c840777c 100644 --- a/sei-tendermint/autobahn/types/lane_id.go +++ b/sei-tendermint/autobahn/types/lane_id.go @@ -12,7 +12,12 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" ) -// LaneID is a validator's continuous membership streak; e_join is the join epoch. +// LaneID identifies a validator's continuous committee membership streak. +// e_join is the epoch in which that streak began. +// +// Identity rules: stay keeps the same LaneID; leave ends that identity; rejoin +// allocates a new LaneID (typically with tip 0). Avail map retention and tipEpoch +// dispose live in package avail (see its package doc). type LaneID struct { utils.ReadOnly validator PublicKey diff --git a/sei-tendermint/internal/autobahn/avail/inner.go b/sei-tendermint/internal/autobahn/avail/inner.go index 97b6f2c152..ae3dd144b6 100644 --- a/sei-tendermint/internal/autobahn/avail/inner.go +++ b/sei-tendermint/internal/autobahn/avail/inner.go @@ -11,8 +11,8 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" ) -// Lane maps: joiners at ApplyEpoch; leavers until tipEpoch omits, then drop + DeleteLane. -// Restart re-attaches leave WALs; tipEpoch omit cleans them up. +// Lane maps: joiners at ApplyEpoch; leavers until tipEpoch dispose (package doc). +// Restart re-attaches leave WALs; tip-stale ones are skipped (tipcut). type inner struct { epoch utils.AtomicSend[*types.Epoch] latestAppQC utils.Option[*types.AppQC] @@ -85,10 +85,10 @@ func newInner(registry *epoch.Registry, loaded utils.Option[*loadedAvailState]) return i, nil } - // Re-attach persisted WALs before prune. Skip tip-stale leave WALs: - // e_join <= anchorEpoch and absent from the anchor committee (same rule as - // staleLaneDisposable's e_join < tip: membership at e implies e_join <= e). - // Those LaneIDs never rejoin; proposal ranges may omit empty lanes. + // Re-attach persisted WALs before prune. Skip tip-stale leave WALs already + // disposable at the prune anchor. Live dispose uses e_join < tip; restart + // tipcut skip uses e_join <= tip because a leave tip may be unnamed in the + // tipcut proposal while still disposable. Those LaneIDs never rejoin. var anchorEpoch types.EpochIndex var anchorCommittee *types.Committee if anchor, ok := l.pruneAnchor.Get(); ok { diff --git a/sei-tendermint/internal/autobahn/avail/state.go b/sei-tendermint/internal/autobahn/avail/state.go index d8a4010133..0fdc5852c3 100644 --- a/sei-tendermint/internal/autobahn/avail/state.go +++ b/sei-tendermint/internal/autobahn/avail/state.go @@ -1,3 +1,21 @@ +// Package avail is the Data Availability Plane and Ordered Event Log: lane +// blocks, CommitQC/AppQC buffers, and pruning. +// +// Lane map lifecycle (CON-358; production ApplyEpoch wiring is #3736). +// Identity rules (stay / leave / rejoin) are on types.LaneID. +// +// Maps (inner.blocks / votes / cursors): +// - Join/stay: ensured at ApplyEpoch (addCommitteeLanes, then Store epoch). +// - Leave: maps remain until tipEpoch (epoch of the first retained CommitQC) +// omits the LaneID, then DeleteLane + map drop on the same persist tick. +// - Dispose: e_join < tipEpoch && !tipCommittee.HasLane(lane). +// +// Subscribe binds LocalLane at subscribe time and serves leave maps until +// dispose → ErrLanePruned. Produce sessions use WaitProduce / WaitMustStop +// (same LaneID stay does not end the session). +// +// Restart re-attaches leave WALs still needed for tip; skips WALs already +// tip-stale at the prune anchor (see tipcut skip in newInner). package avail import ( @@ -87,10 +105,10 @@ func (s *State) WaitMustStop(ctx context.Context, lane types.LaneID) error { return err } -// ApplyEpoch installs the applied committee: add joiner maps, then Store ep under -// the inner lock so waiters never observe the new committee before those maps exist. -// Leavers stay until tipEpoch omits them (persist path). Registry ActivateEpoch is separate. -// Production wiring of ApplyEpoch is #3736; tests call it directly today. +// ApplyEpoch installs the applied committee under the inner lock (joiner maps +// before Store so waiters never observe the new committee without those maps). +// Leavers stay until tipEpoch dispose (see package doc). Registry ActivateEpoch +// is separate; production wiring is #3736. func (s *State) ApplyEpoch(ep *types.Epoch) { for inner, ctrl := range s.inner.Lock() { inner.addCommitteeLanes(ep.Committee()) @@ -537,30 +555,6 @@ func (s *State) PushAppQC(appQC *types.AppQC, commitQC *types.CommitQC) error { return nil } -// waitLaneBound waits until m[lane] is present and n < bound(v), or the lane is -// gone (tipEpoch drop → ErrBadLane). bound is an exclusive upper limit. -func waitLaneBound[V any]( - ctx context.Context, - ctrl *utils.WatchCtrl, - m map[types.LaneID]V, - lane types.LaneID, - n types.BlockNumber, - bound func(V) types.BlockNumber, -) (V, error) { - var zero V - if err := ctrl.WaitUntil(ctx, func() bool { - v, ok := m[lane] - return !ok || n < bound(v) - }); err != nil { - return zero, err - } - v, ok := m[lane] - if !ok { - return zero, ErrBadLane - } - return v, nil -} - // NextBlock returns the index of the next missing block in local storage for the given lane. func (s *State) NextBlock(lane types.LaneID) types.BlockNumber { for inner := range s.inner.Lock() { @@ -577,14 +571,16 @@ func (s *State) NextBlock(lane types.LaneID) types.BlockNumber { // Returns ErrBadLane if the lane map is gone (tipEpoch leave prune). func (s *State) Block(ctx context.Context, lane types.LaneID, n types.BlockNumber) (*types.Signed[*types.LaneProposal], error) { for inner, ctrl := range s.inner.Lock() { - q, err := waitLaneBound(ctx, ctrl, inner.blocks, lane, n, - func(q *queue[types.BlockNumber, *types.Signed[*types.LaneProposal]]) types.BlockNumber { - return q.next - }, - ) - if err != nil { + if err := ctrl.WaitUntil(ctx, func() bool { + q, ok := inner.blocks[lane] + return !ok || n < q.next + }); err != nil { return nil, err } + q, ok := inner.blocks[lane] + if !ok { + return nil, ErrBadLane + } if n < q.first { return nil, types.ErrPruned } @@ -612,15 +608,19 @@ func (s *State) PushBlock(ctx context.Context, p *types.Signed[*types.LanePropos lane := h.Lane() n := h.BlockNumber() for inner, ctrl := range s.inner.Lock() { - q, err := waitLaneBound(ctx, ctrl, inner.blocks, lane, n, - func(q *queue[types.BlockNumber, *types.Signed[*types.LaneProposal]]) types.BlockNumber { - // exclusive: n <= min(q.next, start+cap-1) - return min(q.next, inner.persistedBlockStart[lane]+BlocksPerLane-1) + 1 - }, - ) - if err != nil { + if err := ctrl.WaitUntil(ctx, func() bool { + q, ok := inner.blocks[lane] + if !ok { + return true // tipEpoch drop + } + return n <= min(q.next, inner.persistedBlockStart[lane]+BlocksPerLane-1) + }); err != nil { return err } + q, ok := inner.blocks[lane] + if !ok { + return ErrBadLane + } // not needed any more if q.next != n { return nil @@ -668,15 +668,18 @@ func (s *State) PushVote(ctx context.Context, vote *types.Signed[*types.LaneVote lane := h.Lane() n := h.BlockNumber() for inner, ctrl := range s.inner.Lock() { - q, err := waitLaneBound(ctx, ctrl, inner.votes, lane, n, - func(*queue[types.BlockNumber, blockVotes]) types.BlockNumber { - // votes and persistedBlockStart are dropped together - return inner.persistedBlockStart[lane] + BlocksPerLane - }, - ) - if err != nil { + if err := ctrl.WaitUntil(ctx, func() bool { + if _, ok := inner.votes[lane]; !ok { + return true // tipEpoch drop + } + return n < inner.persistedBlockStart[lane]+BlocksPerLane + }); err != nil { return err } + q, ok := inner.votes[lane] + if !ok { + return ErrBadLane + } if n < q.first { return nil } From 379a9f14d04bdec360c8bb0695b8e17b704ef2e4 Mon Sep 17 00:00:00 2001 From: Wen Date: Sun, 9 Aug 2026 09:58:33 -0700 Subject: [PATCH 04/14] fix(autobahn): capacity wait map presence; session scope.Run Key WaitForLocalCapacity off blocks so a zero start is not treated as prune. Drive produce leave cancel with scope.Run instead of errgroup. Co-authored-by: Cursor --- .../internal/autobahn/avail/state.go | 10 ++++--- .../internal/autobahn/producer/state.go | 26 +++++++++---------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/sei-tendermint/internal/autobahn/avail/state.go b/sei-tendermint/internal/autobahn/avail/state.go index 0fdc5852c3..8265a49d23 100644 --- a/sei-tendermint/internal/autobahn/avail/state.go +++ b/sei-tendermint/internal/autobahn/avail/state.go @@ -757,21 +757,25 @@ func (s *State) fullCommitQC(ctx context.Context, n types.RoadIndex) (*types.Ful // WaitForLocalCapacity waits until the lane has capacity for toProduce. // ErrBadLane if the lane left committee or its map was tipEpoch-pruned while waiting. +// Presence is keyed off blocks (always set for live lanes); persistedBlockStart may +// be absent on a fresh start (zero start), which is not a prune. func (s *State) WaitForLocalCapacity(ctx context.Context, lane types.LaneID, toProduce types.BlockNumber) error { for inner, ctrl := range s.inner.Lock() { if err := ctrl.WaitUntil(ctx, func() bool { if !inner.epoch.Load().Committee().HasLane(lane) { return true } - start, ok := inner.persistedBlockStart[lane] - return !ok || toProduce < start+BlocksPerLane + if _, ok := inner.blocks[lane]; !ok { + return true + } + return toProduce < inner.persistedBlockStart[lane]+BlocksPerLane }); err != nil { return err } if !inner.epoch.Load().Committee().HasLane(lane) { return ErrBadLane } - if _, ok := inner.persistedBlockStart[lane]; !ok { + if _, ok := inner.blocks[lane]; !ok { return ErrBadLane } } diff --git a/sei-tendermint/internal/autobahn/producer/state.go b/sei-tendermint/internal/autobahn/producer/state.go index 4d0357dcbc..4fa71239dc 100644 --- a/sei-tendermint/internal/autobahn/producer/state.go +++ b/sei-tendermint/internal/autobahn/producer/state.go @@ -14,7 +14,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/scope" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" - "golang.org/x/sync/errgroup" "golang.org/x/time/rate" ) @@ -121,18 +120,19 @@ func (s *State) Run(ctx context.Context) error { return err } - g, gctx := errgroup.WithContext(ctx) - g.Go(func() error { - return s.produceSession(gctx, availState, lane) - }) - g.Go(func() error { - // Cancels seal / executed waits that do not observe committee. - if err := availState.WaitMustStop(gctx, lane); err != nil { - return err - } - return context.Canceled - }) - if err := utils.IgnoreCancel(g.Wait()); err != nil { + if err := utils.IgnoreCancel(scope.Run(ctx, func(ctx context.Context, sc scope.Scope) error { + sc.Spawn(func() error { + return s.produceSession(ctx, availState, lane) + }) + sc.Spawn(func() error { + // Cancels seal / executed waits that do not observe committee. + if err := availState.WaitMustStop(ctx, lane); err != nil { + return err + } + return context.Canceled + }) + return nil + })); err != nil { return err } s.clearMempool() From f1d7f3ff6a612019cdc36fb9cab37b0da7a59590 Mon Sep 17 00:00:00 2001 From: Wen Date: Sun, 9 Aug 2026 10:27:41 -0700 Subject: [PATCH 05/14] fix(autobahn): drop newInner head-gap skip Restore contiguousSuffix tip integrity: WAL must start at q.next. No-anchor mid-WAL is unsupported; tip-stale leave WALs stay tipcut-skipped. Co-authored-by: Cursor --- sei-tendermint/internal/autobahn/avail/inner.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sei-tendermint/internal/autobahn/avail/inner.go b/sei-tendermint/internal/autobahn/avail/inner.go index ae3dd144b6..d3ee834429 100644 --- a/sei-tendermint/internal/autobahn/avail/inner.go +++ b/sei-tendermint/internal/autobahn/avail/inner.go @@ -144,15 +144,13 @@ func newInner(registry *epoch.Registry, loaded utils.Option[*loadedAvailState]) } // Restore persisted blocks for re-attached lanes. Gaps / bad parent / over-cap → error. + // No head-gap skip: WAL must start at q.next. No-anchor mid-WAL is unsupported + // (rare; first AppQC forms quickly). Tip-stale leave WALs are tipcut-skipped above. for lane, bs := range l.blocks { q, ok := i.blocks[lane] if !ok || len(bs) == 0 { continue } - // Unnamed-by-tipEpoch tips start at First=0; advance to WAL start before load. - if bs[0].Number > q.next { - q.prune(bs[0].Number) - } var lastHash types.BlockHeaderHash for j, b := range bs { if q.Len() >= BlocksPerLane { From 1dc9ec11eebc165149fbfef4d7e7ed470aef5119 Mon Sep 17 00:00:00 2001 From: Wen Date: Mon, 10 Aug 2026 13:27:49 -0700 Subject: [PATCH 06/14] refactor(autobahn): document ordering; easy LaneID/committee nits Document replica vs lane order on Committee. Use Option.Or and cmp.Or, document normalizeWeights, take test lanes from the committee, and put lane_id first in proto source (tag unchanged). Co-authored-by: Cursor --- sei-tendermint/autobahn/types/committee.go | 18 +++++++++++------- .../autobahn/types/committee_test.go | 5 +++-- sei-tendermint/autobahn/types/lane_id.go | 8 ++++---- .../internal/autobahn/autobahn.proto | 8 ++------ 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/sei-tendermint/autobahn/types/committee.go b/sei-tendermint/autobahn/types/committee.go index 07b6bbfb19..1739ef4a79 100644 --- a/sei-tendermint/autobahn/types/committee.go +++ b/sei-tendermint/autobahn/types/committee.go @@ -23,8 +23,13 @@ func (s ImSlice[T]) All() iter.Seq[T] { return slices.Values(s.s) } // Committee represents the consensus committee. // Lanes carry membership (validator + e_join); weights are voting stake. +// +// Members are totally ordered for Leader/EvmShard lottery and tipcut header +// concatenation. Replicas order by PublicKey; lanes by LaneID.Compare +// (validator, then e_join). With one lane per validator those coincide, so +// walking Lanes() is walking replica order. type Committee struct { - lanes ImSlice[LaneID] // sorted; one lane per member + lanes ImSlice[LaneID] // sorted by LaneID.Compare; one per member byValidator map[PublicKey]LaneID weights map[PublicKey]uint64 totalWeight uint64 @@ -50,10 +55,11 @@ func (c *Committee) Lane(v PublicKey) utils.Option[LaneID] { return utils.Some(lane) } -// Lanes is the list of nodes which are eligible to produce blocks. +// Lanes returns members in LaneID order (see Committee). func (c *Committee) Lanes() ImSlice[LaneID] { return c.lanes } // Deterministic random oracle selecting a replica with probability proportional to the weight. +// Walks Lanes() so seed → PublicKey is network-wide deterministic (see Committee). func (c *Committee) randomReplica(seed []byte) PublicKey { h := sha256.Sum256(seed[:]) var x, total uint256.Int @@ -147,15 +153,13 @@ func ActivateCommittee(prev *Committee, weights map[PublicKey]uint64, e EpochInd } lanes := make([]LaneID, 0, len(weights)) for v := range weights { - eJoin := e - if prevLane, ok := prev.Lane(v).Get(); ok { - eJoin = prevLane.eJoin - } - lanes = append(lanes, NewLaneID(v, eJoin)) + lanes = append(lanes, prev.Lane(v).Or(NewLaneID(v, e))) } return finalizeCommittee(lanes, weights, totalWeight) } +// normalizeWeights clones weights, drops zero entries, and returns the filtered +// map plus total stake. Errors on overflow or empty total. func normalizeWeights(weights map[PublicKey]uint64) (map[PublicKey]uint64, uint64, error) { weights = maps.Clone(weights) totalWeight := uint64(0) diff --git a/sei-tendermint/autobahn/types/committee_test.go b/sei-tendermint/autobahn/types/committee_test.go index 61c1cf3041..93860151c0 100644 --- a/sei-tendermint/autobahn/types/committee_test.go +++ b/sei-tendermint/autobahn/types/committee_test.go @@ -25,7 +25,7 @@ func TestNewCommittee_FiltersOutZeroWeightValidators(t *testing.T) { if committee.HasReplica(zeroWeightKey) { t.Fatal("HasReplica() = true for zero-weight validator, want false") } - if !committee.HasLane(NewLaneID(nonZeroWeightKey, 0)) { + if !committee.HasLane(committee.Lane(nonZeroWeightKey).OrPanic("member")) { t.Fatal("HasLane(nonZero@e0) = false, want true") } if got := committee.Lanes().Len(); got != 1 { @@ -91,7 +91,8 @@ func makeEpoch(rng utils.Rng) (*Epoch, []SecretKey) { func TestLaneQCVerifyChecksWeight(t *testing.T) { rng := utils.TestRng() ep, keys := makeEpoch(rng) - vote := NewLaneVote(NewBlock(NewLaneID(keys[0].Public(), 0), 0, GenBlockHeaderHash(rng), GenPayload(rng)).Header()) + lane := ep.Committee().Lane(keys[0].Public()).OrPanic("keys[0]") + vote := NewLaneVote(NewBlock(lane, 0, GenBlockHeaderHash(rng), GenPayload(rng)).Header()) heavyOnly := NewLaneQC([]*Signed[*LaneVote]{ Sign(keys[0], vote), diff --git a/sei-tendermint/autobahn/types/lane_id.go b/sei-tendermint/autobahn/types/lane_id.go index 50c840777c..ce7f2df231 100644 --- a/sei-tendermint/autobahn/types/lane_id.go +++ b/sei-tendermint/autobahn/types/lane_id.go @@ -34,10 +34,10 @@ func (l LaneID) EJoin() EpochIndex { return l.eJoin } // Compare orders by validator, then e_join. func (l LaneID) Compare(other LaneID) int { - if c := l.validator.Compare(other.validator); c != 0 { - return c - } - return cmp.Compare(l.eJoin, other.eJoin) + return cmp.Or( + l.validator.Compare(other.validator), + cmp.Compare(l.eJoin, other.eJoin), + ) } // Bytes returns a stable encoding: pubkey bytes || big-endian e_join. diff --git a/sei-tendermint/internal/autobahn/autobahn.proto b/sei-tendermint/internal/autobahn/autobahn.proto index 195d5bc703..9ffb22a1fb 100644 --- a/sei-tendermint/internal/autobahn/autobahn.proto +++ b/sei-tendermint/internal/autobahn/autobahn.proto @@ -81,14 +81,12 @@ message Signature { message BlockHeader { option (hashable.hashable) = true; option (wireguard.sized) = true; - // Field 1 was PublicKey "lane"; LaneID is additive on a new number/name so - // WIRE / WIRE_JSON stay compatible (must reserve the old name). + optional LaneID lane_id = 5; // required reserved 1; reserved "lane"; optional uint64 block_number = 2; // required optional bytes parent_hash = 3 [(wireguard.max_size) = 32]; // required optional bytes payload_hash = 4 [(wireguard.max_size) = 32]; // required - optional LaneID lane_id = 5; // required } message Payload { @@ -121,14 +119,12 @@ message LaneQC { message LaneRange { option (hashable.hashable) = true; option (wireguard.sized) = true; - // Field 1 was PublicKey "lane"; LaneID is additive on a new number/name so - // WIRE / WIRE_JSON stay compatible (must reserve the old name). + optional LaneID lane_id = 5; // required reserved 1; reserved "lane"; optional uint64 first = 2; // required optional uint64 next = 3; // required optional bytes last_hash = 4 [(wireguard.max_size) = 32]; // required - optional LaneID lane_id = 5; // required } message View { From 1ef1c52883b18dc72f63df495e6c27c49c53048c Mon Sep 17 00:00:00 2001 From: Wen Date: Mon, 10 Aug 2026 14:48:49 -0700 Subject: [PATCH 07/14] chore(autobahn): regenerate pb after lane_id source reorder CI lint regenerates protos and diffs; field declaration order in autobahn.proto changed the generated Go descriptors. Co-authored-by: Cursor --- .../internal/autobahn/pb/autobahn.pb.go | 64 +++++++++---------- .../autobahn/pb/autobahn.wireguard.go | 4 +- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/sei-tendermint/internal/autobahn/pb/autobahn.pb.go b/sei-tendermint/internal/autobahn/pb/autobahn.pb.go index 5605608386..942d0ad008 100644 --- a/sei-tendermint/internal/autobahn/pb/autobahn.pb.go +++ b/sei-tendermint/internal/autobahn/pb/autobahn.pb.go @@ -529,10 +529,10 @@ func (x *Signature) GetSig() []byte { type BlockHeader struct { state protoimpl.MessageState `protogen:"open.v1"` + LaneId *LaneID `protobuf:"bytes,5,opt,name=lane_id,json=laneId,proto3,oneof" json:"lane_id,omitempty"` // required BlockNumber *uint64 `protobuf:"varint,2,opt,name=block_number,json=blockNumber,proto3,oneof" json:"block_number,omitempty"` // required ParentHash []byte `protobuf:"bytes,3,opt,name=parent_hash,json=parentHash,proto3,oneof" json:"parent_hash,omitempty"` // required PayloadHash []byte `protobuf:"bytes,4,opt,name=payload_hash,json=payloadHash,proto3,oneof" json:"payload_hash,omitempty"` // required - LaneId *LaneID `protobuf:"bytes,5,opt,name=lane_id,json=laneId,proto3,oneof" json:"lane_id,omitempty"` // required unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -567,6 +567,13 @@ func (*BlockHeader) Descriptor() ([]byte, []int) { return file_autobahn_autobahn_proto_rawDescGZIP(), []int{9} } +func (x *BlockHeader) GetLaneId() *LaneID { + if x != nil { + return x.LaneId + } + return nil +} + func (x *BlockHeader) GetBlockNumber() uint64 { if x != nil && x.BlockNumber != nil { return *x.BlockNumber @@ -588,13 +595,6 @@ func (x *BlockHeader) GetPayloadHash() []byte { return nil } -func (x *BlockHeader) GetLaneId() *LaneID { - if x != nil { - return x.LaneId - } - return nil -} - type Payload struct { state protoimpl.MessageState `protogen:"open.v1"` CreatedAt *Timestamp `protobuf:"bytes,1,opt,name=created_at,json=createdAt,proto3,oneof" json:"created_at,omitempty"` // required @@ -769,10 +769,10 @@ func (x *LaneQC) GetSigs() []*Signature { type LaneRange struct { state protoimpl.MessageState `protogen:"open.v1"` + LaneId *LaneID `protobuf:"bytes,5,opt,name=lane_id,json=laneId,proto3,oneof" json:"lane_id,omitempty"` // required First *uint64 `protobuf:"varint,2,opt,name=first,proto3,oneof" json:"first,omitempty"` // required Next *uint64 `protobuf:"varint,3,opt,name=next,proto3,oneof" json:"next,omitempty"` // required LastHash []byte `protobuf:"bytes,4,opt,name=last_hash,json=lastHash,proto3,oneof" json:"last_hash,omitempty"` // required - LaneId *LaneID `protobuf:"bytes,5,opt,name=lane_id,json=laneId,proto3,oneof" json:"lane_id,omitempty"` // required unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -807,6 +807,13 @@ func (*LaneRange) Descriptor() ([]byte, []int) { return file_autobahn_autobahn_proto_rawDescGZIP(), []int{13} } +func (x *LaneRange) GetLaneId() *LaneID { + if x != nil { + return x.LaneId + } + return nil +} + func (x *LaneRange) GetFirst() uint64 { if x != nil && x.First != nil { return *x.First @@ -828,13 +835,6 @@ func (x *LaneRange) GetLastHash() []byte { return nil } -func (x *LaneRange) GetLaneId() *LaneID { - if x != nil { - return x.LaneId - } - return nil -} - type View struct { state protoimpl.MessageState `protogen:"open.v1"` Index *uint64 `protobuf:"varint,1,opt,name=index,proto3,oneof" json:"index,omitempty"` // required @@ -2284,17 +2284,17 @@ const file_autobahn_autobahn_proto_rawDesc = "" + "\x03sig\x18\x02 \x01(\fB\x06؈\xe2\xab\f@H\x01R\x03sig\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\x06\n" + "\x04_keyB\x06\n" + "\x04_sig\"\x9b\x02\n" + - "\vBlockHeader\x12&\n" + - "\fblock_number\x18\x02 \x01(\x04H\x00R\vblockNumber\x88\x01\x01\x12,\n" + - "\vparent_hash\x18\x03 \x01(\fB\x06؈\xe2\xab\f H\x01R\n" + + "\vBlockHeader\x12.\n" + + "\alane_id\x18\x05 \x01(\v2\x10.autobahn.LaneIDH\x00R\x06laneId\x88\x01\x01\x12&\n" + + "\fblock_number\x18\x02 \x01(\x04H\x01R\vblockNumber\x88\x01\x01\x12,\n" + + "\vparent_hash\x18\x03 \x01(\fB\x06؈\xe2\xab\f H\x02R\n" + "parentHash\x88\x01\x01\x12.\n" + - "\fpayload_hash\x18\x04 \x01(\fB\x06؈\xe2\xab\f H\x02R\vpayloadHash\x88\x01\x01\x12.\n" + - "\alane_id\x18\x05 \x01(\v2\x10.autobahn.LaneIDH\x03R\x06laneId\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\x0f\n" + + "\fpayload_hash\x18\x04 \x01(\fB\x06؈\xe2\xab\f H\x03R\vpayloadHash\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\n" + + "\n" + + "\b_lane_idB\x0f\n" + "\r_block_numberB\x0e\n" + "\f_parent_hashB\x0f\n" + - "\r_payload_hashB\n" + - "\n" + - "\b_lane_idJ\x04\b\x01\x10\x02R\x04lane\"\xd5\x02\n" + + "\r_payload_hashJ\x04\b\x01\x10\x02R\x04lane\"\xd5\x02\n" + "\aPayload\x127\n" + "\n" + "created_at\x18\x01 \x01(\v2\x13.autobahn.TimestampH\x00R\tcreatedAt\x88\x01\x01\x12-\n" + @@ -2314,17 +2314,17 @@ const file_autobahn_autobahn_proto_rawDesc = "" + "\x06LaneQC\x12)\n" + "\x04vote\x18\x01 \x01(\v2\x15.autobahn.BlockHeaderR\x04vote\x12/\n" + "\x04sigs\x18\x02 \x03(\v2\x13.autobahn.SignatureB\x06Ј\xe2\xab\fdR\x04sigs:\x06\xe8\x88\xe2\xab\f\x01\"\xe0\x01\n" + - "\tLaneRange\x12\x19\n" + - "\x05first\x18\x02 \x01(\x04H\x00R\x05first\x88\x01\x01\x12\x17\n" + - "\x04next\x18\x03 \x01(\x04H\x01R\x04next\x88\x01\x01\x12(\n" + - "\tlast_hash\x18\x04 \x01(\fB\x06؈\xe2\xab\f H\x02R\blastHash\x88\x01\x01\x12.\n" + - "\alane_id\x18\x05 \x01(\v2\x10.autobahn.LaneIDH\x03R\x06laneId\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\b\n" + + "\tLaneRange\x12.\n" + + "\alane_id\x18\x05 \x01(\v2\x10.autobahn.LaneIDH\x00R\x06laneId\x88\x01\x01\x12\x19\n" + + "\x05first\x18\x02 \x01(\x04H\x01R\x05first\x88\x01\x01\x12\x17\n" + + "\x04next\x18\x03 \x01(\x04H\x02R\x04next\x88\x01\x01\x12(\n" + + "\tlast_hash\x18\x04 \x01(\fB\x06؈\xe2\xab\f H\x03R\blastHash\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\n" + + "\n" + + "\b_lane_idB\b\n" + "\x06_firstB\a\n" + "\x05_nextB\f\n" + "\n" + - "_last_hashB\n" + - "\n" + - "\b_lane_idJ\x04\b\x01\x10\x02R\x04lane\"\x97\x01\n" + + "_last_hashJ\x04\b\x01\x10\x02R\x04lane\"\x97\x01\n" + "\x04View\x12\x19\n" + "\x05index\x18\x01 \x01(\x04H\x00R\x05index\x88\x01\x01\x12\x1b\n" + "\x06number\x18\x02 \x01(\x04H\x01R\x06number\x88\x01\x01\x12$\n" + diff --git a/sei-tendermint/internal/autobahn/pb/autobahn.wireguard.go b/sei-tendermint/internal/autobahn/pb/autobahn.wireguard.go index d60d837174..4666ab25af 100644 --- a/sei-tendermint/internal/autobahn/pb/autobahn.wireguard.go +++ b/sei-tendermint/internal/autobahn/pb/autobahn.wireguard.go @@ -179,10 +179,10 @@ func init() { // Register the wireguard.Schema generated for autobahn.BlockHeader. runtime.MustRegister[*BlockHeader](runtime.Schema{ + 5: {MaxCount: 1, Nested: utils.Some(reflect.TypeFor[*LaneID]())}, 2: {MaxCount: 1}, 3: {MaxCount: 1, MaxSize: 32}, 4: {MaxCount: 1, MaxSize: 32}, - 5: {MaxCount: 1, Nested: utils.Some(reflect.TypeFor[*LaneID]())}, }) // Register the wireguard.Schema generated for autobahn.Payload. @@ -207,10 +207,10 @@ func init() { // Register the wireguard.Schema generated for autobahn.LaneRange. runtime.MustRegister[*LaneRange](runtime.Schema{ + 5: {MaxCount: 1, Nested: utils.Some(reflect.TypeFor[*LaneID]())}, 2: {MaxCount: 1}, 3: {MaxCount: 1}, 4: {MaxCount: 1, MaxSize: 32}, - 5: {MaxCount: 1, Nested: utils.Some(reflect.TypeFor[*LaneID]())}, }) // Register the wireguard.Schema generated for autobahn.View. From 169d08c19bab81f9901e494fc5e2952aa0003837 Mon Sep 17 00:00:00 2001 From: Wen Date: Mon, 10 Aug 2026 18:33:26 -0700 Subject: [PATCH 08/14] refactor(autobahn): make LaneID a plain value with Joined field Address review: drop ReadOnly/getters so LaneID is passed by value with public fields, and rename e_join to joined across types and proto. Co-authored-by: Cursor --- sei-tendermint/autobahn/types/committee.go | 77 +++++++++++-------- .../autobahn/types/committee_activate_test.go | 14 ++-- sei-tendermint/autobahn/types/lane_id.go | 47 ++++++----- sei-tendermint/autobahn/types/testonly.go | 6 +- .../autobahn/types/wireguard_test.go | 2 +- .../internal/autobahn/autobahn.proto | 4 +- .../internal/autobahn/avail/conv_test.go | 2 +- .../internal/autobahn/avail/inner.go | 6 +- .../internal/autobahn/avail/inner_test.go | 28 +++---- .../internal/autobahn/avail/state.go | 10 +-- .../internal/autobahn/avail/state_test.go | 14 ++-- .../internal/autobahn/epoch/registry.go | 4 +- .../internal/autobahn/pb/autobahn.pb.go | 20 ++--- .../protoutils/alloc_scan_load_test.go | 2 +- 14 files changed, 124 insertions(+), 112 deletions(-) diff --git a/sei-tendermint/autobahn/types/committee.go b/sei-tendermint/autobahn/types/committee.go index 1739ef4a79..dca4e24567 100644 --- a/sei-tendermint/autobahn/types/committee.go +++ b/sei-tendermint/autobahn/types/committee.go @@ -22,14 +22,13 @@ func (s ImSlice[T]) At(i int) T { return s.s[i] } func (s ImSlice[T]) All() iter.Seq[T] { return slices.Values(s.s) } // Committee represents the consensus committee. -// Lanes carry membership (validator + e_join); weights are voting stake. +// Lanes carry membership (validator + joined); weights are voting stake. // -// Members are totally ordered for Leader/EvmShard lottery and tipcut header -// concatenation. Replicas order by PublicKey; lanes by LaneID.Compare -// (validator, then e_join). With one lane per validator those coincide, so -// walking Lanes() is walking replica order. +// Membership order is replica order (PublicKey.Compare). Leader/EvmShard and +// tipcut header concatenation walk that order. Lanes() follows the same order +// (one LaneID per replica); LaneID.Compare is for sorting lane lists elsewhere. type Committee struct { - lanes ImSlice[LaneID] // sorted by LaneID.Compare; one per member + lanes ImSlice[LaneID] // in Replicas() order; one per member byValidator map[PublicKey]LaneID weights map[PublicKey]uint64 totalWeight uint64 @@ -43,8 +42,8 @@ func (c *Committee) HasReplica(k PublicKey) bool { } func (c *Committee) HasLane(l LaneID) bool { - got, ok := c.byValidator[l.validator] - return ok && got.eJoin == l.eJoin + got, ok := c.byValidator[l.Validator] + return ok && got.Joined == l.Joined } func (c *Committee) Lane(v PublicKey) utils.Option[LaneID] { @@ -55,11 +54,22 @@ func (c *Committee) Lane(v PublicKey) utils.Option[LaneID] { return utils.Some(lane) } -// Lanes returns members in LaneID order (see Committee). +// Replicas yields validators in PublicKey order (membership order). +func (c *Committee) Replicas() iter.Seq[PublicKey] { + return func(yield func(PublicKey) bool) { + for lane := range c.lanes.All() { + if !yield(lane.Validator) { + return + } + } + } +} + +// Lanes returns each replica's LaneID in Replicas() order. func (c *Committee) Lanes() ImSlice[LaneID] { return c.lanes } // Deterministic random oracle selecting a replica with probability proportional to the weight. -// Walks Lanes() so seed → PublicKey is network-wide deterministic (see Committee). +// Walks membership (Replicas) order so seed → PublicKey is network-wide deterministic. func (c *Committee) randomReplica(seed []byte) PublicKey { h := sha256.Sum256(seed[:]) var x, total uint256.Int @@ -67,10 +77,10 @@ func (c *Committee) randomReplica(seed []byte) PublicKey { total.SetUint64(c.totalWeight) y := x.Mod(&x, &total).Uint64() // TODO(gprusak): this can be optimized to O(1) lookup - for lane := range c.lanes.All() { - w := c.weights[lane.validator] + for k := range c.Replicas() { + w := c.weights[k] if y < w { - return lane.validator + return k } y -= w } @@ -129,7 +139,7 @@ func (c *Committee) LaneQuorum() uint64 { return c.Faulty() + 1 } -// NewCommittee is genesis: e_join = 0 for every member. +// NewCommittee is genesis: joined = 0 for every member. func NewCommittee(weights map[PublicKey]uint64) (*Committee, error) { weights, totalWeight, err := normalizeWeights(weights) if err != nil { @@ -139,13 +149,14 @@ func NewCommittee(weights map[PublicKey]uint64) (*Committee, error) { for v := range weights { lanes = append(lanes, NewLaneID(v, 0)) } - return finalizeCommittee(lanes, weights, totalWeight) + return newCommittee(lanes, weights, totalWeight) } -// ActivateCommittee for epoch e>0: copy e_join from prev on stay, stamp e on join. -func ActivateCommittee(prev *Committee, weights map[PublicKey]uint64, e EpochIndex) (*Committee, error) { +// DeriveNext builds the committee for epoch e>0 from this committee: +// copy joined on stay, stamp e on join. EpochIndex stays on Epoch, not Committee. +func (c *Committee) DeriveNext(weights map[PublicKey]uint64, e EpochIndex) (*Committee, error) { if e == 0 { - return nil, errors.New("ActivateCommittee: epoch must be > 0") + return nil, errors.New("DeriveNext: epoch must be > 0") } weights, totalWeight, err := normalizeWeights(weights) if err != nil { @@ -153,9 +164,9 @@ func ActivateCommittee(prev *Committee, weights map[PublicKey]uint64, e EpochInd } lanes := make([]LaneID, 0, len(weights)) for v := range weights { - lanes = append(lanes, prev.Lane(v).Or(NewLaneID(v, e))) + lanes = append(lanes, c.Lane(v).Or(NewLaneID(v, e))) } - return finalizeCommittee(lanes, weights, totalWeight) + return newCommittee(lanes, weights, totalWeight) } // normalizeWeights clones weights, drops zero entries, and returns the filtered @@ -182,23 +193,27 @@ func normalizeWeights(weights map[PublicKey]uint64) (map[PublicKey]uint64, uint6 return weights, totalWeight, nil } -// finalizeCommittee sorts lanes and rejects duplicate validators (multiple e_join). -func finalizeCommittee(lanes []LaneID, weights map[PublicKey]uint64, totalWeight uint64) (*Committee, error) { - slices.SortFunc(lanes, LaneID.Compare) - for i := 1; i < len(lanes); i++ { - if lanes[i].validator == lanes[i-1].validator { +// newCommittee rejects duplicate validators, orders replicas by PublicKey, +// and stores lanes in that same order (one LaneID per replica). +func newCommittee(lanes []LaneID, weights map[PublicKey]uint64, totalWeight uint64) (*Committee, error) { + byValidator := make(map[PublicKey]LaneID, len(lanes)) + for _, lane := range lanes { + if _, ok := byValidator[lane.Validator]; ok { return nil, fmt.Errorf( - "duplicate validator in committee lanes: %q with e_join %d and %d", - lanes[i].validator, lanes[i-1].eJoin, lanes[i].eJoin, + "duplicate validator in committee lanes: %q with joined %d and %d", + lane.Validator, byValidator[lane.Validator].Joined, lane.Joined, ) } + byValidator[lane.Validator] = lane } - byValidator := make(map[PublicKey]LaneID, len(lanes)) - for _, lane := range lanes { - byValidator[lane.validator] = lane + replicas := slices.Collect(maps.Keys(byValidator)) + slices.SortFunc(replicas, PublicKey.Compare) + ordered := make([]LaneID, len(replicas)) + for i, v := range replicas { + ordered[i] = byValidator[v] } return &Committee{ - lanes: ImSlice[LaneID]{lanes}, + lanes: ImSlice[LaneID]{ordered}, byValidator: byValidator, weights: weights, totalWeight: totalWeight, diff --git a/sei-tendermint/autobahn/types/committee_activate_test.go b/sei-tendermint/autobahn/types/committee_activate_test.go index e94e4bf554..3eb8043b71 100644 --- a/sei-tendermint/autobahn/types/committee_activate_test.go +++ b/sei-tendermint/autobahn/types/committee_activate_test.go @@ -7,7 +7,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" ) -func TestActivateCommittee_StayLeaveRejoin(t *testing.T) { +func TestDeriveNext_StayLeaveRejoin(t *testing.T) { rng := utils.TestRng() a := GenSecretKey(rng).Public() b := GenSecretKey(rng).Public() @@ -31,8 +31,8 @@ func TestActivateCommittee_StayLeaveRejoin(t *testing.T) { require.False(t, c0.HasLane(NewLaneID(c, 0))) requireLanesSorted(t, c0) - // Epoch 1: A,B,D stay → copy e_join=0. - c1, err := ActivateCommittee(c0, map[PublicKey]uint64{a: 1, b: 1, d: 1}, 1) + // Epoch 1: A,B,D stay → copy joined=0. + c1, err := c0.DeriveNext(map[PublicKey]uint64{a: 1, b: 1, d: 1}, 1) require.NoError(t, err) require.Equal(t, NewLaneID(a, 0), c1.Lane(a).OrPanic("a")) require.Equal(t, NewLaneID(b, 0), c1.Lane(b).OrPanic("b")) @@ -40,7 +40,7 @@ func TestActivateCommittee_StayLeaveRejoin(t *testing.T) { requireLanesSorted(t, c1) // Epoch 2: B,D leave; C joins. A stays. - c2, err := ActivateCommittee(c1, map[PublicKey]uint64{a: 1, c: 1}, 2) + c2, err := c1.DeriveNext(map[PublicKey]uint64{a: 1, c: 1}, 2) require.NoError(t, err) require.Equal(t, NewLaneID(a, 0), c2.Lane(a).OrPanic("a")) require.Equal(t, NewLaneID(c, 2), c2.Lane(c).OrPanic("c")) @@ -50,7 +50,7 @@ func TestActivateCommittee_StayLeaveRejoin(t *testing.T) { requireLanesSorted(t, c2) // Epoch 3: D rejoins; C and A stay. - c3, err := ActivateCommittee(c2, map[PublicKey]uint64{a: 1, c: 1, d: 1}, 3) + c3, err := c2.DeriveNext(map[PublicKey]uint64{a: 1, c: 1, d: 1}, 3) require.NoError(t, err) require.Equal(t, NewLaneID(a, 0), c3.Lane(a).OrPanic("a")) require.Equal(t, NewLaneID(c, 2), c3.Lane(c).OrPanic("c")) @@ -59,10 +59,10 @@ func TestActivateCommittee_StayLeaveRejoin(t *testing.T) { requireLanesSorted(t, c3) } -func TestFinalizeCommittee_RejectsDuplicatePubKeyDifferentEJoin(t *testing.T) { +func TestFinalizeCommittee_RejectsDuplicatePubKeyDifferentJoined(t *testing.T) { rng := utils.TestRng() v := GenSecretKey(rng).Public() - _, err := finalizeCommittee( + _, err := newCommittee( []LaneID{NewLaneID(v, 0), NewLaneID(v, 1)}, map[PublicKey]uint64{v: 1}, 1, diff --git a/sei-tendermint/autobahn/types/lane_id.go b/sei-tendermint/autobahn/types/lane_id.go index ce7f2df231..34dd83db55 100644 --- a/sei-tendermint/autobahn/types/lane_id.go +++ b/sei-tendermint/autobahn/types/lane_id.go @@ -13,57 +13,54 @@ import ( ) // LaneID identifies a validator's continuous committee membership streak. -// e_join is the epoch in which that streak began. +// Joined is the epoch in which that streak began. // // Identity rules: stay keeps the same LaneID; leave ends that identity; rejoin // allocates a new LaneID (typically with tip 0). Avail map retention and tipEpoch // dispose live in package avail (see its package doc). +// +// LaneID is a plain value type (passed by value); fields are public by design. type LaneID struct { - utils.ReadOnly - validator PublicKey - eJoin EpochIndex + Validator PublicKey + Joined EpochIndex } -func NewLaneID(validator PublicKey, eJoin EpochIndex) LaneID { - return LaneID{validator: validator, eJoin: eJoin} +func NewLaneID(validator PublicKey, joined EpochIndex) LaneID { + return LaneID{Validator: validator, Joined: joined} } -func (l LaneID) Validator() PublicKey { return l.validator } - -func (l LaneID) EJoin() EpochIndex { return l.eJoin } - -// Compare orders by validator, then e_join. +// Compare orders by validator, then joined. func (l LaneID) Compare(other LaneID) int { return cmp.Or( - l.validator.Compare(other.validator), - cmp.Compare(l.eJoin, other.eJoin), + l.Validator.Compare(other.Validator), + cmp.Compare(l.Joined, other.Joined), ) } -// Bytes returns a stable encoding: pubkey bytes || big-endian e_join. +// Bytes returns a stable encoding: pubkey bytes || big-endian joined. func (l LaneID) Bytes() []byte { - vb := l.validator.Bytes() + vb := l.Validator.Bytes() b := make([]byte, 0, len(vb)+8) b = append(b, vb...) - return binary.BigEndian.AppendUint64(b, uint64(l.eJoin)) + return binary.BigEndian.AppendUint64(b, uint64(l.Joined)) } -// LaneIDFromBytes parses Bytes() encoding (exactly ed25519 pubkey || u64be e_join). +// LaneIDFromBytes parses Bytes() encoding (exactly ed25519 pubkey || u64be joined). func LaneIDFromBytes(b []byte) (LaneID, error) { want := ed25519.PublicKeySize + 8 if len(b) != want { return LaneID{}, fmt.Errorf("LaneID: got %d bytes, want %d", len(b), want) } - eJoin := EpochIndex(binary.BigEndian.Uint64(b[ed25519.PublicKeySize:])) + joined := EpochIndex(binary.BigEndian.Uint64(b[ed25519.PublicKeySize:])) validator, err := PublicKeyFromBytes(b[:ed25519.PublicKeySize]) if err != nil { return LaneID{}, fmt.Errorf("LaneID validator: %w", err) } - return NewLaneID(validator, eJoin), nil + return NewLaneID(validator, joined), nil } func (l LaneID) String() string { - return fmt.Sprintf("%s@e%d", l.validator.String(), l.eJoin) + return fmt.Sprintf("%s@e%d", l.Validator.String(), l.Joined) } func (l LaneID) HexString() string { return hex.EncodeToString(l.Bytes()) } @@ -71,8 +68,8 @@ func (l LaneID) HexString() string { return hex.EncodeToString(l.Bytes()) } var LaneIDConv = protoutils.Conv[LaneID, *pb.LaneID]{ Encode: func(l LaneID) *pb.LaneID { return &pb.LaneID{ - Validator: PublicKeyConv.Encode(l.validator), - EJoin: utils.Alloc(uint64(l.eJoin)), + Validator: PublicKeyConv.Encode(l.Validator), + Joined: utils.Alloc(uint64(l.Joined)), } }, Decode: func(p *pb.LaneID) (LaneID, error) { @@ -80,9 +77,9 @@ var LaneIDConv = protoutils.Conv[LaneID, *pb.LaneID]{ if err != nil { return LaneID{}, fmt.Errorf("validator: %w", err) } - if p.EJoin == nil { - return LaneID{}, fmt.Errorf("e_join: missing") + if p.Joined == nil { + return LaneID{}, fmt.Errorf("joined: missing") } - return NewLaneID(validator, EpochIndex(*p.EJoin)), nil + return NewLaneID(validator, EpochIndex(*p.Joined)), nil }, } diff --git a/sei-tendermint/autobahn/types/testonly.go b/sei-tendermint/autobahn/types/testonly.go index 0129b1f5d4..0c8e0a2e03 100644 --- a/sei-tendermint/autobahn/types/testonly.go +++ b/sei-tendermint/autobahn/types/testonly.go @@ -74,7 +74,7 @@ func GenSecretKey(rng utils.Rng) SecretKey { } // GenCommittee generates a random Committee of the given size. -// Each member gets an independent random e_join (via GenEpochIndex). +// Each member gets an independent random joined (via GenEpochIndex). // Returns the generated secret keys as well. func GenCommittee(rng utils.Rng, size int) (*Committee, []SecretKey) { sks := utils.GenSliceN(rng, size, GenSecretKey) @@ -95,7 +95,7 @@ func GenCommittee(rng utils.Rng, size int) (*Committee, []SecretKey) { for _, v := range vs { lanes = append(lanes, NewLaneID(v, GenEpochIndex(rng))) } - return utils.OrPanic1(finalizeCommittee(lanes, weights, total)), sks + return utils.OrPanic1(newCommittee(lanes, weights, total)), sks } // TestKeysWithWeight returns a deterministic subset of keys whose committee weight reaches the requested threshold. @@ -119,7 +119,7 @@ func TestSecretKey(nodeID NodeID) SecretKey { return SecretKey{key: ed25519.TestSecretKey([]byte(nodeID))} } -// GenLaneID generates a random LaneID (random validator, random e_join). +// GenLaneID generates a random LaneID (random validator, random joined). func GenLaneID(rng utils.Rng) LaneID { return NewLaneID(TestSecretKey(GenNodeID(rng)).Public(), GenEpochIndex(rng)) } diff --git a/sei-tendermint/autobahn/types/wireguard_test.go b/sei-tendermint/autobahn/types/wireguard_test.go index e389917c0b..312024ebe7 100644 --- a/sei-tendermint/autobahn/types/wireguard_test.go +++ b/sei-tendermint/autobahn/types/wireguard_test.go @@ -191,7 +191,7 @@ func TestFullProposalWireguardAcceptsMaxValidators(t *testing.T) { rng := utils.TestRng() laneQCs := map[LaneID]*LaneQC{} for lane := range committee.Lanes().All() { - key := secretKeyFor(keys, lane.Validator()) + key := secretKeyFor(keys, lane.Validator) vote := NewLaneVote(NewBlock(lane, 0, GenBlockHeaderHash(rng), GenPayload(rng)).Header()) laneQCs[lane] = NewLaneQC([]*Signed[*LaneVote]{Sign(key, vote)}) } diff --git a/sei-tendermint/internal/autobahn/autobahn.proto b/sei-tendermint/internal/autobahn/autobahn.proto index 9ffb22a1fb..f4c28ac4b2 100644 --- a/sei-tendermint/internal/autobahn/autobahn.proto +++ b/sei-tendermint/internal/autobahn/autobahn.proto @@ -63,12 +63,12 @@ message PublicKey { } // LaneID identifies a validator's lane for a continuous committee membership -// streak. e_join is the epoch in which the validator most recently joined. +// streak. joined is the epoch in which the validator most recently joined. message LaneID { option (hashable.hashable) = true; option (wireguard.sized) = true; optional PublicKey validator = 1; // required - optional uint64 e_join = 2; // required + optional uint64 joined = 2; // required } message Signature { diff --git a/sei-tendermint/internal/autobahn/avail/conv_test.go b/sei-tendermint/internal/autobahn/avail/conv_test.go index be2cf6f85d..e65d994e70 100644 --- a/sei-tendermint/internal/autobahn/avail/conv_test.go +++ b/sei-tendermint/internal/autobahn/avail/conv_test.go @@ -14,7 +14,7 @@ func TestPruneAnchorConv(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane := types.NewLaneID(keys[0].Public(), 0) + lane := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") block := types.NewBlock(lane, 0, types.BlockHeaderHash{}, types.GenPayload(rng)) laneQCs := map[types.LaneID]*types.LaneQC{ lane: types.NewLaneQC(makeLaneVotes(keys, block.Header())), diff --git a/sei-tendermint/internal/autobahn/avail/inner.go b/sei-tendermint/internal/autobahn/avail/inner.go index d3ee834429..f4ed9185fb 100644 --- a/sei-tendermint/internal/autobahn/avail/inner.go +++ b/sei-tendermint/internal/autobahn/avail/inner.go @@ -86,8 +86,8 @@ func newInner(registry *epoch.Registry, loaded utils.Option[*loadedAvailState]) } // Re-attach persisted WALs before prune. Skip tip-stale leave WALs already - // disposable at the prune anchor. Live dispose uses e_join < tip; restart - // tipcut skip uses e_join <= tip because a leave tip may be unnamed in the + // disposable at the prune anchor. Live dispose uses joined < tip; restart + // tipcut skip uses joined <= tip because a leave tip may be unnamed in the // tipcut proposal while still disposable. Those LaneIDs never rejoin. var anchorEpoch types.EpochIndex var anchorCommittee *types.Committee @@ -100,7 +100,7 @@ func newInner(registry *epoch.Registry, loaded utils.Option[*loadedAvailState]) anchorCommittee = ep.Committee() } for lane := range l.blocks { - if anchorCommittee != nil && lane.EJoin() <= anchorEpoch && !anchorCommittee.HasLane(lane) { + if anchorCommittee != nil && lane.Joined <= anchorEpoch && !anchorCommittee.HasLane(lane) { continue } if _, ok := i.blocks[lane]; ok { diff --git a/sei-tendermint/internal/autobahn/avail/inner_test.go b/sei-tendermint/internal/autobahn/avail/inner_test.go index 9c95c58124..4a2dd154df 100644 --- a/sei-tendermint/internal/autobahn/avail/inner_test.go +++ b/sei-tendermint/internal/autobahn/avail/inner_test.go @@ -23,7 +23,7 @@ func TestPruneMismatchedIndices(t *testing.T) { registry, keys := epoch.GenRegistry(rng, 4) makeCommitQC := func(prev utils.Option[*types.CommitQC]) *types.CommitQC { - l := types.NewLaneID(keys[0].Public(), 0) + l := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") lr := types.LaneRangeOpt(prev, l) b := types.NewBlock(l, lr.Next(), lr.LastHash(), types.GenPayload(rng)) lqcs := map[types.LaneID]*types.LaneQC{ @@ -122,7 +122,7 @@ func TestNewInnerLoadedNoAnchor(t *testing.T) { func TestNewInnerLoadedBlocksContiguous(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane := types.NewLaneID(keys[0].Public(), 0) + lane := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") // Build 3 contiguous blocks: 0, 1, 2. var parent types.BlockHeaderHash @@ -160,7 +160,7 @@ func TestNewInnerLoadedBlocksContiguous(t *testing.T) { func TestNewInnerLoadedBlocksEmptySlice(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane := types.NewLaneID(keys[0].Public(), 0) + lane := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") loaded := &loadedAvailState{ blocks: map[types.LaneID][]persist.LoadedBlock{lane: {}}, @@ -200,8 +200,8 @@ func TestNewInnerLoadedBlocksUnknownLane(t *testing.T) { func TestNewInnerLoadedBlocksMultipleLanes(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane0 := types.NewLaneID(keys[0].Public(), 0) - lane1 := types.NewLaneID(keys[1].Public(), 0) + lane0 := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") + lane1 := registry.LatestEpoch().Committee().Lane(keys[1].Public()).OrPanic("keys[1]") var parent0 types.BlockHeaderHash var bs0 []persist.LoadedBlock @@ -331,7 +331,7 @@ func TestNewInnerLoadedCommitQCsWithAppQC(t *testing.T) { func TestNewInnerLoadedAllThree(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane := types.NewLaneID(keys[0].Public(), 0) + lane := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") // AppQC at road index 2. roadIdx := types.RoadIndex(2) @@ -394,7 +394,7 @@ func TestNewInnerLoadedAllThree(t *testing.T) { func TestPruneAdvancesNextBlockToPersist(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane := types.NewLaneID(keys[0].Public(), 0) + lane := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") i, err := newInner(registry, utils.None[*loadedAvailState]()) require.NoError(t, err) @@ -687,7 +687,7 @@ func TestNewInnerLoadedCommitQCsGapAfterAnchorReturnsError(t *testing.T) { func TestNewInnerLoadedBlocksGapReturnsError(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane := types.NewLaneID(keys[0].Public(), 0) + lane := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") // Blocks 3, 4, 6, 7 with no anchor — queue starts at 0, so block 3 // fails the contiguity check immediately (expected 0, got 3). @@ -711,7 +711,7 @@ func TestNewInnerLoadedBlocksGapReturnsError(t *testing.T) { func TestNewInnerLoadedBlocksParentHashMismatchReturnsError(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane := types.NewLaneID(keys[0].Public(), 0) + lane := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") // Build blocks 0, 1 with correct chaining, then block 2 with wrong parent. var parent types.BlockHeaderHash @@ -739,7 +739,7 @@ func TestNewInnerLoadedBlocksParentHashMismatchReturnsError(t *testing.T) { func TestNewInnerLoadedBlocksOverCapacityReturnsError(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 4) - lane := types.NewLaneID(keys[0].Public(), 0) + lane := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") // Build BlocksPerLane + 5 contiguous blocks — more than the lane capacity. // Since runtime enforces the capacity limit, exceeding it on disk indicates @@ -780,7 +780,7 @@ func TestNewInnerPruneAnchorPrunesBlockQueues(t *testing.T) { appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) pruneQC := qcs[2] - lane := types.NewLaneID(keys[0].Public(), 0) + lane := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") // Persist some blocks starting at the lane range for the prune CommitQC. lrFirst := pruneQC.LaneRange(lane).First() @@ -913,7 +913,7 @@ func TestNewInnerRestoresLeaveLaneNamedByAnchor(t *testing.T) { require.Equal(t, lrFirst+1, i.blocks[laneB].next) } -// With anchor epoch N, lanes with e_join <= N absent from that epoch's committee +// With anchor epoch N, lanes with joined <= N absent from that epoch's committee // are skipped (left for good; orphan WAL dirs may remain unused on disk). func TestNewInnerSkipsStaleLaneAbsentFromAnchor(t *testing.T) { rng := utils.TestRng() @@ -929,9 +929,9 @@ func TestNewInnerSkipsStaleLaneAbsentFromAnchor(t *testing.T) { qc1 := makeCommitQC(ep1, []types.SecretKey{a, cKey}, utils.None[*types.CommitQC](), nil, utils.None[*types.AppQC]()) require.Equal(t, types.EpochIndex(1), qc1.Proposal().EpochIndex()) - // e_join == N covers the <= bound (not only e_join < N). + // joined == N covers the <= bound (not only joined < N). orphan := types.NewLaneID(types.GenSecretKey(rng).Public(), qc1.Proposal().EpochIndex()) - require.Equal(t, orphan.EJoin(), qc1.Proposal().EpochIndex()) + require.Equal(t, orphan.Joined, qc1.Proposal().EpochIndex()) require.False(t, ep1.Committee().HasLane(orphan)) app1 := types.NewAppProposal(qc1.GlobalRange().First, qc1.Index(), types.GenAppHash(rng), 1) diff --git a/sei-tendermint/internal/autobahn/avail/state.go b/sei-tendermint/internal/autobahn/avail/state.go index 8265a49d23..a9f7d9d6e3 100644 --- a/sei-tendermint/internal/autobahn/avail/state.go +++ b/sei-tendermint/internal/autobahn/avail/state.go @@ -8,7 +8,7 @@ // - Join/stay: ensured at ApplyEpoch (addCommitteeLanes, then Store epoch). // - Leave: maps remain until tipEpoch (epoch of the first retained CommitQC) // omits the LaneID, then DeleteLane + map drop on the same persist tick. -// - Dispose: e_join < tipEpoch && !tipCommittee.HasLane(lane). +// - Dispose: joined < tipEpoch && !tipCommittee.HasLane(lane). // // Subscribe binds LocalLane at subscribe time and serves leave maps until // dispose → ErrLanePruned. Produce sessions use WaitProduce / WaitMustStop @@ -130,14 +130,14 @@ func tipEpochOf(inner *inner, registry *epoch.Registry) (utils.Option[*types.Epo return utils.Some(ep), nil } -// staleLaneDisposable: tipEpoch omits lane and e_join < tip (joiners at/after tip stay). +// staleLaneDisposable: tipEpoch omits lane and joined < tip (joiners at/after tip stay). // None tipEpoch → false. func staleLaneDisposable(lane types.LaneID, tipEpoch utils.Option[*types.Epoch]) bool { ep, ok := tipEpoch.Get() if !ok { return false } - return lane.EJoin() < ep.EpochIndex() && !ep.Committee().HasLane(lane) + return lane.Joined < ep.EpochIndex() && !ep.Committee().HasLane(lane) } // deleteStaleLaneWAL Deletes WALs for tip-stale leave maps. @@ -594,7 +594,7 @@ func (s *State) Block(ctx context.Context, lane types.LaneID, n types.BlockNumbe // Returns ErrBadLane if tipEpoch drop removes the lane map while waiting. func (s *State) PushBlock(ctx context.Context, p *types.Signed[*types.LaneProposal]) error { h := p.Msg().Block().Header() - if p.Key() != h.Lane().Validator() { + if p.Key() != h.Lane().Validator { return fmt.Errorf("signer %v does not match lane %v", p.Key(), h.Lane()) } if _, err := s.data.Registry().VerifyInWindow(func(c *types.Committee) error { @@ -813,7 +813,7 @@ func (s *State) WaitForLaneQCs( // ProduceLocalBlock appends block n on the WaitProduce session lane. func (s *State) ProduceLocalBlock(lane types.LaneID, n types.BlockNumber, payload *types.Payload) (*types.Signed[*types.LaneProposal], error) { - if s.key.Public() != lane.Validator() { + if s.key.Public() != lane.Validator { return nil, ErrBadLane } var result *types.Signed[*types.LaneProposal] diff --git a/sei-tendermint/internal/autobahn/avail/state_test.go b/sei-tendermint/internal/autobahn/avail/state_test.go index d954114a24..1c420f8815 100644 --- a/sei-tendermint/internal/autobahn/avail/state_test.go +++ b/sei-tendermint/internal/autobahn/avail/state_test.go @@ -408,7 +408,7 @@ func TestStateMismatchedQCs(t *testing.T) { } // 1. Produce a block so we have a non-empty range - lane := types.NewLaneID(keys[0].Public(), 0) + lane := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") p := types.GenPayload(rng) b, err := state.ProduceLocalBlock(lane, state.NextBlock(lane), p) require.NoError(t, err) @@ -444,7 +444,7 @@ func TestPushBlockRejectsBadParentHash(t *testing.T) { state := utils.OrPanic1(NewState(keys[0], ds, utils.Some(t.TempDir()))) // Produce a valid first block on our lane. - lane := types.NewLaneID(keys[0].Public(), 0) + lane := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") _, err := state.ProduceLocalBlock(lane, state.NextBlock(lane), types.GenPayload(rng)) require.NoError(t, err) @@ -467,7 +467,7 @@ func TestPushBlockRejectsWrongSigner(t *testing.T) { state := utils.OrPanic1(NewState(keys[0], ds, utils.Some(t.TempDir()))) // Create a block on keys[0]'s lane but sign it with keys[1]. - lane := types.NewLaneID(keys[0].Public(), 0) + lane := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") block := types.NewBlock(lane, 0, types.GenBlockHeaderHash(rng), types.GenPayload(rng)) prop := types.Sign(keys[1], types.NewLaneProposal(block)) @@ -540,7 +540,7 @@ func TestNewStateWithPersistence(t *testing.T) { t.Run("loads persisted blocks", func(t *testing.T) { dir := t.TempDir() ds := newTestDataState(&data.Config{Registry: registry}) - lane := types.NewLaneID(keys[0].Public(), 0) + lane := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") // Persist blocks using BlockPersister. bp, _, err := persist.NewBlockPersister(utils.Some(dir)) @@ -567,7 +567,7 @@ func TestNewStateWithPersistence(t *testing.T) { t.Run("loads persisted AppQC and blocks together", func(t *testing.T) { dir := t.TempDir() ds := newTestDataState(&data.Config{Registry: registry}) - lane := types.NewLaneID(keys[0].Public(), 0) + lane := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") roadIdx := types.RoadIndex(2) globalNum := types.GlobalBlockNumber(5) @@ -771,7 +771,7 @@ func TestNewStateWithPersistence(t *testing.T) { t.Run("anchor past all persisted blocks truncates lane WAL", func(t *testing.T) { dir := t.TempDir() ds := newTestDataState(&data.Config{Registry: registry}) - lane := types.NewLaneID(keys[0].Public(), 0) + lane := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") // Persist commitQCs 0-9 and blocks 0-2 for one lane. qcs := make([]*types.CommitQC, 10) @@ -843,7 +843,7 @@ func TestNewStateWithPersistence(t *testing.T) { t.Run("failed NewState releases WAL locks", func(t *testing.T) { dir := t.TempDir() ds := newTestDataState(&data.Config{Registry: registry}) - lane := types.NewLaneID(keys[0].Public(), 0) + lane := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") // Seed one lane so the failing NewState below has a lane WAL to leak, then release the seeder. bp, _, err := persist.NewBlockPersister(utils.Some(dir)) diff --git a/sei-tendermint/internal/autobahn/epoch/registry.go b/sei-tendermint/internal/autobahn/epoch/registry.go index 32c0cc63ff..3a13be168e 100644 --- a/sei-tendermint/internal/autobahn/epoch/registry.go +++ b/sei-tendermint/internal/autobahn/epoch/registry.go @@ -68,7 +68,7 @@ func (r *Registry) LatestEpoch() *types.Epoch { panic("unreachable") } -// ActivateEpoch appends latest+1 via ActivateCommittee. +// ActivateEpoch appends latest+1 via Committee.DeriveNext. // // Scaffolding for #3736: does not validate roads.First vs prior RoadRange; prior // range left as stored. Tests may pass OpenRoadRange() until multi-epoch roads wire up. @@ -84,7 +84,7 @@ func (r *Registry) ActivateEpoch( if _, exists := s.m[next]; exists { return nil, fmt.Errorf("epoch %d already exists", next) } - committee, err := types.ActivateCommittee(prev.Committee(), weights, next) + committee, err := prev.Committee().DeriveNext(weights, next) if err != nil { return nil, err } diff --git a/sei-tendermint/internal/autobahn/pb/autobahn.pb.go b/sei-tendermint/internal/autobahn/pb/autobahn.pb.go index 942d0ad008..0684ab2aca 100644 --- a/sei-tendermint/internal/autobahn/pb/autobahn.pb.go +++ b/sei-tendermint/internal/autobahn/pb/autobahn.pb.go @@ -422,11 +422,11 @@ func (x *PublicKey) GetEd25519() []byte { } // LaneID identifies a validator's lane for a continuous committee membership -// streak. e_join is the epoch in which the validator most recently joined. +// streak. joined is the epoch in which the validator most recently joined. type LaneID struct { state protoimpl.MessageState `protogen:"open.v1"` - Validator *PublicKey `protobuf:"bytes,1,opt,name=validator,proto3,oneof" json:"validator,omitempty"` // required - EJoin *uint64 `protobuf:"varint,2,opt,name=e_join,json=eJoin,proto3,oneof" json:"e_join,omitempty"` // required + Validator *PublicKey `protobuf:"bytes,1,opt,name=validator,proto3,oneof" json:"validator,omitempty"` // required + Joined *uint64 `protobuf:"varint,2,opt,name=joined,proto3,oneof" json:"joined,omitempty"` // required unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -468,9 +468,9 @@ func (x *LaneID) GetValidator() *PublicKey { return nil } -func (x *LaneID) GetEJoin() uint64 { - if x != nil && x.EJoin != nil { - return *x.EJoin +func (x *LaneID) GetJoined() uint64 { + if x != nil && x.Joined != nil { + return *x.Joined } return 0 } @@ -2272,13 +2272,13 @@ const file_autobahn_autobahn_proto_rawDesc = "" + "\tPublicKey\x12%\n" + "\aed25519\x18\x01 \x01(\fB\x06؈\xe2\xab\f H\x00R\aed25519\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\n" + "\n" + - "\b_ed25519\"\x83\x01\n" + + "\b_ed25519\"\x84\x01\n" + "\x06LaneID\x126\n" + - "\tvalidator\x18\x01 \x01(\v2\x13.autobahn.PublicKeyH\x00R\tvalidator\x88\x01\x01\x12\x1a\n" + - "\x06e_join\x18\x02 \x01(\x04H\x01R\x05eJoin\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\f\n" + + "\tvalidator\x18\x01 \x01(\v2\x13.autobahn.PublicKeyH\x00R\tvalidator\x88\x01\x01\x12\x1b\n" + + "\x06joined\x18\x02 \x01(\x04H\x01R\x06joined\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\f\n" + "\n" + "_validatorB\t\n" + - "\a_e_join\"t\n" + + "\a_joined\"t\n" + "\tSignature\x12*\n" + "\x03key\x18\x01 \x01(\v2\x13.autobahn.PublicKeyH\x00R\x03key\x88\x01\x01\x12\x1d\n" + "\x03sig\x18\x02 \x01(\fB\x06؈\xe2\xab\f@H\x01R\x03sig\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\x06\n" + diff --git a/sei-tendermint/internal/protoutils/alloc_scan_load_test.go b/sei-tendermint/internal/protoutils/alloc_scan_load_test.go index 60780de2f0..e81c136eb6 100644 --- a/sei-tendermint/internal/protoutils/alloc_scan_load_test.go +++ b/sei-tendermint/internal/protoutils/alloc_scan_load_test.go @@ -29,7 +29,7 @@ func maxBlock() *autopb.Block { Header: &autopb.BlockHeader{ LaneId: &autopb.LaneID{ Validator: &autopb.PublicKey{Ed25519: make([]byte, 32)}, - EJoin: proto.Uint64(0), + Joined: proto.Uint64(0), }, BlockNumber: proto.Uint64(1), ParentHash: make([]byte, 32), From e176193126195051c82903cfc16ca09cadad1af2 Mon Sep 17 00:00:00 2001 From: Wen Date: Mon, 10 Aug 2026 19:09:56 -0700 Subject: [PATCH 09/14] refactor(autobahn): clarify next-CommitQC epoch; rename WaitForLocalLane Pass nextCommitQCEpoch into newInner instead of implying LatestEpoch, and rename WaitProduce to WaitForLocalLane to match what it waits for. Co-authored-by: Cursor --- .../internal/autobahn/avail/inner.go | 9 ++- .../internal/autobahn/avail/inner_test.go | 55 ++++++++++--------- .../internal/autobahn/avail/state.go | 14 +++-- .../internal/autobahn/producer/state.go | 6 +- sei-tendermint/internal/p2p/giga/avail.go | 6 +- 5 files changed, 47 insertions(+), 43 deletions(-) diff --git a/sei-tendermint/internal/autobahn/avail/inner.go b/sei-tendermint/internal/autobahn/avail/inner.go index f4ed9185fb..05d8544737 100644 --- a/sei-tendermint/internal/autobahn/avail/inner.go +++ b/sei-tendermint/internal/autobahn/avail/inner.go @@ -14,6 +14,8 @@ import ( // Lane maps: joiners at ApplyEpoch; leavers until tipEpoch dispose (package doc). // Restart re-attaches leave WALs; tip-stale ones are skipped (tipcut). type inner struct { + // epoch is the epoch of the next CommitQC. ApplyEpoch advances it. + // It is not Registry.LatestEpoch: activation may be ahead of the next QC. epoch utils.AtomicSend[*types.Epoch] latestAppQC utils.Option[*types.AppQC] latestCommitQC utils.AtomicSend[utils.Option[*types.CommitQC]] @@ -58,8 +60,11 @@ type loadedAvailState struct { blocks map[types.LaneID][]persist.LoadedBlock } -func newInner(registry *epoch.Registry, loaded utils.Option[*loadedAvailState]) (*inner, error) { - ep := registry.LatestEpoch() +// newInner seeds lane maps from nextCommitQCEpoch's committee (the epoch of the +// next CommitQC), then re-attaches leave WALs from loaded that are still needed. +// nextCommitQCEpoch is not Registry.LatestEpoch — activation may be ahead. +func newInner(nextCommitQCEpoch *types.Epoch, registry *epoch.Registry, loaded utils.Option[*loadedAvailState]) (*inner, error) { + ep := nextCommitQCEpoch votes := map[types.LaneID]*queue[types.BlockNumber, blockVotes]{} blocks := map[types.LaneID]*queue[types.BlockNumber, *types.Signed[*types.LaneProposal]]{} for lane := range ep.Committee().Lanes().All() { diff --git a/sei-tendermint/internal/autobahn/avail/inner_test.go b/sei-tendermint/internal/autobahn/avail/inner_test.go index 4a2dd154df..d3bd6448ca 100644 --- a/sei-tendermint/internal/autobahn/avail/inner_test.go +++ b/sei-tendermint/internal/autobahn/avail/inner_test.go @@ -73,7 +73,7 @@ func TestNewInnerFreshStart(t *testing.T) { rng := utils.TestRng() registry, _ := epoch.GenRegistry(rng, 4) - i, err := newInner(registry, utils.None[*loadedAvailState]()) + i, err := newInner(registry.LatestEpoch(), registry, utils.None[*loadedAvailState]()) require.NoError(t, err) require.False(t, i.latestAppQC.IsPresent()) @@ -110,7 +110,7 @@ func TestNewInnerLoadedNoAnchor(t *testing.T) { loaded := &loadedAvailState{} - i, err := newInner(registry, utils.Some(loaded)) + i, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.NoError(t, err) // No anchor loaded, app votes should start at the registry's first block. @@ -137,7 +137,7 @@ func TestNewInnerLoadedBlocksContiguous(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - i, err := newInner(registry, utils.Some(loaded)) + i, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.NoError(t, err) q := i.blocks[lane] @@ -166,7 +166,7 @@ func TestNewInnerLoadedBlocksEmptySlice(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: {}}, } - i, err := newInner(registry, utils.Some(loaded)) + i, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.NoError(t, err) q := i.blocks[lane] @@ -186,7 +186,7 @@ func TestNewInnerLoadedBlocksUnknownLane(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{unknownLane: {{Number: 0, Proposal: b}}}, } - i, err := newInner(registry, utils.Some(loaded)) + i, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.NoError(t, err) for lane := range registry.LatestEpoch().Committee().Lanes().All() { @@ -223,7 +223,7 @@ func TestNewInnerLoadedBlocksMultipleLanes(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane0: bs0, lane1: bs1}, } - i, err := newInner(registry, utils.Some(loaded)) + i, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.NoError(t, err) q0 := i.blocks[lane0] @@ -260,7 +260,7 @@ func TestNewInnerLoadedCommitQCsNoAppQC(t *testing.T) { commitQCs: loadedQCs, } - inner, err := newInner(registry, utils.Some(loaded)) + inner, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.NoError(t, err) // Without anchor, commitQCs.first = 0. All 3 should be restored. @@ -306,7 +306,7 @@ func TestNewInnerLoadedCommitQCsWithAppQC(t *testing.T) { commitQCs: loadedQCs, } - inner, err := newInner(registry, utils.Some(loaded)) + inner, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.NoError(t, err) // latestAppQC should be set by prune. @@ -367,7 +367,7 @@ func TestNewInnerLoadedAllThree(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - inner, err := newInner(registry, utils.Some(loaded)) + inner, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.NoError(t, err) // AppQC restored. @@ -396,7 +396,7 @@ func TestPruneAdvancesNextBlockToPersist(t *testing.T) { registry, keys := epoch.GenRegistry(rng, 4) lane := registry.LatestEpoch().Committee().Lane(keys[0].Public()).OrPanic("keys[0]") - i, err := newInner(registry, utils.None[*loadedAvailState]()) + i, err := newInner(registry.LatestEpoch(), registry, utils.None[*loadedAvailState]()) require.NoError(t, err) // Push blocks 0-4 on one lane. @@ -471,7 +471,7 @@ func TestNewInnerLoadedCommitQCsAllBeforeAppQCArePruned(t *testing.T) { pruneAnchor: utils.Some(&PruneAnchor{AppQC: appQC, CommitQC: qcs[5]}), } - inner, err := newInner(registry, utils.Some(loaded)) + inner, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.NoError(t, err) // prune() pushes the anchor's CommitQC into the queue. @@ -500,7 +500,7 @@ func TestNewInnerAnchorWithNoCommitQCFiles(t *testing.T) { pruneAnchor: utils.Some(&PruneAnchor{AppQC: appQC, CommitQC: qcs[3]}), } - inner, err := newInner(registry, utils.Some(loaded)) + inner, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.NoError(t, err) // prune() should push the anchor's CommitQC into the queue. @@ -543,7 +543,7 @@ func TestNewInnerLoadedCommitQCsGapReturnsError(t *testing.T) { commitQCs: loadedQCs, } - _, err := newInner(registry, utils.Some(loaded)) + _, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.Error(t, err) require.Contains(t, err.Error(), "non-contiguous") } @@ -556,7 +556,7 @@ func TestNewInnerLoadedCommitQCsEmpty(t *testing.T) { commitQCs: nil, } - inner, err := newInner(registry, utils.Some(loaded)) + inner, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.NoError(t, err) require.Equal(t, types.RoadIndex(0), inner.commitQCs.first) @@ -591,7 +591,7 @@ func TestNewInnerLoadedCommitQCsGapWithAppQCAnchor(t *testing.T) { commitQCs: loadedQCs, } - inner, err := newInner(registry, utils.Some(loaded)) + inner, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.NoError(t, err) // Only QC@10 loaded. @@ -640,7 +640,7 @@ func TestNewInnerLoadedCommitQCsBelowAnchorSkipped(t *testing.T) { commitQCs: loadedQCs, } - inner, err := newInner(registry, utils.Some(loaded)) + inner, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.NoError(t, err) // prune(3) pushes QC@3 (next=4). Indices 1,2,3 are skipped. 4,5 pushed. @@ -679,7 +679,7 @@ func TestNewInnerLoadedCommitQCsGapAfterAnchorReturnsError(t *testing.T) { commitQCs: loadedQCs, } - _, err := newInner(registry, utils.Some(loaded)) + _, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.Error(t, err) require.Contains(t, err.Error(), "non-contiguous") } @@ -703,7 +703,7 @@ func TestNewInnerLoadedBlocksGapReturnsError(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - _, err := newInner(registry, utils.Some(loaded)) + _, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.Error(t, err) require.Contains(t, err.Error(), "non-contiguous") } @@ -731,7 +731,7 @@ func TestNewInnerLoadedBlocksParentHashMismatchReturnsError(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - _, err := newInner(registry, utils.Some(loaded)) + _, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.Error(t, err) require.Contains(t, err.Error(), "parent hash mismatch") } @@ -757,7 +757,7 @@ func TestNewInnerLoadedBlocksOverCapacityReturnsError(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - _, err := newInner(registry, utils.Some(loaded)) + _, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.Error(t, err) require.Contains(t, err.Error(), "exceeds capacity") } @@ -800,7 +800,7 @@ func TestNewInnerPruneAnchorPrunesBlockQueues(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - i, err := newInner(registry, utils.Some(loaded)) + i, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.NoError(t, err) // prune() should advance block queue first to the prune anchor's lane range. @@ -836,7 +836,7 @@ func TestNewInnerPruneAnchorCommitQCUsedForPrune(t *testing.T) { }, } - i, err := newInner(registry, utils.Some(loaded)) + i, err := newInner(registry.LatestEpoch(), registry, utils.Some(loaded)) require.NoError(t, err) // prune(appQC@1, pruneQC@1) should advance commitQCs.first to 1. @@ -845,8 +845,8 @@ func TestNewInnerPruneAnchorCommitQCUsedForPrune(t *testing.T) { require.Equal(t, types.RoadIndex(3), i.commitQCs.next) } -// Leave-lane WALs are re-attached on restart even when LatestEpoch omits them -// (kept until tipEpoch prune while the node is running). +// Leave-lane WALs are re-attached on restart even when the next CommitQC epoch +// omits them (kept until tipEpoch prune while the node is running). func TestNewInnerRestoresLeaveLaneWAL(t *testing.T) { rng := utils.TestRng() registry, keys := epoch.GenRegistry(rng, 3) @@ -869,7 +869,7 @@ func TestNewInnerRestoresLeaveLaneWAL(t *testing.T) { require.NoError(t, err) require.False(t, ep1.Committee().HasLane(laneB)) - i, err := newInner(registry, utils.Some(loaded)) + i, err := newInner(ep1, registry, utils.Some(loaded)) require.NoError(t, err) require.Contains(t, i.blocks, laneB) require.Equal(t, types.BlockNumber(1), i.blocks[laneB].next) @@ -906,7 +906,8 @@ func TestNewInnerRestoresLeaveLaneNamedByAnchor(t *testing.T) { ) require.NoError(t, err) - i, err := newInner(registry, utils.Some(loaded)) + // Next CommitQC is still in ep0 (tip QC epoch); leave B is re-attached for tip. + i, err := newInner(ep0, registry, utils.Some(loaded)) require.NoError(t, err) require.Contains(t, i.blocks, laneB) require.Equal(t, lrFirst, i.blocks[laneB].first) @@ -949,7 +950,7 @@ func TestNewInnerSkipsStaleLaneAbsentFromAnchor(t *testing.T) { }, } - i, err := newInner(registry, utils.Some(loaded)) + i, err := newInner(ep1, registry, utils.Some(loaded)) require.NoError(t, err) require.NotContains(t, i.blocks, orphan) } diff --git a/sei-tendermint/internal/autobahn/avail/state.go b/sei-tendermint/internal/autobahn/avail/state.go index a9f7d9d6e3..a0fa1e0143 100644 --- a/sei-tendermint/internal/autobahn/avail/state.go +++ b/sei-tendermint/internal/autobahn/avail/state.go @@ -11,7 +11,7 @@ // - Dispose: joined < tipEpoch && !tipCommittee.HasLane(lane). // // Subscribe binds LocalLane at subscribe time and serves leave maps until -// dispose → ErrLanePruned. Produce sessions use WaitProduce / WaitMustStop +// dispose → ErrLanePruned. Produce sessions use WaitForLocalLane / WaitMustStop // (same LaneID stay does not end the session). // // Restart re-attaches leave WALs still needed for tip; skips WALs already @@ -85,8 +85,8 @@ func (s *State) WaitLocalLane(ctx context.Context, pred func(utils.Option[types. return lane, nil } -// WaitProduce waits until LocalLane is Some (produce session start). -func (s *State) WaitProduce(ctx context.Context) (types.LaneID, error) { +// WaitForLocalLane waits until LocalLane is Some. +func (s *State) WaitForLocalLane(ctx context.Context) (types.LaneID, error) { laneOpt, err := s.WaitLocalLane(ctx, func(opt utils.Option[types.LaneID]) bool { return opt.IsPresent() }) @@ -317,8 +317,10 @@ func NewState(key types.SecretKey, data *data.State, stateDir utils.Option[strin } }() + // TODO(#3736): restore the epoch of the next CommitQC from persisted tip / + // applied state rather than LatestEpoch (ActivateEpoch may be ahead of ApplyEpoch). ep := data.Registry().LatestEpoch() - inner, err := newInner(data.Registry(), loaded) + inner, err := newInner(ep, data.Registry(), loaded) if err != nil { return nil, err } @@ -330,7 +332,7 @@ func NewState(key types.SecretKey, data *data.State, stateDir utils.Option[strin if anchor, ok := ls.pruneAnchor.Get(); ok { c := ep.Committee() for lane := range inner.blocks { - // allowCreate only for lanes still in the latest committee; leave WALs truncate in place. + // allowCreate only for lanes still in next-CommitQC committee; leave WALs truncate in place. if err := pers.blocks.MaybePruneAndPersistLane(lane, c.HasLane(lane), utils.Some(anchor.CommitQC), nil, utils.None[func(*types.Signed[*types.LaneProposal])]()); err != nil { return nil, fmt.Errorf("prune stale block WAL entries: %w", err) } @@ -811,7 +813,7 @@ func (s *State) WaitForLaneQCs( panic("unreachable") } -// ProduceLocalBlock appends block n on the WaitProduce session lane. +// ProduceLocalBlock appends block n on the WaitForLocalLane session lane. func (s *State) ProduceLocalBlock(lane types.LaneID, n types.BlockNumber, payload *types.Payload) (*types.Signed[*types.LaneProposal], error) { if s.key.Public() != lane.Validator { return nil, ErrBadLane diff --git a/sei-tendermint/internal/autobahn/producer/state.go b/sei-tendermint/internal/autobahn/producer/state.go index 4fa71239dc..1cda8c2662 100644 --- a/sei-tendermint/internal/autobahn/producer/state.go +++ b/sei-tendermint/internal/autobahn/producer/state.go @@ -111,11 +111,11 @@ func (s *State) clearMempool() { // This is needed so that we can track the evm nonces of sequenced txs - mempool admits txs // sequentially in the nonce order. // -// Sessions: WaitProduce → produce until WaitMustStop; then clearMempool. Stay keeps the session. +// Sessions: WaitForLocalLane → produce until WaitMustStop; then clearMempool. Stay keeps the session. func (s *State) Run(ctx context.Context) error { availState := s.consensus.Avail() for ctx.Err() == nil { - lane, err := availState.WaitProduce(ctx) + lane, err := availState.WaitForLocalLane(ctx) if err != nil { return err } @@ -225,7 +225,7 @@ func (s *State) produceSession(ctx context.Context, availState *avail.State, lan }) } -// sessionOpErr maps leave ErrBadLane → Canceled so Run can WaitProduce again. +// sessionOpErr maps leave ErrBadLane → Canceled so Run can WaitForLocalLane again. func (s *State) sessionOpErr(lane types.LaneID, op string, err error) error { if errors.Is(err, avail.ErrBadLane) { if got, ok := s.consensus.Avail().LocalLane().Get(); !ok || got != lane { diff --git a/sei-tendermint/internal/p2p/giga/avail.go b/sei-tendermint/internal/p2p/giga/avail.go index 3487e71914..5e38b8ebbd 100644 --- a/sei-tendermint/internal/p2p/giga/avail.go +++ b/sei-tendermint/internal/p2p/giga/avail.go @@ -10,7 +10,6 @@ import ( apb "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/pb" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/giga/pb" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/rpc" - "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/seilog" ) @@ -64,10 +63,7 @@ func (x *Service) subscribeLaneProposals(ctx context.Context, first types.BlockN return nil, err } logger.Info("StreamLaneProposals: not a committee lane member; waiting to subscribe") - if _, err := a.WaitLocalLane(ctx, func(opt utils.Option[types.LaneID]) bool { - _, ok := opt.Get() - return ok - }); err != nil { + if _, err := a.WaitForLocalLane(ctx); err != nil { return nil, err } first = 0 From a908f562e2d891db0237e3d87d6e11a7166ef81f Mon Sep 17 00:00:00 2001 From: Wen Date: Mon, 10 Aug 2026 20:40:46 -0700 Subject: [PATCH 10/14] refactor(autobahn): tip-dispose leave maps via SyncLanes; Epoch.IsClosed Drop tip-stale leave maps under the persist lock, then SyncLanes so disk matches memory (including NewState orphans). Move dispose predicate to types.Epoch.IsClosed; use ErrPruned for missing maps; rename WaitForCapacity. Co-authored-by: Cursor --- sei-tendermint/autobahn/types/epoch.go | 7 + sei-tendermint/autobahn/types/epoch_test.go | 30 +++ .../internal/autobahn/avail/inner_test.go | 41 ---- .../internal/autobahn/avail/state.go | 178 +++++++----------- .../internal/autobahn/avail/state_test.go | 106 +++++++++++ .../internal/autobahn/avail/subscriptions.go | 10 +- .../autobahn/consensus/persist/blocks.go | 30 ++- .../internal/autobahn/producer/state.go | 4 +- .../internal/p2p/giga/avail_test.go | 2 +- 9 files changed, 250 insertions(+), 158 deletions(-) create mode 100644 sei-tendermint/autobahn/types/epoch_test.go diff --git a/sei-tendermint/autobahn/types/epoch.go b/sei-tendermint/autobahn/types/epoch.go index 3ab13df54a..630a351fd6 100644 --- a/sei-tendermint/autobahn/types/epoch.go +++ b/sei-tendermint/autobahn/types/epoch.go @@ -49,3 +49,10 @@ 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 } + +// IsClosed reports whether lane is closed as of this epoch: Joined is strictly +// before this epoch and the lane is absent from this committee. Joiners at or +// after this epoch are not closed. Avail uses tipEpoch.IsClosed for leave dispose. +func (e *Epoch) IsClosed(lane LaneID) bool { + return lane.Joined < e.epochIndex && !e.committee.HasLane(lane) +} diff --git a/sei-tendermint/autobahn/types/epoch_test.go b/sei-tendermint/autobahn/types/epoch_test.go new file mode 100644 index 0000000000..4d4069ecf1 --- /dev/null +++ b/sei-tendermint/autobahn/types/epoch_test.go @@ -0,0 +1,30 @@ +package types + +import ( + "testing" + "time" + + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" +) + +func TestEpochIsClosed(t *testing.T) { + rng := utils.TestRng() + a := GenSecretKey(rng).Public() + b := GenSecretKey(rng).Public() + c := GenSecretKey(rng).Public() + + ep1 := NewEpoch(1, OpenRoadRange(), time.Time{}, + utils.OrPanic1(NewCommittee(map[PublicKey]uint64{a: 1, c: 1})), 0) + + stay := NewLaneID(a, 0) + leave := NewLaneID(b, 0) + joiner := NewLaneID(c, 1) + // joined == tip: not closed (live dispose uses Joined < tip). + sameEpochAbsent := NewLaneID(GenSecretKey(rng).Public(), 1) + + require.False(t, ep1.IsClosed(stay)) + require.True(t, ep1.IsClosed(leave)) + require.False(t, ep1.IsClosed(joiner)) + require.False(t, ep1.IsClosed(sameEpochAbsent)) +} diff --git a/sei-tendermint/internal/autobahn/avail/inner_test.go b/sei-tendermint/internal/autobahn/avail/inner_test.go index d3bd6448ca..0f4cdda0bf 100644 --- a/sei-tendermint/internal/autobahn/avail/inner_test.go +++ b/sei-tendermint/internal/autobahn/avail/inner_test.go @@ -913,44 +913,3 @@ func TestNewInnerRestoresLeaveLaneNamedByAnchor(t *testing.T) { require.Equal(t, lrFirst, i.blocks[laneB].first) require.Equal(t, lrFirst+1, i.blocks[laneB].next) } - -// With anchor epoch N, lanes with joined <= N absent from that epoch's committee -// are skipped (left for good; orphan WAL dirs may remain unused on disk). -func TestNewInnerSkipsStaleLaneAbsentFromAnchor(t *testing.T) { - rng := utils.TestRng() - registry, keys := epoch.GenRegistry(rng, 3) - a := keys[0] - cKey := types.GenSecretKey(rng) - - ep1, err := registry.ActivateEpoch( - map[types.PublicKey]uint64{a.Public(): 1, cKey.Public(): 1}, - types.OpenRoadRange(), time.Time{}, registry.FirstBlock(), - ) - require.NoError(t, err) - qc1 := makeCommitQC(ep1, []types.SecretKey{a, cKey}, utils.None[*types.CommitQC](), nil, utils.None[*types.AppQC]()) - require.Equal(t, types.EpochIndex(1), qc1.Proposal().EpochIndex()) - - // joined == N covers the <= bound (not only joined < N). - orphan := types.NewLaneID(types.GenSecretKey(rng).Public(), qc1.Proposal().EpochIndex()) - require.Equal(t, orphan.Joined, qc1.Proposal().EpochIndex()) - require.False(t, ep1.Committee().HasLane(orphan)) - - app1 := types.NewAppProposal(qc1.GlobalRange().First, qc1.Index(), types.GenAppHash(rng), 1) - appQC1 := types.NewAppQC([]*types.Signed[*types.AppVote]{ - types.Sign(a, types.NewAppVote(app1)), - types.Sign(cKey, types.NewAppVote(app1)), - }) - - ob := testSignedBlock(types.GenSecretKey(rng), orphan, 0, types.BlockHeaderHash{}, rng) - loaded := &loadedAvailState{ - pruneAnchor: utils.Some(&PruneAnchor{AppQC: appQC1, CommitQC: qc1}), - commitQCs: []persist.LoadedCommitQC{{Index: qc1.Index(), QC: qc1}}, - blocks: map[types.LaneID][]persist.LoadedBlock{ - orphan: {{Number: 0, Proposal: ob}}, - }, - } - - i, err := newInner(ep1, registry, utils.Some(loaded)) - require.NoError(t, err) - require.NotContains(t, i.blocks, orphan) -} diff --git a/sei-tendermint/internal/autobahn/avail/state.go b/sei-tendermint/internal/autobahn/avail/state.go index a0fa1e0143..3f090a9cb0 100644 --- a/sei-tendermint/internal/autobahn/avail/state.go +++ b/sei-tendermint/internal/autobahn/avail/state.go @@ -4,18 +4,24 @@ // Lane map lifecycle (CON-358; production ApplyEpoch wiring is #3736). // Identity rules (stay / leave / rejoin) are on types.LaneID. // +// Two CommitQC epochs matter: +// - tipEpoch: epoch of the first retained CommitQC — retires leave maps +// (tipEpoch.IsClosed → drop + SyncLanes). +// - latestCommitEpoch: epoch of latestCommitQC (else inner.epoch before any QC) +// — admits block/vote ingest for that committee's lanes (and retained leave +// maps until tip retires them). A missing map is never waited on: ApplyEpoch +// installs maps before ingest; absence means pruned or not yet/no longer admitted. +// // Maps (inner.blocks / votes / cursors): // - Join/stay: ensured at ApplyEpoch (addCommitteeLanes, then Store epoch). -// - Leave: maps remain until tipEpoch (epoch of the first retained CommitQC) -// omits the LaneID, then DeleteLane + map drop on the same persist tick. -// - Dispose: joined < tipEpoch && !tipCommittee.HasLane(lane). +// - Leave: maps remain until tipEpoch.IsClosed, then map drop + SyncLanes. // // Subscribe binds LocalLane at subscribe time and serves leave maps until // dispose → ErrLanePruned. Produce sessions use WaitForLocalLane / WaitMustStop // (same LaneID stay does not end the session). // -// Restart re-attaches leave WALs still needed for tip; skips WALs already -// tip-stale at the prune anchor (see tipcut skip in newInner). +// Restart re-attaches leave WALs still needed for tip; SyncLanes deletes WALs +// already tip-stale at the prune anchor (see tipcut skip in newInner). package avail import ( @@ -38,8 +44,9 @@ import ( // ErrBadLane . var ErrBadLane = errors.New("bad lane") -// ErrLanePruned: SubscribeLaneProposals.Recv after tipEpoch drop of the bound leave map. -// Leave alone keeps serving until prune; rejoin needs a new Subscribe. +// ErrLanePruned: SubscribeLaneProposals.Recv after the bound leave map is +// disposed (tipEpoch.IsClosed). Leave alone keeps serving until then; rejoin +// needs a new Subscribe. var ErrLanePruned = errors.New("lane pruned") const BlocksPerLane = 3 * types.MaxLaneRangeInProposal @@ -117,57 +124,19 @@ func (s *State) ApplyEpoch(ep *types.Epoch) { } } -// tipEpochOf is the registry epoch of the first retained CommitQC. -func tipEpochOf(inner *inner, registry *epoch.Registry) (utils.Option[*types.Epoch], error) { +// epochOfFirst is the registry epoch of the first retained CommitQC (tipEpoch). +func epochOfFirst(inner *inner, registry *epoch.Registry) (utils.Option[*types.Epoch], error) { if inner.commitQCs.first >= inner.commitQCs.next { return utils.None[*types.Epoch](), nil } idx := inner.commitQCs.q[inner.commitQCs.first].Proposal().EpochIndex() ep, found := registry.EpochByIndex(idx) if !found { - return utils.None[*types.Epoch](), fmt.Errorf("unknown epoch_index %d for tipEpoch CommitQC", idx) + return utils.None[*types.Epoch](), fmt.Errorf("unknown epoch_index %d for first retained CommitQC", idx) } return utils.Some(ep), nil } -// staleLaneDisposable: tipEpoch omits lane and joined < tip (joiners at/after tip stay). -// None tipEpoch → false. -func staleLaneDisposable(lane types.LaneID, tipEpoch utils.Option[*types.Epoch]) bool { - ep, ok := tipEpoch.Get() - if !ok { - return false - } - return lane.Joined < ep.EpochIndex() && !ep.Committee().HasLane(lane) -} - -// deleteStaleLaneWAL Deletes WALs for tip-stale leave maps. -// DeleteLane no-ops if a lane never opened a WAL (empty leave). -func (s *State) deleteStaleLaneWAL(lanes []types.LaneID) error { - for _, lane := range lanes { - if err := s.persisters.blocks.DeleteLane(lane); err != nil { - return fmt.Errorf("DeleteLane(%s): %w", lane, err) - } - } - return nil -} - -// pruneStaleLeave Deletes WALs then drops maps for tip-stale leave LaneIDs -// (same tick as runPersist after Parallel). -func (s *State) pruneStaleLeave(staleLeave []types.LaneID) error { - if err := s.deleteStaleLaneWAL(staleLeave); err != nil { - return err - } - if len(staleLeave) == 0 { - return nil - } - for inner, ctrl := range s.inner.Lock() { - if inner.dropLanes(staleLeave) > 0 { - ctrl.Updated() - } - } - return nil -} - // persisters holds all disk persistence components. Either all are present // (real I/O) or all are no-op (testing). It is a pure I/O struct — all inner // state access goes through State methods. @@ -325,9 +294,14 @@ func NewState(key types.SecretKey, data *data.State, stateDir utils.Option[strin return nil, err } + // Disk must match in-memory lanes (tipcut skip / crash orphans). + if err := persist.SyncLanes(pers.blocks, inner.blocks); err != nil { + return nil, fmt.Errorf("sync lane WALs to memory set: %w", err) + } + // Truncate WAL entries below the prune anchor that were filtered out by - // loadPersistedState. Includes restored leave lanes; tipEpoch prune deletes - // leave WALs once their maps become staleLeave. + // loadPersistedState. Includes restored leave lanes; SyncLanes deletes + // leave WALs once their maps are dropped (tipEpoch.IsClosed). if ls, ok := loaded.Get(); ok { if anchor, ok := ls.pruneAnchor.Get(); ok { c := ep.Committee() @@ -569,8 +543,7 @@ func (s *State) NextBlock(lane types.LaneID) types.BlockNumber { // Block returns block n of the given lane. // Waits until the block is available. -// Returns ErrPruned if the block has been already pruned. -// Returns ErrBadLane if the lane map is gone (tipEpoch leave prune). +// Returns ErrPruned if the block or lane has already been pruned. func (s *State) Block(ctx context.Context, lane types.LaneID, n types.BlockNumber) (*types.Signed[*types.LaneProposal], error) { for inner, ctrl := range s.inner.Lock() { if err := ctrl.WaitUntil(ctx, func() bool { @@ -581,7 +554,7 @@ func (s *State) Block(ctx context.Context, lane types.LaneID, n types.BlockNumbe } q, ok := inner.blocks[lane] if !ok { - return nil, ErrBadLane + return nil, types.ErrPruned } if n < q.first { return nil, types.ErrPruned @@ -593,7 +566,7 @@ func (s *State) Block(ctx context.Context, lane types.LaneID, n types.BlockNumbe // PushBlock pushes a block to the state. // Waits until all previous blocks are available. -// Returns ErrBadLane if tipEpoch drop removes the lane map while waiting. +// No-op if the lane map is gone (pruned leave or not admitted). func (s *State) PushBlock(ctx context.Context, p *types.Signed[*types.LaneProposal]) error { h := p.Msg().Block().Header() if p.Key() != h.Lane().Validator { @@ -613,7 +586,7 @@ func (s *State) PushBlock(ctx context.Context, p *types.Signed[*types.LanePropos if err := ctrl.WaitUntil(ctx, func() bool { q, ok := inner.blocks[lane] if !ok { - return true // tipEpoch drop + return true } return n <= min(q.next, inner.persistedBlockStart[lane]+BlocksPerLane-1) }); err != nil { @@ -621,7 +594,7 @@ func (s *State) PushBlock(ctx context.Context, p *types.Signed[*types.LanePropos } q, ok := inner.blocks[lane] if !ok { - return ErrBadLane + return nil } // not needed any more if q.next != n { @@ -656,7 +629,7 @@ func (s *State) PushBlock(ctx context.Context, p *types.Signed[*types.LanePropos // PushVote pushes a LaneVote to the state. // Waits until the lane has enough capacity for the new vote. // It does NOT wait for the previous votes. -// Returns ErrBadLane if tipEpoch drop removes the lane map while waiting. +// No-op if the lane map is gone (pruned leave or not admitted). func (s *State) PushVote(ctx context.Context, vote *types.Signed[*types.LaneVote]) error { if _, err := s.data.Registry().VerifyInWindow(func(c *types.Committee) error { if err := vote.Msg().Verify(c); err != nil { @@ -672,7 +645,7 @@ func (s *State) PushVote(ctx context.Context, vote *types.Signed[*types.LaneVote for inner, ctrl := range s.inner.Lock() { if err := ctrl.WaitUntil(ctx, func() bool { if _, ok := inner.votes[lane]; !ok { - return true // tipEpoch drop + return true } return n < inner.persistedBlockStart[lane]+BlocksPerLane }); err != nil { @@ -680,7 +653,7 @@ func (s *State) PushVote(ctx context.Context, vote *types.Signed[*types.LaneVote } q, ok := inner.votes[lane] if !ok { - return ErrBadLane + return nil } if n < q.first { return nil @@ -696,8 +669,7 @@ func (s *State) PushVote(ctx context.Context, vote *types.Signed[*types.LaneVote } // headers collects headers for the given range. -// Missing vote queue (leave map dropped past AppQC floor) → ErrPruned so PushQC can skip; -// ErrBadLane would kill avail.Run. +// Returns ErrPruned if the range is unavailable (missing map or below first). func (s *State) headers(ctx context.Context, lr *types.LaneRange) ([]*types.BlockHeader, error) { // Empty range is always available. if lr.First() == lr.Next() { @@ -709,7 +681,6 @@ func (s *State) headers(ctx context.Context, lr *types.LaneRange) ([]*types.Bloc for i := range headers { n := lr.Next() - types.BlockNumber(i) - 1 //nolint:gosec // i is bounded by len(headers) which is a small block range; no overflow risk for { - // Re-check after Wait: tipEpoch may drop the leave map mid-assembly. q, ok := inner.votes[lr.Lane()] if !ok { return nil, types.ErrPruned @@ -757,11 +728,12 @@ func (s *State) fullCommitQC(ctx context.Context, n types.RoadIndex) (*types.Ful return types.NewFullCommitQC(qc, commitHeaders), nil } -// WaitForLocalCapacity waits until the lane has capacity for toProduce. -// ErrBadLane if the lane left committee or its map was tipEpoch-pruned while waiting. +// WaitForCapacity waits until the lane has capacity for toProduce. +// ErrBadLane if the lane left the applied committee. +// ErrPruned if the lane map is gone. // Presence is keyed off blocks (always set for live lanes); persistedBlockStart may // be absent on a fresh start (zero start), which is not a prune. -func (s *State) WaitForLocalCapacity(ctx context.Context, lane types.LaneID, toProduce types.BlockNumber) error { +func (s *State) WaitForCapacity(ctx context.Context, lane types.LaneID, toProduce types.BlockNumber) error { for inner, ctrl := range s.inner.Lock() { if err := ctrl.WaitUntil(ctx, func() bool { if !inner.epoch.Load().Committee().HasLane(lane) { @@ -778,7 +750,7 @@ func (s *State) WaitForLocalCapacity(ctx context.Context, lane types.LaneID, toP return ErrBadLane } if _, ok := inner.blocks[lane]; !ok { - return ErrBadLane + return types.ErrPruned } } return nil @@ -942,13 +914,11 @@ func (s *State) runPersist(ctx context.Context, pers persisters) error { s.markBlockPersisted(header.Lane(), header.BlockNumber()+1) } - blocksByLane := make(map[types.LaneID][]*types.Signed[*types.LaneProposal]) - for _, proposal := range batch.blocks { - lane := proposal.Msg().Block().Header().Lane() - blocksByLane[lane] = append(blocksByLane[lane], proposal) - } + committee := s.epoch.Load().Committee() - active := s.epoch.Load().Committee() + if err := persist.SyncLanes(pers.blocks, batch.blocks); err != nil { + return err + } // 2. Persist commit-QCs and per-lane blocks in parallel. // Callees handle empty inputs gracefully (no-op when nothing to write/truncate). @@ -958,27 +928,12 @@ func (s *State) runPersist(ctx context.Context, pers persisters) error { s.markCommitQCsPersisted(qc) })) }) - // Collect lanes: any lane with blocks in this batch, plus all lanes - // in the anchor epoch (for WAL pruning). - // TODO(#3736): only lanes of the latest CommitQC's epoch are - // admitted — do not union earlier epochs from batch.commitQCs. - batchLanes := map[types.LaneID]struct{}{} - for lane := range blocksByLane { - batchLanes[lane] = struct{}{} - } - if anchor, ok := anchorQC.Get(); ok { - ep, epOK := s.data.Registry().EpochByIndex(anchor.Proposal().EpochIndex()) - if !epOK { - return fmt.Errorf("unknown epoch_index %d", anchor.Proposal().EpochIndex()) - } - for lane := range ep.Committee().Lanes().All() { - batchLanes[lane] = struct{}{} + for lane, proposals := range batch.blocks { + if len(proposals) == 0 && !anchorQC.IsPresent() { + continue } - } - for lane := range batchLanes { - proposals := blocksByLane[lane] ps.Spawn(func() error { - allowCreate := active.HasLane(lane) || len(proposals) > 0 + allowCreate := committee.HasLane(lane) || len(proposals) > 0 return pers.blocks.MaybePruneAndPersistLane(lane, allowCreate, anchorQC, proposals, utils.Some(markBlock)) }) } @@ -986,20 +941,15 @@ func (s *State) runPersist(ctx context.Context, pers persisters) error { }); err != nil { return err } - if err := s.pruneStaleLeave(batch.staleLeave); err != nil { - return err - } } } // persistBatch holds the data collected under lock for one persist iteration. +// blocks keys are the in-memory lanes after tip-stale drop (empty slice = no appends). type persistBatch struct { - blocks []*types.Signed[*types.LaneProposal] + blocks map[types.LaneID][]*types.Signed[*types.LaneProposal] commitQCs []*types.CommitQC pruneAnchor utils.Option[*PruneAnchor] - // staleLeave: tipEpoch-disposable map keys skipped for append this tick. - // WAL deleted then maps dropped after Parallel (same iteration). - staleLeave []types.LaneID } // advancePersistedBlockStart updates the per-lane block admission watermark @@ -1023,6 +973,9 @@ func (s *State) advancePersistedBlockStart(commitQC *types.CommitQC) { // callers (acquires s.inner lock internally). func (s *State) markBlockPersisted(lane types.LaneID, next types.BlockNumber) { for inner, ctrl := range s.inner.Lock() { + if _, ok := inner.blocks[lane]; !ok { + return + } inner.nextBlockToPersist[lane] = next ctrl.Updated() } @@ -1038,8 +991,7 @@ func (s *State) markCommitQCsPersisted(qc *types.CommitQC) { } // collectPersistBatch waits for new blocks or commitQCs and collects them under lock. -// TipEpoch-stale leave maps are listed in staleLeave (not appended); runPersist -// Deletes their WALs then drops the maps in the same iteration. +// TipEpoch-stale leave maps are dropped; blocks keys are the surviving in-memory lanes. func (s *State) collectPersistBatch( ctx context.Context, lastPersistedAppQCNext types.RoadIndex, @@ -1066,18 +1018,32 @@ func (s *State) collectPersistBatch( }); err != nil { return b, err } - tipEpoch, err := tipEpochOf(inner, s.data.Registry()) + tipEpoch, err := epochOfFirst(inner, s.data.Registry()) if err != nil { return b, err } - for lane, q := range inner.blocks { - if staleLaneDisposable(lane, tipEpoch) { - b.staleLeave = append(b.staleLeave, lane) - continue + var staleLeave []types.LaneID + if tip, ok := tipEpoch.Get(); ok { + for lane := range inner.blocks { + if tip.IsClosed(lane) { + staleLeave = append(staleLeave, lane) + } + } + } + if len(staleLeave) > 0 { + if inner.dropLanes(staleLeave) > 0 { + ctrl.Updated() } + } + // One entry per in-memory lane; empty slice means no appends this tick. + b.blocks = make(map[types.LaneID][]*types.Signed[*types.LaneProposal], len(inner.blocks)) + for lane, q := range inner.blocks { start := max(inner.nextBlockToPersist[lane], q.first) for n := start; n < q.next; n++ { - b.blocks = append(b.blocks, q.q[n]) + b.blocks[lane] = append(b.blocks[lane], q.q[n]) + } + if _, ok := b.blocks[lane]; !ok { + b.blocks[lane] = nil } } commitQCNext = max(commitQCNext, inner.commitQCs.first) diff --git a/sei-tendermint/internal/autobahn/avail/state_test.go b/sei-tendermint/internal/autobahn/avail/state_test.go index 1c420f8815..8a1de0dcbc 100644 --- a/sei-tendermint/internal/autobahn/avail/state_test.go +++ b/sei-tendermint/internal/autobahn/avail/state_test.go @@ -258,6 +258,112 @@ func testState(t *testing.T, stateDir utils.Option[string]) { } } +// ApplyEpoch keeps leave maps until tipEpoch dispose; the same persist tick +// SyncLanes-deletes the leave WAL. +func TestApplyEpoch_TipEpochDisposeDeletesLeaveWAL(t *testing.T) { + ctx := t.Context() + rng := utils.TestRng() + registry, keys := epoch.GenRegistry(rng, 3) + a, b := keys[0], keys[1] + cKey := types.GenSecretKey(rng) + + stateDir := t.TempDir() + var state *State + require.NoError(t, scope.Run(ctx, func(ctx context.Context, s scope.Scope) error { + ds := newTestDataState(&data.Config{Registry: registry}) + s.SpawnBgNamed("data.Run", func() error { + return utils.IgnoreCancel(ds.Run(ctx)) + }) + var err error + state, err = NewState(a, ds, utils.Some(stateDir)) + if err != nil { + return err + } + // Seed B's WAL before Run so we are not racing the persist goroutine. + laneB := registry.LatestEpoch().Committee().Lane(b.Public()).OrPanic("b") + signedB := types.Sign(b, types.NewLaneProposal( + types.NewBlock(laneB, 0, types.BlockHeaderHash{}, types.GenPayload(rng)), + )) + if err := state.persisters.blocks.MaybePruneAndPersistLane( + laneB, true, utils.None[*types.CommitQC](), + []*types.Signed[*types.LaneProposal]{signedB}, noBlockCB, + ); err != nil { + return err + } + laneBPath := filepath.Join(stateDir, "blocks", laneB.HexString()) + if _, err := os.Stat(laneBPath); err != nil { + return fmt.Errorf("lane B WAL: %w", err) + } + + s.SpawnBgNamed("avail.Run", func() error { + return utils.IgnoreCancel(state.Run(ctx)) + }) + + laneA := registry.LatestEpoch().Committee().Lane(a.Public()).OrPanic("a") + if _, err := state.ProduceLocalBlock(laneA, 0, types.GenPayload(rng)); err != nil { + return err + } + + ep, err := registry.ActivateEpoch( + map[types.PublicKey]uint64{a.Public(): 1, cKey.Public(): 1}, + types.OpenRoadRange(), time.Time{}, registry.FirstBlock(), + ) + if err != nil { + return err + } + if ep.EpochIndex() != 1 { + return fmt.Errorf("epoch index: got %d want 1", ep.EpochIndex()) + } + state.ApplyEpoch(ep) + + laneA2 := ep.Committee().Lane(a.Public()).OrPanic("a") + laneC := ep.Committee().Lane(cKey.Public()).OrPanic("c") + if laneA2 != laneA { + return fmt.Errorf("stay lane changed: %v vs %v", laneA, laneA2) + } + if got := state.LocalLane().OrPanic("stay"); got != laneA2 { + return fmt.Errorf("LocalLane: got %v want %v", got, laneA2) + } + if got := state.NextBlock(laneC); got != 0 { + return fmt.Errorf("joiner NextBlock: got %d", got) + } + if ep.Committee().HasLane(laneB) { + return fmt.Errorf("ep1 still has leave lane B") + } + for inner := range state.inner.Lock() { + if _, ok := inner.blocks[laneB]; !ok { + return fmt.Errorf("leave maps dropped before tipEpoch") + } + } + if _, err := os.Stat(laneBPath); err != nil { + return fmt.Errorf("leave WAL gone before tipEpoch: %w", err) + } + + // First retained CommitQC is ep1 → tipEpoch dispose of B (joined 0). + qc := makeCommitQC(ep, []types.SecretKey{a, cKey}, utils.None[*types.CommitQC](), nil, utils.None[*types.AppQC]()) + if err := state.PushCommitQC(ctx, qc); err != nil { + return fmt.Errorf("PushCommitQC: %w", err) + } + if err := state.waitForCommitQC(ctx, qc.Proposal().Index()); err != nil { + return fmt.Errorf("waitForCommitQC: %w", err) + } + + for inner := range state.inner.Lock() { + if _, ok := inner.blocks[laneB]; ok { + return fmt.Errorf("leave maps still present after tipEpoch dispose") + } + if _, ok := inner.blocks[laneC]; !ok { + return fmt.Errorf("joiner maps missing after tipEpoch dispose") + } + } + if _, err := os.Stat(laneBPath); !os.IsNotExist(err) { + return fmt.Errorf("leave WAL still on disk: %v", err) + } + return nil + })) + require.NoError(t, state.Close()) +} + // TestStateRestartFromPersisted runs the state with persistence through 2 // iterations (blocks → votes → commitQC → appQC each), stops, and restarts // from the same directory. This verifies that what the runtime persist diff --git a/sei-tendermint/internal/autobahn/avail/subscriptions.go b/sei-tendermint/internal/autobahn/avail/subscriptions.go index 24436ba86f..234710773d 100644 --- a/sei-tendermint/internal/autobahn/avail/subscriptions.go +++ b/sei-tendermint/internal/autobahn/avail/subscriptions.go @@ -32,13 +32,15 @@ func (r *LaneProposalsRecv) Recv(ctx context.Context) (*types.Signed[*types.Lane b, err := r.state.Block(ctx, r.lane, r.next) if err != nil { if errors.Is(err, types.ErrPruned) { + // Number prune advances; leave dispose also returns ErrPruned. + for inner := range r.state.inner.Lock() { + if _, ok := inner.blocks[r.lane]; !ok { + return nil, ErrLanePruned + } + } r.next += 1 continue } - if errors.Is(err, ErrBadLane) { - // TipEpoch pruned leave map (or DeleteLane race). - return nil, ErrLanePruned - } return nil, fmt.Errorf("x.avail.Block(): %w", err) } r.next += 1 diff --git a/sei-tendermint/internal/autobahn/consensus/persist/blocks.go b/sei-tendermint/internal/autobahn/consensus/persist/blocks.go index 4fb48fb007..6c930bd8a1 100644 --- a/sei-tendermint/internal/autobahn/consensus/persist/blocks.go +++ b/sei-tendermint/internal/autobahn/consensus/persist/blocks.go @@ -173,7 +173,7 @@ func (lw *laneWAL) close() error { // MaybePruneAndPersistLane holds the per-lane lock for the entire // truncate-then-append sequence, so concurrent calls on the same lane // serialize correctly. Different lanes are fully parallel. -// Lanes may be removed via DeleteLane once tipEpoch omits them. +// Lanes may be removed via SyncLanes/DeleteLane once tipEpoch omits them. type BlockPersister struct { dir utils.Option[string] // immutable after construction lanes utils.RWMutex[map[types.LaneID]*laneWAL] @@ -339,9 +339,9 @@ func (bp *BlockPersister) MaybePruneAndPersistLane( } // NOTE: MaybePruneAndPersistLane releases the map RLock before acquiring -// the per-lane lock. DeleteLane must not overlap an in-flight -// MaybePruneAndPersistLane on the same lane. Avail calls DeleteLane after -// runPersist's Parallel batch returns for tip-stale leave maps. +// the per-lane lock. SyncLanes/DeleteLane must not overlap an in-flight +// MaybePruneAndPersistLane on the same lane. Avail calls SyncLanes after +// tip-stale map drop and before runPersist's Parallel batch. // // No-op if the lane WAL is not open (never created, or already deleted). func (bp *BlockPersister) DeleteLane(lane types.LaneID) error { @@ -368,6 +368,28 @@ func (bp *BlockPersister) DeleteLane(lane types.LaneID) error { panic("unreachable") } +// SyncLanes deletes open WALs whose LaneID is not a key of keep. Idempotent. +// Must not overlap MaybePruneAndPersistLane on a lane being deleted. +func SyncLanes[V any](bp *BlockPersister, keep map[types.LaneID]V) error { + if _, ok := bp.dir.Get(); !ok { + return nil + } + var stale []types.LaneID + for lanes := range bp.lanes.RLock() { + for lane := range lanes { + if _, ok := keep[lane]; !ok { + stale = append(stale, lane) + } + } + } + for _, lane := range stale { + if err := bp.DeleteLane(lane); err != nil { + return err + } + } + return nil +} + // Close shuts down all per-lane WALs, releasing the exclusive lock each one holds on its directory. // // Production does not call this: a node exits by rugpull and the OS reclaims everything. It exists so diff --git a/sei-tendermint/internal/autobahn/producer/state.go b/sei-tendermint/internal/autobahn/producer/state.go index 1cda8c2662..ce0c6d0bd0 100644 --- a/sei-tendermint/internal/autobahn/producer/state.go +++ b/sei-tendermint/internal/autobahn/producer/state.go @@ -165,8 +165,8 @@ func (s *State) produceSession(ctx context.Context, availState *avail.State, lan limiter := rate.NewLimiter(limit, burst) lastBlockTime := time.Now() for toProduce := firstBlock; ; toProduce += 1 { - if err := availState.WaitForLocalCapacity(ctx, lane, toProduce); err != nil { - return s.sessionOpErr(lane, "availState.WaitForLocalCapacity()", err) + if err := availState.WaitForCapacity(ctx, lane, toProduce); err != nil { + return s.sessionOpErr(lane, "availState.WaitForCapacity()", err) } var payload *types.Payload // Wait until either diff --git a/sei-tendermint/internal/p2p/giga/avail_test.go b/sei-tendermint/internal/p2p/giga/avail_test.go index 9d8d9af606..12db30d252 100644 --- a/sei-tendermint/internal/p2p/giga/avail_test.go +++ b/sei-tendermint/internal/p2p/giga/avail_test.go @@ -86,7 +86,7 @@ func TestAvailClientServer(t *testing.T) { lane := a.LocalLane().OrPanic("local") for range totalBlocks { n := a.NextBlock(lane) - if err := a.WaitForLocalCapacity(ctx, lane, n); err != nil { + if err := a.WaitForCapacity(ctx, lane, n); err != nil { return fmt.Errorf("waitForLocalCapacity(): %w", err) } if _, err := a.ProduceLocalBlock(lane, n, types.GenPayload(rng)); err != nil { From a23de585353ae42ba29b2a337db48c4b32fb44b6 Mon Sep 17 00:00:00 2001 From: Wen Date: Mon, 10 Aug 2026 21:09:36 -0700 Subject: [PATCH 11/14] refactor(autobahn): make producer mempool optional per produce session Mempool should not exist without a local lane: hold Option[*mempool] in the Watch inner, align on session start, and clear on every session exit. Co-authored-by: Cursor --- .../internal/autobahn/producer/mempool.go | 67 ++++++++++----- .../autobahn/producer/mempool_test.go | 13 +-- .../internal/autobahn/producer/state.go | 82 +++++++------------ 3 files changed, 86 insertions(+), 76 deletions(-) diff --git a/sei-tendermint/internal/autobahn/producer/mempool.go b/sei-tendermint/internal/autobahn/producer/mempool.go index d523f71d43..cb8a06ad5c 100644 --- a/sei-tendermint/internal/autobahn/producer/mempool.go +++ b/sei-tendermint/internal/autobahn/producer/mempool.go @@ -16,7 +16,7 @@ var errTooLarge = errors.New("transaction too large") var errBadNonce = errors.New("bad nonce") var errMempoolFull = errors.New("mempool is full") -// ErrNotProducing: LocalLane None or mempool not aligned (leave / pre-align gap). +// ErrNotProducing: no mempool (no local lane / leave) or mempool not aligned to LocalLane. var ErrNotProducing = errors.New("not producing") type blockSpec struct { @@ -30,9 +30,16 @@ type blockSpec struct { evmNonces map[common.Address]uint64 } +// mempoolInner is the Watch target. Watch cannot replace its value, so the +// optional mempool lives here (None when not producing). +type mempoolInner struct { + m utils.Option[*mempool] +} + +// mempool exists only while producing on a local lane. type mempool struct { capacity uint64 - lane utils.Option[types.LaneID] + lane types.LaneID first types.BlockNumber next types.BlockNumber blocks map[types.BlockNumber]*blockSpec @@ -41,6 +48,19 @@ type mempool struct { evmTxs map[common.Hash]tmtypes.Tx } +func newMempool(capacity uint64, lane types.LaneID, n types.BlockNumber) *mempool { + return &mempool{ + capacity: capacity, + lane: lane, + first: n, + next: n, + blocks: map[types.BlockNumber]*blockSpec{}, + nextBlock: &blockSpec{evmNonces: map[common.Address]uint64{}}, + evmNonces: map[common.Address]uint64{}, + evmTxs: map[common.Hash]tmtypes.Tx{}, + } +} + func (m *mempool) IsFull() bool { return uint64(m.next-m.first) >= m.capacity && len(m.nextBlock.txs) > 0 } @@ -60,32 +80,40 @@ func (m *mempool) SealBlock() { // TODO(gprusak): this rpc is probably unused, but if it is // consider whether unsequenced/unexecuted lane txs should be included here. func (s *State) UnconfirmedTxs() [][]byte { - for m := range s.mempool.Lock() { - return m.nextBlock.txs + for inner := range s.mempool.Lock() { + if m, ok := inner.m.Get(); ok { + return m.nextBlock.txs + } + return nil } - panic("uneachable") + panic("unreachable") } func (s *State) EvmNextPendingNonce(addr common.Address) uint64 { - for m := range s.mempool.Lock() { - if nonce, ok := m.evmNonces[addr]; ok { - return nonce + for inner := range s.mempool.Lock() { + if m, ok := inner.m.Get(); ok { + if nonce, ok := m.evmNonces[addr]; ok { + return nonce + } } } return s.app.EvmNonce(addr) } func (s *State) EvmTxByHash(hash common.Hash) (tmtypes.Tx, bool) { - for m := range s.mempool.Lock() { - tx, ok := m.evmTxs[hash] - return tx, ok + for inner := range s.mempool.Lock() { + if m, ok := inner.m.Get(); ok { + tx, ok := m.evmTxs[hash] + return tx, ok + } + return nil, false } panic("unreachable") } // Removes txs from mempool assigned to lane blocks Date: Mon, 10 Aug 2026 21:39:02 -0700 Subject: [PATCH 12/14] refactor(autobahn): StreamLaneProposals by LaneID with client reconnect Request carries the producer LaneID; server verifies key and ends cleanly on prune. Client WaitLane-reconnects, excluding only after leave/rejoin and treating mux.ErrRemoteClosed as the clean stream end. Co-authored-by: Cursor --- .../internal/autobahn/avail/state.go | 11 +- .../internal/autobahn/avail/subscriptions.go | 36 +++++- .../autobahn/avail/subscriptions_test.go | 114 +++++++++++++++++- .../internal/autobahn/avail/testonly.go | 6 +- sei-tendermint/internal/p2p/giga/api.proto | 1 + sei-tendermint/internal/p2p/giga/avail.go | 105 ++++++++-------- sei-tendermint/internal/p2p/giga/data_test.go | 4 +- sei-tendermint/internal/p2p/giga/pb/api.pb.go | 40 ++++-- .../internal/p2p/giga/pb/api.wireguard.go | 3 +- sei-tendermint/internal/p2p/giga/service.go | 5 +- sei-tendermint/internal/p2p/giga/types.go | 15 ++- .../internal/p2p/giga/types_test.go | 5 +- .../internal/p2p/giga_router_validator.go | 4 +- sei-tendermint/internal/p2p/mux/stream.go | 4 +- 14 files changed, 269 insertions(+), 84 deletions(-) diff --git a/sei-tendermint/internal/autobahn/avail/state.go b/sei-tendermint/internal/autobahn/avail/state.go index 3f090a9cb0..dcfc6ab97f 100644 --- a/sei-tendermint/internal/autobahn/avail/state.go +++ b/sei-tendermint/internal/autobahn/avail/state.go @@ -16,9 +16,9 @@ // - Join/stay: ensured at ApplyEpoch (addCommitteeLanes, then Store epoch). // - Leave: maps remain until tipEpoch.IsClosed, then map drop + SyncLanes. // -// Subscribe binds LocalLane at subscribe time and serves leave maps until -// dispose → ErrLanePruned. Produce sessions use WaitForLocalLane / WaitMustStop -// (same LaneID stay does not end the session). +// SubscribeLaneProposals binds an explicit LaneID (must be this node's key) and +// serves leave maps until dispose → ErrLanePruned. Produce sessions use +// WaitForLocalLane / WaitMustStop (same LaneID stay does not end the session). // // Restart re-attaches leave WALs still needed for tip; SyncLanes deletes WALs // already tip-stale at the prune anchor (see tipcut skip in newInner). @@ -78,6 +78,11 @@ func (s *State) LocalLane() utils.Option[types.LaneID] { return s.epoch.Load().Committee().Lane(s.key.Public()) } +// Lane is pk's applied-committee LaneID, if any. +func (s *State) Lane(pk types.PublicKey) utils.Option[types.LaneID] { + return s.epoch.Load().Committee().Lane(pk) +} + // WaitLocalLane waits until pred(LocalLane()). Stay does not satisfy a "changed" pred. func (s *State) WaitLocalLane(ctx context.Context, pred func(utils.Option[types.LaneID]) bool) (utils.Option[types.LaneID], error) { pk := s.key.Public() diff --git a/sei-tendermint/internal/autobahn/avail/subscriptions.go b/sei-tendermint/internal/autobahn/avail/subscriptions.go index 234710773d..67923b6b36 100644 --- a/sei-tendermint/internal/autobahn/avail/subscriptions.go +++ b/sei-tendermint/internal/autobahn/avail/subscriptions.go @@ -6,16 +6,19 @@ import ( "fmt" "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" ) -// SubscribeLaneProposals binds LocalLane at subscribe time. After leave, serves -// the leave map until tipEpoch prune → ErrLanePruned; rejoin needs a new Subscribe. +// SubscribeLaneProposals binds the given lane (must be this node's key). After leave, +// serves the leave map until tipEpoch prune → ErrLanePruned; rejoin needs a new Subscribe +// with the new LaneID. // // Back-leash (AppQC in prior epoch before ActivateEpoch) means tipEpoch prune -// drops the leave map before rejoin, so Recv ends before LocalLane is Some(new). -func (s *State) SubscribeLaneProposals(first types.BlockNumber) (*LaneProposalsRecv, error) { - lane, ok := s.LocalLane().Get() - if !ok { +// drops the leave map before rejoin, so Recv ends before a new LaneID is Some. +// Rejoin is at least one epoch after leave, so a live stream on the leave map +// is not expected to overlap production on the new LaneID. +func (s *State) SubscribeLaneProposals(lane types.LaneID, first types.BlockNumber) (*LaneProposalsRecv, error) { + if lane.Validator != s.key.Public() { return nil, ErrBadLane } return &LaneProposalsRecv{s, lane, first}, nil @@ -48,6 +51,27 @@ func (r *LaneProposalsRecv) Recv(ctx context.Context) (*types.Signed[*types.Lane } } +// WaitLane waits until the applied committee has a LaneID for pk. +// If exclude is Some, also requires the LaneID to differ (e.g. after leave/rejoin). +func (s *State) WaitLane(ctx context.Context, pk types.PublicKey, exclude utils.Option[types.LaneID]) (types.LaneID, error) { + var lane types.LaneID + _, err := s.epoch.Wait(ctx, func(ep *types.Epoch) bool { + got, ok := ep.Committee().Lane(pk).Get() + if !ok { + return false + } + if prev, has := exclude.Get(); has && got == prev { + return false + } + lane = got + return true + }) + if err != nil { + return types.LaneID{}, err + } + return lane, nil +} + func (s *State) SubscribeLaneVotes() *LaneVotesRecv { return &LaneVotesRecv{s, map[types.LaneID]types.BlockNumber{}} } diff --git a/sei-tendermint/internal/autobahn/avail/subscriptions_test.go b/sei-tendermint/internal/autobahn/avail/subscriptions_test.go index 303631fb8b..f193bf7e96 100644 --- a/sei-tendermint/internal/autobahn/avail/subscriptions_test.go +++ b/sei-tendermint/internal/autobahn/avail/subscriptions_test.go @@ -1,6 +1,7 @@ package avail import ( + "context" "testing" "time" @@ -24,7 +25,7 @@ func TestSubscribeLaneProposals_ErrLanePrunedAfterMapDrop(t *testing.T) { lane0 := state.LocalLane().OrPanic("genesis") want, err := state.ProduceLocalBlock(lane0, 0, types.GenPayload(rng)) require.NoError(t, err) - sub, err := state.SubscribeLaneProposals(0) + sub, err := state.SubscribeLaneProposals(lane0, 0) require.NoError(t, err) ep, err := registry.ActivateEpoch( @@ -33,7 +34,10 @@ func TestSubscribeLaneProposals_ErrLanePrunedAfterMapDrop(t *testing.T) { ) require.NoError(t, err) state.ApplyEpoch(ep) - _, err = state.SubscribeLaneProposals(0) + + // Wrong producer key is rejected even if that peer has a lane in the committee. + otherLane := types.NewLaneID(b.Public(), ep.EpochIndex()) + _, err = state.SubscribeLaneProposals(otherLane, 0) require.ErrorIs(t, err, ErrBadLane) got, err := sub.Recv(t.Context()) @@ -47,3 +51,109 @@ func TestSubscribeLaneProposals_ErrLanePrunedAfterMapDrop(t *testing.T) { _, err = sub.Recv(t.Context()) require.ErrorIs(t, err, ErrLanePruned) } + +func TestSubscribeLaneProposals_WrongValidator(t *testing.T) { + rng := utils.TestRng() + registry, keys := epoch.GenRegistry(rng, 2) + a, b := keys[0], keys[1] + db := memblock.NewBlockDB() + t.Cleanup(func() { require.NoError(t, db.Close()) }) + ds := utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, db)) + state := utils.OrPanic1(NewState(a, ds, utils.None[string]())) + + _, err := state.SubscribeLaneProposals(types.NewLaneID(b.Public(), 0), 0) + require.ErrorIs(t, err, ErrBadLane) +} + +// Leave → tip dispose → rejoin allocates a new LaneID; WaitLane skips the closed +// identity and Subscribe serves the new lane (StreamLaneProposals client path). +func TestWaitLane_LeaveRejoinNewLaneID(t *testing.T) { + ctx := t.Context() + rng := utils.TestRng() + registry, keys := epoch.GenRegistry(rng, 2) + a, b := keys[0], keys[1] + db := memblock.NewBlockDB() + t.Cleanup(func() { require.NoError(t, db.Close()) }) + ds := utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, db)) + state := utils.OrPanic1(NewState(a, ds, utils.None[string]())) + + lane0 := state.LocalLane().OrPanic("genesis") + got, err := state.WaitLane(ctx, a.Public(), utils.None[types.LaneID]()) + require.NoError(t, err) + require.Equal(t, lane0, got) + + sub, err := state.SubscribeLaneProposals(lane0, 0) + require.NoError(t, err) + _, err = state.ProduceLocalBlock(lane0, 0, types.GenPayload(rng)) + require.NoError(t, err) + _, err = sub.Recv(ctx) + require.NoError(t, err) + + // Leave: a out of committee. Leave map still serves until dispose. + epLeave, err := registry.ActivateEpoch( + map[types.PublicKey]uint64{b.Public(): 1}, + types.OpenRoadRange(), time.Time{}, registry.FirstBlock(), + ) + require.NoError(t, err) + state.ApplyEpoch(epLeave) + require.False(t, state.LocalLane().IsPresent()) + + // Tip dispose of leave map → stream ends (ErrLanePruned). + for inner, ctrl := range state.inner.Lock() { + inner.dropLanes([]types.LaneID{lane0}) + ctrl.Updated() + } + _, err = sub.Recv(ctx) + require.ErrorIs(t, err, ErrLanePruned) + + // Client would WaitLane(..., exclude=lane0); must not accept the closed identity. + waitCtx, cancel := context.WithCancel(ctx) + done := make(chan types.LaneID, 1) + go func() { + lane, err := state.WaitLane(waitCtx, a.Public(), utils.Some(lane0)) + if err == nil { + done <- lane + } + close(done) + }() + select { + case <-done: + t.Fatal("WaitLane returned before rejoin") + case <-time.After(20 * time.Millisecond): + } + + // Rejoin under a new LaneID (Joined = leave epoch index). + epJoin, err := registry.ActivateEpoch( + map[types.PublicKey]uint64{a.Public(): 1, b.Public(): 1}, + types.OpenRoadRange(), time.Time{}, registry.FirstBlock(), + ) + require.NoError(t, err) + state.ApplyEpoch(epJoin) + lane1 := state.LocalLane().OrPanic("rejoin") + require.NotEqual(t, lane0, lane1) + require.Equal(t, a.Public(), lane1.Validator) + + select { + case got := <-done: + require.Equal(t, lane1, got) + case <-time.After(time.Second): + cancel() + t.Fatal("WaitLane did not observe rejoin LaneID") + } + cancel() + + // New subscribe on the rejoin lane; closed lane0 still key-ok but map gone → prune on Recv. + sub0, err := state.SubscribeLaneProposals(lane0, 0) + require.NoError(t, err) + _, err = sub0.Recv(ctx) + require.ErrorIs(t, err, ErrLanePruned) + + sub1, err := state.SubscribeLaneProposals(lane1, 0) + require.NoError(t, err) + want, err := state.ProduceLocalBlock(lane1, 0, types.GenPayload(rng)) + require.NoError(t, err) + gotBlk, err := sub1.Recv(ctx) + require.NoError(t, err) + require.Equal(t, want.Msg().Block().Header().Hash(), gotBlk.Msg().Block().Header().Hash()) + require.Equal(t, lane1, gotBlk.Msg().Block().Header().Lane()) +} diff --git a/sei-tendermint/internal/autobahn/avail/testonly.go b/sei-tendermint/internal/autobahn/avail/testonly.go index 6f8f446f8b..cc1783b803 100644 --- a/sei-tendermint/internal/autobahn/avail/testonly.go +++ b/sei-tendermint/internal/autobahn/avail/testonly.go @@ -13,7 +13,11 @@ func RunTestNetwork(ctx context.Context, states []*State) error { for _, from := range states { for _, to := range states { s.Spawn(func() error { - sub, err := from.SubscribeLaneProposals(0) + lane, ok := from.LocalLane().Get() + if !ok { + return errors.New("SubscribeLaneProposals: no local lane") + } + sub, err := from.SubscribeLaneProposals(lane, 0) if err != nil { return err } diff --git a/sei-tendermint/internal/p2p/giga/api.proto b/sei-tendermint/internal/p2p/giga/api.proto index 10ac870470..f425a5c036 100644 --- a/sei-tendermint/internal/p2p/giga/api.proto +++ b/sei-tendermint/internal/p2p/giga/api.proto @@ -43,6 +43,7 @@ message AppVote { message StreamLaneProposalsReq { option (wireguard.sized) = true; uint64 first_block_number = 1; + optional autobahn.LaneID lane_id = 2; // required } message StreamAppQCsReq { diff --git a/sei-tendermint/internal/p2p/giga/avail.go b/sei-tendermint/internal/p2p/giga/avail.go index 5e38b8ebbd..0f796e6f44 100644 --- a/sei-tendermint/internal/p2p/giga/avail.go +++ b/sei-tendermint/internal/p2p/giga/avail.go @@ -9,12 +9,11 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/avail" apb "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/pb" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/giga/pb" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/mux" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/rpc" - "github.com/sei-protocol/seilog" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" ) -var logger = seilog.NewLogger("tendermint", "internal", "p2p", "giga") - func (x *Service) serverStreamLaneProposals(ctx context.Context, server rpc.Server[API]) error { return StreamLaneProposals.Serve(ctx, server, func(ctx context.Context, stream rpc.Stream[*pb.LaneProposal, *pb.StreamLaneProposalsReq]) error { reqRaw, err := stream.Recv(ctx) @@ -25,51 +24,27 @@ func (x *Service) serverStreamLaneProposals(ctx context.Context, server rpc.Serv if err != nil { return fmt.Errorf("StreamLaneProposalsReqConv.Decode(): %w", err) } - // ErrLanePruned ends the stream; leave alone keeps serving. Do not bubble — - // wait and resubscribe (rejoin tip is 0; back-leash prunes before rejoin). - first := req.FirstBlockNumber + sub, err := x.validatorState().Avail().SubscribeLaneProposals(req.LaneID, req.FirstBlockNumber) + if err != nil { + return err + } for { - sub, err := x.subscribeLaneProposals(ctx, first) + p, err := sub.Recv(ctx) if err != nil { + // Lane closed / tipcut pruned: end the stream cleanly so the client + // can wait for a new LaneID of this producer. + if errors.Is(err, avail.ErrLanePruned) { + return nil + } return err } - for { - p, err := sub.Recv(ctx) - if err != nil { - if errors.Is(err, avail.ErrLanePruned) { - logger.Info("StreamLaneProposals: leave-lane tipcut pruned; pausing until resubscribe") - first = 0 - break - } - return err - } - if err := stream.Send(ctx, LaneProposalConv.Encode(p)); err != nil { - return fmt.Errorf("stream.Send(): %w", err) - } + if err := stream.Send(ctx, LaneProposalConv.Encode(p)); err != nil { + return fmt.Errorf("stream.Send(): %w", err) } } }) } -// subscribeLaneProposals waits for LocalLane then binds Subscribe; ErrBadLane → retry. -func (x *Service) subscribeLaneProposals(ctx context.Context, first types.BlockNumber) (*avail.LaneProposalsRecv, error) { - a := x.validatorState().Avail() - for { - sub, err := a.SubscribeLaneProposals(first) - if err == nil { - return sub, nil - } - if !errors.Is(err, avail.ErrBadLane) { - return nil, err - } - logger.Info("StreamLaneProposals: not a committee lane member; waiting to subscribe") - if _, err := a.WaitForLocalLane(ctx); err != nil { - return nil, err - } - first = 0 - } -} - func (x *Service) serverStreamLaneVotes(ctx context.Context, server rpc.Server[API]) error { return StreamLaneVotes.Serve(ctx, server, func(ctx context.Context, stream rpc.Stream[*pb.LaneVote, *pb.StreamLaneVotesReq]) error { reqRaw, err := stream.Recv(ctx) @@ -156,34 +131,68 @@ func (x *Service) serverStreamCommitQCs(ctx context.Context, server rpc.Server[A }) } -func (x *Service) clientStreamLaneProposals(ctx context.Context, c rpc.Client[API]) error { +func (x *Service) clientStreamLaneProposals(ctx context.Context, c rpc.Client[API], peer types.PublicKey) error { + a := x.validatorState().Avail() + var exclude utils.Option[types.LaneID] + first := types.BlockNumber(0) + for ctx.Err() == nil { + lane, err := a.WaitLane(ctx, peer, exclude) + if err != nil { + return err + } + if err := x.streamLaneProposalsOnce(ctx, c, lane, first); err != nil { + return err + } + // Stream ended. Only exclude when the applied committee has dropped or + // replaced this LaneID (leave / rejoin). If it is still present, reconnect + // to the same identity — a Stay / transport blip must not hang on + // WaitLane(exclude). Rejoin is at least one epoch after leave, so tip prune + // of the leave map lands before a new LaneID; we do not need to cancel the + // old stream early on rejoin. + cur, ok := a.Lane(peer).Get() + if !ok || cur != lane { + exclude = utils.Some(lane) + first = 0 + } else { + exclude = utils.None[types.LaneID]() + first = a.NextBlock(lane) + } + } + return ctx.Err() +} + +func (x *Service) streamLaneProposalsOnce(ctx context.Context, c rpc.Client[API], lane types.LaneID, first types.BlockNumber) error { stream, err := StreamLaneProposals.Call(ctx, c) if err != nil { return err } defer stream.Close() - req := &StreamLaneProposalsReq{} // TODO(gprusak): dissemination of LaneProposals is the main source of bandwidth consumption. // * to keep low latency, we need to push the lane proposals (streaming is required) - // * to avoid wasting bandwidth, we should set req.FirstBlockNumber (for that we need to authenticate validator in handshake) - // * the current implementation assumes a fully connected network - with a different topology we will need to be smarter. + // * to avoid wasting bandwidth, set FirstBlockNumber from local tip once peers are authenticated + req := &StreamLaneProposalsReq{LaneID: lane, FirstBlockNumber: first} if err := stream.Send(ctx, StreamLaneProposalsReqConv.Encode(req)); err != nil { return fmt.Errorf("client.StreamLaneProposals(): %w", err) } for { rawProposal, err := stream.Recv(ctx) if err != nil { + if ctx.Err() != nil { + return ctx.Err() + } + // Server closed after lane prune (handler returns nil → mux CLOSE). + if errors.Is(err, mux.ErrRemoteClosed) { + return nil + } return fmt.Errorf("stream.Recv(): %w", err) } proposal, err := LaneProposalConv.Decode(rawProposal) if err != nil { return fmt.Errorf("LaneProposalConv.Decode(): %w", err) } - // Sanity check, checking that the producer only sends their own proposals. - // TODO(gprusak): authenticate the peer to be able to do this check. - /*if got, want := proposal.Msg().Block().Header().Lane(), c.cfg.GetKey(); got != want { - return fmt.Errorf("producer = %q, want %q", got, want) - }*/ + if proposal.Msg().Block().Header().Lane() != lane { + return fmt.Errorf("producer lane = %v, want %v", proposal.Msg().Block().Header().Lane(), lane) + } if err := x.validatorState().Avail().PushBlock(ctx, proposal); err != nil { return fmt.Errorf("s.PushLaneProposal(): %w", err) } diff --git a/sei-tendermint/internal/p2p/giga/data_test.go b/sei-tendermint/internal/p2p/giga/data_test.go index 1e90e455ac..bbeb2e7bda 100644 --- a/sei-tendermint/internal/p2p/giga/data_test.go +++ b/sei-tendermint/internal/p2p/giga/data_test.go @@ -78,7 +78,7 @@ func (e *testEnv) AddNode(key types.SecretKey) *testNode { func (e *testEnv) Run(ctx context.Context) error { return utils.IgnoreAfterCancel(ctx, scope.Run(ctx, func(ctx context.Context, s scope.Scope) error { - for _, x := range e.nodes { + for xKey, x := range e.nodes { s.SpawnNamed("node", func() error { return x.Run(ctx) }) for _, y := range e.nodes { xConn, yConn := conn.NewTestConn() @@ -87,7 +87,7 @@ func (e *testEnv) Run(ctx context.Context) error { s.SpawnNamed("mux server", func() error { return server.Run(ctx, xConn) }) s.SpawnNamed("mux client", func() error { return client.Run(ctx, yConn) }) s.SpawnNamed("RunServer", func() error { return x.service.RunServer(ctx, server) }) - s.SpawnNamed("RunClient", func() error { return y.service.RunClient(ctx, client, true) }) + s.SpawnNamed("RunClient", func() error { return y.service.RunClient(ctx, client, xKey, true) }) } } return nil diff --git a/sei-tendermint/internal/p2p/giga/pb/api.pb.go b/sei-tendermint/internal/p2p/giga/pb/api.pb.go index 21987c8ed9..48b33255e8 100644 --- a/sei-tendermint/internal/p2p/giga/pb/api.pb.go +++ b/sei-tendermint/internal/p2p/giga/pb/api.pb.go @@ -266,6 +266,7 @@ func (x *AppVote) GetAppVoteV2() *pb.SignedAppVote { type StreamLaneProposalsReq struct { state protoimpl.MessageState `protogen:"open.v1"` FirstBlockNumber uint64 `protobuf:"varint,1,opt,name=first_block_number,json=firstBlockNumber,proto3" json:"first_block_number,omitempty"` + LaneId *pb.LaneID `protobuf:"bytes,2,opt,name=lane_id,json=laneId,proto3,oneof" json:"lane_id,omitempty"` // required unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -307,6 +308,13 @@ func (x *StreamLaneProposalsReq) GetFirstBlockNumber() uint64 { return 0 } +func (x *StreamLaneProposalsReq) GetLaneId() *pb.LaneID { + if x != nil { + return x.LaneId + } + return nil +} + type StreamAppQCsReq struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -649,9 +657,12 @@ const file_p2p_giga_api_proto_rawDesc = "" + "\fLaneProposal\x12?\n" + "\x10lane_proposal_v2\x18\x02 \x01(\v2\x15.autobahn.SignedBlockR\x0elaneProposalV2:\x06\xe8\x88\xe2\xab\f\x01J\x04\b\x01\x10\x02R\rlane_proposal\"Z\n" + "\aAppVote\x127\n" + - "\vapp_vote_v2\x18\x02 \x01(\v2\x17.autobahn.SignedAppVoteR\tappVoteV2:\x06\xe8\x88\xe2\xab\f\x01J\x04\b\x01\x10\x02R\bapp_vote\"N\n" + + "\vapp_vote_v2\x18\x02 \x01(\v2\x17.autobahn.SignedAppVoteR\tappVoteV2:\x06\xe8\x88\xe2\xab\f\x01J\x04\b\x01\x10\x02R\bapp_vote\"\x8a\x01\n" + "\x16StreamLaneProposalsReq\x12,\n" + - "\x12first_block_number\x18\x01 \x01(\x04R\x10firstBlockNumber:\x06\xe8\x88\xe2\xab\f\x01\"\x19\n" + + "\x12first_block_number\x18\x01 \x01(\x04R\x10firstBlockNumber\x12.\n" + + "\alane_id\x18\x02 \x01(\v2\x10.autobahn.LaneIDH\x00R\x06laneId\x88\x01\x01:\x06\xe8\x88\xe2\xab\f\x01B\n" + + "\n" + + "\b_lane_id\"\x19\n" + "\x0fStreamAppQCsReq:\x06\xe8\x88\xe2\xab\f\x01\"s\n" + "\x10StreamAppQCsResp\x12&\n" + "\x06app_qc\x18\x01 \x01(\v2\x0f.autobahn.AppQCR\x05appQc\x12/\n" + @@ -700,22 +711,24 @@ var file_p2p_giga_api_proto_goTypes = []any{ (*pb.SignedBlockHeader)(nil), // 15: autobahn.SignedBlockHeader (*pb.SignedBlock)(nil), // 16: autobahn.SignedBlock (*pb.SignedAppVote)(nil), // 17: autobahn.SignedAppVote - (*pb.AppQC)(nil), // 18: autobahn.AppQC - (*pb.CommitQC)(nil), // 19: autobahn.CommitQC - (*pb.Block)(nil), // 20: autobahn.Block + (*pb.LaneID)(nil), // 18: autobahn.LaneID + (*pb.AppQC)(nil), // 19: autobahn.AppQC + (*pb.CommitQC)(nil), // 20: autobahn.CommitQC + (*pb.Block)(nil), // 21: autobahn.Block } var file_p2p_giga_api_proto_depIdxs = []int32{ 15, // 0: p2p.giga.LaneVote.lane_vote_v2:type_name -> autobahn.SignedBlockHeader 16, // 1: p2p.giga.LaneProposal.lane_proposal_v2:type_name -> autobahn.SignedBlock 17, // 2: p2p.giga.AppVote.app_vote_v2:type_name -> autobahn.SignedAppVote - 18, // 3: p2p.giga.StreamAppQCsResp.app_qc:type_name -> autobahn.AppQC - 19, // 4: p2p.giga.StreamAppQCsResp.commit_qc:type_name -> autobahn.CommitQC - 20, // 5: p2p.giga.GetBlockResp.block:type_name -> autobahn.Block - 6, // [6:6] is the sub-list for method output_type - 6, // [6:6] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 18, // 3: p2p.giga.StreamLaneProposalsReq.lane_id:type_name -> autobahn.LaneID + 19, // 4: p2p.giga.StreamAppQCsResp.app_qc:type_name -> autobahn.AppQC + 20, // 5: p2p.giga.StreamAppQCsResp.commit_qc:type_name -> autobahn.CommitQC + 21, // 6: p2p.giga.GetBlockResp.block:type_name -> autobahn.Block + 7, // [7:7] is the sub-list for method output_type + 7, // [7:7] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_p2p_giga_api_proto_init() } @@ -723,6 +736,7 @@ func file_p2p_giga_api_proto_init() { if File_p2p_giga_api_proto != nil { return } + file_p2p_giga_api_proto_msgTypes[6].OneofWrappers = []any{} file_p2p_giga_api_proto_msgTypes[13].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ diff --git a/sei-tendermint/internal/p2p/giga/pb/api.wireguard.go b/sei-tendermint/internal/p2p/giga/pb/api.wireguard.go index ff1241a359..c1e76c87c8 100644 --- a/sei-tendermint/internal/p2p/giga/pb/api.wireguard.go +++ b/sei-tendermint/internal/p2p/giga/pb/api.wireguard.go @@ -33,7 +33,7 @@ func (*AppVote) MaxSize() int { } func (*StreamLaneProposalsReq) MaxSize() int { - return 11 + return 60 } func (*StreamAppQCsReq) MaxSize() int { @@ -96,6 +96,7 @@ func init() { // Register the wireguard.Schema generated for p2p.giga.StreamLaneProposalsReq. runtime.MustRegister[*StreamLaneProposalsReq](runtime.Schema{ 1: {MaxCount: 1}, + 2: {MaxCount: 1, Nested: utils.Some(reflect.TypeFor[*pb.LaneID]())}, }) // Register the wireguard.Schema generated for p2p.giga.StreamAppQCsReq. diff --git a/sei-tendermint/internal/p2p/giga/service.go b/sei-tendermint/internal/p2p/giga/service.go index 72cff7cc95..a1c020765d 100644 --- a/sei-tendermint/internal/p2p/giga/service.go +++ b/sei-tendermint/internal/p2p/giga/service.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/consensus" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/data" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/rpc" @@ -81,7 +82,7 @@ func (x *Service) RunServer(ctx context.Context, server rpc.Server[API]) error { }) } -func (x *Service) RunClient(ctx context.Context, client rpc.Client[API], getBlock bool) error { +func (x *Service) RunClient(ctx context.Context, client rpc.Client[API], peer types.PublicKey, getBlock bool) error { // TODO: implement a uniform robust GetBlock peer-selection / retry strategy // so connections that lack a height (including self) do not need a separate // getBlock=false path to avoid starving the shared fetch queue. @@ -89,7 +90,7 @@ func (x *Service) RunClient(ctx context.Context, client rpc.Client[API], getBloc s.Spawn(func() error { return x.clientPing(ctx, client) }) s.Spawn(func() error { return x.clientConsensus(ctx, client) }) s.Spawn(func() error { return x.clientStreamFullCommitQCs(ctx, client) }) - s.Spawn(func() error { return x.clientStreamLaneProposals(ctx, client) }) + s.Spawn(func() error { return x.clientStreamLaneProposals(ctx, client, peer) }) s.Spawn(func() error { return x.clientStreamLaneVotes(ctx, client) }) s.Spawn(func() error { return x.clientStreamCommitQCs(ctx, client) }) s.Spawn(func() error { return x.clientStreamAppVotes(ctx, client) }) diff --git a/sei-tendermint/internal/p2p/giga/types.go b/sei-tendermint/internal/p2p/giga/types.go index d742224f45..a19ebb08c0 100644 --- a/sei-tendermint/internal/p2p/giga/types.go +++ b/sei-tendermint/internal/p2p/giga/types.go @@ -10,6 +10,7 @@ import ( ) type StreamLaneProposalsReq struct { + LaneID types.LaneID FirstBlockNumber types.BlockNumber } @@ -73,10 +74,20 @@ var AppVoteConv = protoutils.Conv[*types.Signed[*types.AppVote], *pb.AppVote]{ var StreamLaneProposalsReqConv = protoutils.Conv[*StreamLaneProposalsReq, *pb.StreamLaneProposalsReq]{ Encode: func(m *StreamLaneProposalsReq) *pb.StreamLaneProposalsReq { - return &pb.StreamLaneProposalsReq{FirstBlockNumber: uint64(m.FirstBlockNumber)} + return &pb.StreamLaneProposalsReq{ + LaneId: types.LaneIDConv.Encode(m.LaneID), + FirstBlockNumber: uint64(m.FirstBlockNumber), + } }, Decode: func(m *pb.StreamLaneProposalsReq) (*StreamLaneProposalsReq, error) { - return &StreamLaneProposalsReq{FirstBlockNumber: types.BlockNumber(m.FirstBlockNumber)}, nil + lane, err := types.LaneIDConv.DecodeReq(m.LaneId) + if err != nil { + return nil, fmt.Errorf("lane_id: %w", err) + } + return &StreamLaneProposalsReq{ + LaneID: lane, + FirstBlockNumber: types.BlockNumber(m.FirstBlockNumber), + }, nil }, } diff --git a/sei-tendermint/internal/p2p/giga/types_test.go b/sei-tendermint/internal/p2p/giga/types_test.go index 4b5bfb19e3..cb9eba4dc4 100644 --- a/sei-tendermint/internal/p2p/giga/types_test.go +++ b/sei-tendermint/internal/p2p/giga/types_test.go @@ -15,7 +15,10 @@ func TestConv(t *testing.T) { LaneVoteConv.Test(types.GenSigned(rng, types.GenLaneVote(rng))), LaneProposalConv.Test(types.GenSigned(rng, types.GenLaneProposal(rng))), AppVoteConv.Test(types.GenSigned(rng, types.GenAppVote(rng))), - StreamLaneProposalsReqConv.Test(&StreamLaneProposalsReq{FirstBlockNumber: types.GenBlockNumber(rng)}), + StreamLaneProposalsReqConv.Test(&StreamLaneProposalsReq{ + LaneID: types.GenLaneID(rng), + FirstBlockNumber: types.GenBlockNumber(rng), + }), StreamAppQCsRespConv.Test(&StreamAppQCsResp{ AppQC: types.GenAppQC(rng), CommitQC: types.GenCommitQC(rng), diff --git a/sei-tendermint/internal/p2p/giga_router_validator.go b/sei-tendermint/internal/p2p/giga_router_validator.go index 1fe0fca384..78eb2dd8a6 100644 --- a/sei-tendermint/internal/p2p/giga_router_validator.go +++ b/sei-tendermint/internal/p2p/giga_router_validator.go @@ -72,12 +72,12 @@ func (r *gigaValidatorRouter) Run(ctx context.Context) error { // (r.key.Public), not validatorKey (consensus signing key used by // EvmProxy): GigaNodeAddr.Key is a NodePublicKey. selfKey := r.key.Public() - for _, addr := range r.cfg.ValidatorAddrs { + for validatorKey, addr := range r.cfg.ValidatorAddrs { getBlock := addr.Key != selfKey s.Spawn(func() error { for { err := r.dialAndRunConn(ctx, utils.Some(addr.Key), addr.HostPort, func(ctx context.Context, client rpc.Client[giga.API]) error { - return r.service.RunClient(ctx, client, getBlock) + return r.service.RunClient(ctx, client, validatorKey, getBlock) }) logger.Info("giga connection failed", "addr", addr, "err", err) if err := utils.Sleep(ctx, r.cfg.DialInterval); err != nil { diff --git a/sei-tendermint/internal/p2p/mux/stream.go b/sei-tendermint/internal/p2p/mux/stream.go index 9f9b03a378..1292eff21e 100644 --- a/sei-tendermint/internal/p2p/mux/stream.go +++ b/sei-tendermint/internal/p2p/mux/stream.go @@ -8,9 +8,11 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" ) -var errRemoteClosed = errors.New("remote closed") +var ErrRemoteClosed = errors.New("remote closed") var errClosed = errors.New("closed") +var errRemoteClosed = ErrRemoteClosed // in-package alias + type Stream struct { state *streamState queue *utils.Watch[queue] From 31c10ec55ace262302c370d66305dfc4df69f18c Mon Sep 17 00:00:00 2001 From: Wen Date: Mon, 10 Aug 2026 21:39:29 -0700 Subject: [PATCH 13/14] refactor(autobahn): rename produceSession to runMempool Match the optional mempool session helper to what it actually runs. Co-authored-by: Cursor --- sei-tendermint/internal/autobahn/producer/state.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sei-tendermint/internal/autobahn/producer/state.go b/sei-tendermint/internal/autobahn/producer/state.go index f87571e221..e2a4424163 100644 --- a/sei-tendermint/internal/autobahn/producer/state.go +++ b/sei-tendermint/internal/autobahn/producer/state.go @@ -100,7 +100,7 @@ func (s *State) Run(ctx context.Context) error { err = utils.IgnoreCancel(scope.Run(ctx, func(ctx context.Context, sc scope.Scope) error { sc.Spawn(func() error { - return s.produceSession(ctx, availState, lane) + return s.runMempool(ctx, availState, lane) }) sc.Spawn(func() error { // Cancels seal / executed waits that do not observe committee. @@ -119,7 +119,7 @@ func (s *State) Run(ctx context.Context) error { return ctx.Err() } -func (s *State) produceSession(ctx context.Context, availState *avail.State, lane types.LaneID) error { +func (s *State) runMempool(ctx context.Context, availState *avail.State, lane types.LaneID) error { m := s.alignMempool(lane) firstBlock := m.first return scope.Run(ctx, func(ctx context.Context, scope scope.Scope) error { From 3a79c20bdb06c40c54550939fac255d797e42b3b Mon Sep 17 00:00:00 2001 From: Wen Date: Mon, 10 Aug 2026 21:58:26 -0700 Subject: [PATCH 14/14] fix(autobahn): address seidroid nits on mempool, mux, docs Re-check the live mempool under pruneMempool's lock, drop the errRemoteClosed alias, rename the shadowed prune-anchor epoch, and document silent PushBlock/PushVote on missing maps. Co-authored-by: Cursor --- .../internal/autobahn/avail/inner.go | 4 ++-- .../internal/autobahn/avail/state.go | 7 +++++-- .../internal/autobahn/producer/mempool.go | 5 ++++- sei-tendermint/internal/p2p/mux/mux_test.go | 19 ++++++++++--------- sei-tendermint/internal/p2p/mux/stream.go | 6 ++---- 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/sei-tendermint/internal/autobahn/avail/inner.go b/sei-tendermint/internal/autobahn/avail/inner.go index 05d8544737..2fa229a744 100644 --- a/sei-tendermint/internal/autobahn/avail/inner.go +++ b/sei-tendermint/internal/autobahn/avail/inner.go @@ -98,11 +98,11 @@ func newInner(nextCommitQCEpoch *types.Epoch, registry *epoch.Registry, loaded u var anchorCommittee *types.Committee if anchor, ok := l.pruneAnchor.Get(); ok { anchorEpoch = anchor.CommitQC.Proposal().EpochIndex() - ep, ok := registry.EpochByIndex(anchorEpoch) + anchorEp, ok := registry.EpochByIndex(anchorEpoch) if !ok { return nil, fmt.Errorf("unknown epoch_index %d for prune anchor", anchorEpoch) } - anchorCommittee = ep.Committee() + anchorCommittee = anchorEp.Committee() } for lane := range l.blocks { if anchorCommittee != nil && lane.Joined <= anchorEpoch && !anchorCommittee.HasLane(lane) { diff --git a/sei-tendermint/internal/autobahn/avail/state.go b/sei-tendermint/internal/autobahn/avail/state.go index dcfc6ab97f..3752361870 100644 --- a/sei-tendermint/internal/autobahn/avail/state.go +++ b/sei-tendermint/internal/autobahn/avail/state.go @@ -571,7 +571,9 @@ func (s *State) Block(ctx context.Context, lane types.LaneID, n types.BlockNumbe // PushBlock pushes a block to the state. // Waits until all previous blocks are available. -// No-op if the lane map is gone (pruned leave or not admitted). +// Missing map (tip-pruned leave, or a LaneID never admitted) is a silent no-op: +// VerifyInWindow already rejects forged lanes, and callers must not tear down +// peers over a disposed leave map. func (s *State) PushBlock(ctx context.Context, p *types.Signed[*types.LaneProposal]) error { h := p.Msg().Block().Header() if p.Key() != h.Lane().Validator { @@ -634,7 +636,8 @@ func (s *State) PushBlock(ctx context.Context, p *types.Signed[*types.LanePropos // PushVote pushes a LaneVote to the state. // Waits until the lane has enough capacity for the new vote. // It does NOT wait for the previous votes. -// No-op if the lane map is gone (pruned leave or not admitted). +// Missing map (tip-pruned leave, or a LaneID never admitted) is a silent no-op, +// same as PushBlock. func (s *State) PushVote(ctx context.Context, vote *types.Signed[*types.LaneVote]) error { if _, err := s.data.Registry().VerifyInWindow(func(c *types.Committee) error { if err := vote.Msg().Verify(c); err != nil { diff --git a/sei-tendermint/internal/autobahn/producer/mempool.go b/sei-tendermint/internal/autobahn/producer/mempool.go index cb8a06ad5c..e8092243eb 100644 --- a/sei-tendermint/internal/autobahn/producer/mempool.go +++ b/sei-tendermint/internal/autobahn/producer/mempool.go @@ -113,7 +113,10 @@ func (s *State) EvmTxByHash(hash common.Hash) (tmtypes.Tx, bool) { // Removes txs from mempool assigned to lane blocks inner.send.maxMsgSize { @@ -138,7 +136,7 @@ func (s *Stream) Recv(ctx context.Context, freeBuffer bool) ([]byte, error) { return nil, err } if inner.recv.begin == inner.recv.used { - return nil, errRemoteClosed + return nil, ErrRemoteClosed } i := inner.recv.begin % uint64(len(inner.recv.msgs)) msg := inner.recv.msgs[i]