Skip to content
Merged
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
49 changes: 49 additions & 0 deletions sei-tendermint/autobahn/types/committee.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (

"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
"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"
)

Expand Down Expand Up @@ -131,6 +133,53 @@ func NewCommittee(weights map[PublicKey]uint64) (*Committee, error) {
return newCommittee(nil, weights, 0)
}

// Equal reports whether c and other have the same validators, join epochs, and weights.
func (c *Committee) Equal(other *Committee) bool {
return maps.Equal(c.weights, other.weights) && maps.Equal(c.lanes, other.lanes)
}

var CommitteeConv = protoutils.Conv[*Committee, *pb.Committee]{
Encode: func(c *Committee) *pb.Committee {
record := &pb.Committee{Members: make([]*pb.EpochMember, 0, c.Lanes().Len())}
for lane := range c.Lanes().All() {
record.Members = append(record.Members, &pb.EpochMember{
LaneId: LaneIDConv.Encode(lane),
Weight: utils.Alloc(c.Weight(lane.Validator)),
})
}
return record
},
Decode: func(record *pb.Committee) (*Committee, error) {
if record == nil {
return nil, errors.New("missing")
}
weights := make(map[PublicKey]uint64, len(record.Members))
lanes := make(map[PublicKey]LaneID, len(record.Members))
for i, member := range record.Members {
if member == nil {
return nil, fmt.Errorf("member %d: missing", i)
}
if member.Weight == nil {
return nil, fmt.Errorf("member %d weight: missing", i)
}
if *member.Weight == 0 {
return nil, fmt.Errorf("member %d weight is 0", i)
}
lane, err := LaneIDConv.DecodeReq(member.LaneId)
if err != nil {
return nil, fmt.Errorf("member %d lane: %w", i, err)
}
validator := lane.Validator
if _, ok := weights[validator]; ok {
return nil, fmt.Errorf("duplicate public key %s", validator)
}
weights[validator] = *member.Weight
lanes[validator] = lane
}
return newCommittee(lanes, weights, 0)
},
}

// DeriveNext builds the committee for epoch e>0 from this committee:
// validators that remain keep Joined; new members get Joined = e.
func (c *Committee) DeriveNext(weights map[PublicKey]uint64, e EpochIndex) (*Committee, error) {
Expand Down
69 changes: 69 additions & 0 deletions sei-tendermint/autobahn/types/committee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"
"time"

"github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/pb"
"github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils"
"github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require"
)
Expand Down Expand Up @@ -36,6 +37,33 @@ func TestNewCommittee_FiltersOutZeroWeightValidators(t *testing.T) {
}
}

func TestCommitteeEqual_IgnoresMapOrder(t *testing.T) {
rng := utils.TestRng()
a := GenPublicKey(rng)
b := GenPublicKey(rng)
c1 := utils.OrPanic1(NewCommittee(map[PublicKey]uint64{a: 3, b: 1}))
c2 := utils.OrPanic1(NewCommittee(map[PublicKey]uint64{b: 1, a: 3}))
require.True(t, c1.Equal(c2))

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.

These are two relatively disjoint tests. Shoudln't they be two different tests?

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

}

func TestCommitteeEqual_IgnoresZeroWeights(t *testing.T) {
rng := utils.TestRng()
a := GenPublicKey(rng)
b := GenPublicKey(rng)
c1 := utils.OrPanic1(NewCommittee(map[PublicKey]uint64{a: 3}))
c2 := utils.OrPanic1(NewCommittee(map[PublicKey]uint64{a: 3, b: 0}))
require.True(t, c1.Equal(c2))
}

func TestCommitteeEqual_RejectsDifferentWeights(t *testing.T) {
rng := utils.TestRng()
a := GenPublicKey(rng)
b := GenPublicKey(rng)
c1 := utils.OrPanic1(NewCommittee(map[PublicKey]uint64{a: 3, b: 1}))
c2 := utils.OrPanic1(NewCommittee(map[PublicKey]uint64{a: 3, b: 2}))
require.False(t, c1.Equal(c2))
}

