Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 3 additions & 11 deletions sei-db/ledger_db/block/block_db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 3 additions & 12 deletions sei-db/ledger_db/block/blocksim/blocksim.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
wen-coding marked this conversation as resolved.
Comment thread
wen-coding marked this conversation as resolved.
return committee, keys, nil
}

Expand Down
8 changes: 2 additions & 6 deletions sei-tendermint/autobahn/types/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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[:],
Expand All @@ -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)
}
Expand Down
113 changes: 91 additions & 22 deletions sei-tendermint/autobahn/types/committee.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,14 @@ 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 + joined); weights are voting stake.
//
// 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 {
replicas ImSlice[PublicKey]
lanes ImSlice[LaneID] // in Replicas() order; one per member
byValidator map[PublicKey]LaneID
weights map[PublicKey]uint64
totalWeight uint64
}
Expand All @@ -36,25 +42,42 @@ 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.Joined == l.Joined
}

func (c *Committee) Lane(v PublicKey) utils.Option[LaneID] {
Comment thread
wen-coding marked this conversation as resolved.
lane, ok := c.byValidator[v]
if !ok {
return utils.None[LaneID]()
}
return utils.Some(lane)
}

// Lanes is the list of nodes which are eligible to produce blocks.
func (c *Committee) Lanes() ImSlice[LaneID] { return c.replicas }
// 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
}
}
}
}

// Replicas is the list of nodes which are eligible to participate in the consensus.
func (c *Committee) Replicas() ImSlice[PublicKey] { return c.replicas }
// 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 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
x.SetBytes32(h[:])
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() {
for k := range c.Replicas() {
w := c.weights[k]
if y < w {
return k
Expand Down Expand Up @@ -116,37 +139,83 @@ func (c *Committee) LaneQuorum() uint64 {
return c.Faulty() + 1
}

// NewCommittee is genesis: joined = 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 newCommittee(lanes, weights, totalWeight)
}

// 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("DeriveNext: 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 {
lanes = append(lanes, c.Lane(v).Or(NewLaneID(v, e)))
}
return newCommittee(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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

document what "normalization" actually mean here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

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
}

// 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 joined %d and %d",
lane.Validator, byValidator[lane.Validator].Joined, lane.Joined,
)
}
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]
}
replicas := slices.SortedFunc(maps.Keys(weights), func(a, b PublicKey) int { return a.Compare(b) })
return &Committee{
replicas: ImSlice[PublicKey]{replicas},
lanes: ImSlice[LaneID]{ordered},
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)
}
71 changes: 71 additions & 0 deletions sei-tendermint/autobahn/types/committee_activate_test.go
Original file line number Diff line number Diff line change
@@ -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 TestDeriveNext_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 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"))
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 := 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"))
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 := 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"))
require.Equal(t, NewLaneID(d, 3), c3.Lane(d).OrPanic("d"))
require.False(t, c3.HasLane(NewLaneID(d, 0)))
requireLanesSorted(t, c3)
}

func TestFinalizeCommittee_RejectsDuplicatePubKeyDifferentJoined(t *testing.T) {
rng := utils.TestRng()
v := GenSecretKey(rng).Public()
_, err := newCommittee(
[]LaneID{NewLaneID(v, 0), NewLaneID(v, 1)},
map[PublicKey]uint64{v: 1},
1,
)
require.Error(t, err)
}
19 changes: 8 additions & 11 deletions sei-tendermint/autobahn/types/committee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(committee.Lane(nonZeroWeightKey).OrPanic("member")) {
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)
Expand Down Expand Up @@ -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(keys[0].Public(), 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),
Expand Down Expand Up @@ -211,17 +212,13 @@ 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")
}
require.Error(t, lightMajority.Verify(ep, prev))
}

func TestNewCommittee_RejectsEmptyWeights(t *testing.T) {
Expand Down
Loading