func TestNewCommittee_RejectsZeroTotalWeight(t *testing.T) {
rng := utils.TestRng()

Expand Down Expand Up @@ -291,3 +319,44 @@ func TestDeriveNext_StayLeaveRejoin(t *testing.T) {
require.False(t, c3.HasLane(LaneID{Validator: d, Joined: 0}))
requireLanesSorted(t, c3)
}

func TestCommitteeConv_RejectsZeroWeight(t *testing.T) {
rng := utils.TestRng()
lane := GenLaneID(rng)
_, err := CommitteeConv.Decode(&pb.Committee{Members: []*pb.EpochMember{{
LaneId: LaneIDConv.Encode(lane),
Weight: utils.Alloc[uint64](0),
}}})
require.Error(t, err)
}

func TestCommitteeConv_RejectsDuplicatePublicKey(t *testing.T) {
rng := utils.TestRng()
lane := GenLaneID(rng)
member := &pb.EpochMember{
LaneId: LaneIDConv.Encode(lane),
Weight: utils.Alloc[uint64](1),
}
_, err := CommitteeConv.Decode(&pb.Committee{Members: []*pb.EpochMember{member, member}})
require.Error(t, err)
}

func TestCommitteeConv_PreservesJoined(t *testing.T) {
rng := utils.TestRng()
a := GenPublicKey(rng)
b := GenPublicKey(rng)
c := GenPublicKey(rng)
d := GenPublicKey(rng)
genesis := utils.OrPanic1(NewCommittee(map[PublicKey]uint64{a: 1, c: 3}))
src := utils.OrPanic1(genesis.DeriveNext(map[PublicKey]uint64{a: 2, b: 4, c: 3}, 4))
got, err := CommitteeConv.Decode(CommitteeConv.Encode(src))
require.NoError(t, err)
require.Equal(t, src.Lane(a).OrPanic("a"), got.Lane(a).OrPanic("a"))
require.Equal(t, src.Lane(b).OrPanic("b"), got.Lane(b).OrPanic("b"))
require.Equal(t, src.Lane(c).OrPanic("c"), got.Lane(c).OrPanic("c"))
require.Equal(t, uint64(2), got.Weight(a))
require.Equal(t, uint64(4), got.Weight(b))
require.Equal(t, uint64(3), got.Weight(c))
require.Equal(t, EpochIndex(4), got.Lane(b).OrPanic("b").Joined)
require.False(t, got.HasReplica(d))
}
19 changes: 17 additions & 2 deletions sei-tendermint/autobahn/types/testonly.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,10 +331,25 @@ func GenProposalAt(rng utils.Rng, view View) *Proposal {
// Proposal.Verify accepts it (empty tipcuts are forbidden). For tests that care
// about signature weight or epoch binding rather than real lane/app data.
func ProposalAt(ep *Epoch, view View, globalFirst GlobalBlockNumber) *Proposal {
p, _ := ProposalAtBlocks(ep, view, globalFirst, 1)
return p
}

// ProposalAtBlocks is ProposalAt with a lane range of n blocks, n >= 1.
func ProposalAtBlocks(ep *Epoch, view View, globalFirst GlobalBlockNumber, n int) (*Proposal, []*Block) {
view.EpochIndex = ep.EpochIndex()
lane := ep.Committee().Lanes().At(0)
header := NewBlock(lane, 0, BlockHeaderHash{}, &Payload{}).Header()
return newProposal(view, time.Time{}, []*LaneRange{NewLaneRange(lane, 0, utils.Some(header))}, globalFirst)
blocks := make([]*Block, n)
var parent BlockHeaderHash
num := BlockNumber(0)
for i := 0; i < n; i++ {
b := NewBlock(lane, num, parent, &Payload{})
blocks[i] = b
parent = b.Header().Hash()
num = b.Header().Next()
}
header := blocks[n-1].Header()
return newProposal(view, time.Time{}, []*LaneRange{NewLaneRange(lane, 0, utils.Some(header))}, globalFirst), blocks
}

// GenProposalForEpoch generates a Proposal at a specific view whose epochIndex,
Expand Down
24 changes: 24 additions & 0 deletions sei-tendermint/internal/autobahn/autobahn.proto
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,27 @@ message ConsensusReq {
TimeoutQC timeout_qc = 5;
}
}

message Committee {
option (wireguard.sized) = true;
repeated EpochMember members = 1 [(wireguard.max_count) = 100];
}

// Committee for an execution-derived epoch.
message EpochRecord {
option (wireguard.sized) = true;
optional uint64 index = 1; // required
optional Committee committee = 2; // required
}

// Persisted execution-derived epoch registry state.
message PersistedEpochRegistry {
repeated EpochRecord live = 1;
optional EpochRecord pending = 2; // optional
}

message EpochMember {
option (wireguard.sized) = true;
optional LaneID lane_id = 1; // required
optional uint64 weight = 2; // required, can't be zero
}
5 changes: 1 addition & 4 deletions sei-tendermint/internal/autobahn/avail/inner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,6 @@ func TestAddLane_ReportsNewLaneForEachMembershipPeriod(t *testing.T) {
func TestRefreshConsensusSpec_WithholdsTipUntilNextViewEpochApplied(t *testing.T) {
rng := utils.TestRng()
registry, keys := epoch.GenRegistry(rng, 4)
registry.AdvanceIfNeeded(epoch.LastRoad(0))

ep0 := registry.MustEpoch(0)
ep1 := registry.MustEpoch(1)
Expand Down Expand Up @@ -273,9 +272,7 @@ func TestRefreshConsensusSpec_WithholdsTipUntilNextViewEpochApplied(t *testing.T

func TestPrune_LeavesAppliedToEpochAdvance(t *testing.T) {
rng := utils.TestRng()
registry, keys := epoch.GenRegistry(rng, 4)
registry.AdvanceIfNeeded(epoch.LastRoad(1))
registry.AdvanceIfNeeded(epoch.LastRoad(2))
registry, keys := epoch.GenRegistryThrough(rng, 4, 2)
ep0 := registry.MustEpoch(0)
ep1 := registry.MustEpoch(1)
ep2 := registry.MustEpoch(2)
Expand Down
Loading
Loading