diff --git a/avail.test b/avail.test new file mode 100755 index 0000000000..5b9671f8f3 Binary files /dev/null and b/avail.test differ diff --git a/sei-db/ledger_db/block/block_db_test.go b/sei-db/ledger_db/block/block_db_test.go index 3b905cd4f5..e525a55d09 100644 --- a/sei-db/ledger_db/block/block_db_test.go +++ b/sei-db/ledger_db/block/block_db_test.go @@ -253,7 +253,7 @@ func testPruneStraddleRetainsQC(t *testing.T, build builder) { require.NoError(t, err) got, ok := opt.Get() require.True(t, ok, "straddling QC must be retained") - require.Equal(t, straddled.first, got.QC().GlobalRange(committee).First) + require.Equal(t, straddled.first, got.QC().GlobalRange().First) } // testPruneIdempotentMonotonic asserts PruneBefore is idempotent and the @@ -485,7 +485,7 @@ func testReverseIteratorOrdering(t *testing.T, build builder) { } qc, err := qcIt.QC() require.NoError(t, err) - first := qc.QC().GlobalRange(committee).First + first := qc.QC().GlobalRange().First if qcCount == 0 { require.Equal(t, lastFirst, first, "reverse QCs must surface the last QC first") } @@ -525,8 +525,8 @@ func testResumeAfterRestart(t *testing.T, build builder) { prevQC, ok := recoverLastQC(t, db) require.True(t, ok) - require.Equal(t, last.first, prevQC.GlobalRange(committee).First, "recovered QC must be the last persisted QC") - require.Equal(t, last.next, prevQC.GlobalRange(committee).Next) + require.Equal(t, last.first, prevQC.GlobalRange().First, "recovered QC must be the last persisted QC") + require.Equal(t, last.next, prevQC.GlobalRange().Next) // The recovered QC's upper bound is exactly where the continuation begins; // writing the next contiguous batch must be accepted. @@ -712,7 +712,7 @@ func TestMemblockPruneRemovesBelowWatermark(t *testing.T) { } fqc, err := qcIt.QC() require.NoError(t, err) - require.GreaterOrEqual(t, fqc.QC().GlobalRange(committee).First, watermark, + require.GreaterOrEqual(t, fqc.QC().GlobalRange().First, watermark, "QC iterator must not surface pruned QCs") } require.NoError(t, qcIt.Close()) @@ -842,13 +842,13 @@ func assertBlocksReadable(t *testing.T, db types.BlockDB, batches []batch) { func assertQCsReadable(t *testing.T, db types.BlockDB, committee *types.Committee, batches []batch) { for _, b := range batches { - r := b.qc.QC().GlobalRange(committee) + r := b.qc.QC().GlobalRange() for n := r.First; n < r.Next; n++ { opt, err := db.ReadQCByBlockNumber(n) require.NoError(t, err) got, ok := opt.Get() require.True(t, ok, "QC covering %d should exist", n) - gr := got.QC().GlobalRange(committee) + gr := got.QC().GlobalRange() require.Equal(t, r.First, gr.First) require.Equal(t, r.Next, gr.Next) require.Len(t, got.Headers(), len(b.qc.Headers()), "QC must round-trip its full header set") @@ -902,7 +902,7 @@ func assertIterators(t *testing.T, db types.BlockDB, committee *types.Committee, } qc, err := qcIt.QC() require.NoError(t, err) - first := qc.QC().GlobalRange(committee).First + first := qc.QC().GlobalRange().First if haveQC { require.Greater(t, first, prevFirst, "QCs must iterate ascending by First") } @@ -960,7 +960,7 @@ func buildCommittee() (*types.Committee, []types.SecretKey) { keys[i] = types.GenSecretKey(rng) replicas[i] = keys[i].Public() } - committee := utils.OrPanic1(types.NewRoundRobinElection(replicas, 0, genesisTime)) + committee := utils.OrPanic1(types.NewRoundRobinElection(replicas)) return committee, keys } @@ -972,7 +972,7 @@ func generateBatches(committee *types.Committee, keys []types.SecretKey) []batch batches := make([]batch, 0, numBatches) for range numBatches { fqc, blocks := buildFullCommitQC(rng, committee, keys, prev) - r := fqc.QC().GlobalRange(committee) + r := fqc.QC().GlobalRange() batches = append(batches, batch{first: r.First, next: r.Next, blocks: blocks, qc: fqc}) prev = utils.Some(fqc.QC()) } @@ -991,18 +991,12 @@ func buildFullCommitQC( parent := bs[len(bs)-1] return types.NewBlock(producer, parent.Header().Next(), parent.Header().Hash(), types.GenPayload(rng)) } - return types.NewBlock( - producer, - types.LaneRangeOpt(prev, producer).Next(), - types.GenBlockHeaderHash(rng), - types.GenPayload(rng), - ) + return types.NewBlock(producer, types.LaneRangeOpt(prev, producer).Next(), types.GenBlockHeaderHash(rng), types.GenPayload(rng)) } for range blocksPerQC { producer := committee.Lanes().At(rng.Intn(committee.Lanes().Len())) blocks[producer] = append(blocks[producer], makeBlock(producer)) } - laneQCs := map[types.LaneID]*types.LaneQC{} var headers []*types.BlockHeader var blockList []*types.Block @@ -1015,35 +1009,16 @@ func buildFullCommitQC( } } } - - viewSpec := types.ViewSpec{CommitQC: prev} - leader := committee.Leader(viewSpec.View()) - var leaderKey types.SecretKey - for _, k := range keys { - if k.Public() == leader { - leaderKey = k - break - } - } - proposal := utils.OrPanic1(types.NewProposal( - leaderKey, - committee, - viewSpec, - genesisTime, - laneQCs, - func() utils.Option[*types.AppQC] { - if n := types.GlobalRangeOpt(prev, committee).Next; n > 0 { - p := types.NewAppProposal(n-1, viewSpec.View().Index, types.GenAppHash(rng)) - return utils.Some(testAppQC(keys, p)) - } - return utils.None[*types.AppQC]() - }(), - )) - votes := make([]*types.Signed[*types.CommitVote], 0, len(keys)) - for _, k := range keys { - votes = append(votes, types.Sign(k, types.NewCommitVote(proposal.Proposal().Msg()))) + var appQC utils.Option[*types.AppQC] + if cqc, ok := prev.Get(); ok { + vs := types.ViewSpec{CommitQC: prev} + p := types.NewAppProposal(cqc.GlobalRange().Next-1, vs.View().Index, types.GenAppHash(rng)) + appQC = utils.Some(testAppQC(keys, p)) + } else { + appQC = utils.None[*types.AppQC]() } - return types.NewFullCommitQC(types.NewCommitQC(votes), headers), blockList + cqc := types.BuildCommitQC(committee, keys, prev, 0, genesisTime, laneQCs, appQC) + return types.NewFullCommitQC(cqc, headers), blockList } func testLaneQC(keys []types.SecretKey, header *types.BlockHeader) *types.LaneQC { diff --git a/sei-db/ledger_db/block/blocksim/block_generator.go b/sei-db/ledger_db/block/blocksim/block_generator.go index 6d0b1d048f..b23e069501 100644 --- a/sei-db/ledger_db/block/blocksim/block_generator.go +++ b/sei-db/ledger_db/block/blocksim/block_generator.go @@ -74,7 +74,7 @@ func (g *BlockGenerator) mainLoop() { func (g *BlockGenerator) buildBatch() *generatedBatch { fqc, blocks := g.buildFullCommitQC() - r := fqc.QC().GlobalRange(g.committee) + r := fqc.QC().GlobalRange() g.prev = utils.Some(fqc.QC()) return &generatedBatch{first: r.First, next: r.Next, blocks: blocks, qc: fqc} } @@ -145,10 +145,13 @@ func (g *BlockGenerator) buildFullCommitQC() (*types.FullCommitQC, []*types.Bloc leaderKey, committee, viewSpec, + 0, + genesisTime, time.Now(), laneQCs, func() utils.Option[*types.AppQC] { - if n := types.GlobalRangeOpt(prev, committee).Next; n > 0 { + if cqc, ok := prev.Get(); ok { + n := cqc.GlobalRange().Next p := types.NewAppProposal(n-1, viewSpec.View().Index, types.GenAppHash(rng)) return utils.Some(testAppQC(keys, p)) } diff --git a/sei-db/ledger_db/block/blocksim/blocksim.go b/sei-db/ledger_db/block/blocksim/blocksim.go index 38eebfca5f..c601d94235 100644 --- a/sei-db/ledger_db/block/blocksim/blocksim.go +++ b/sei-db/ledger_db/block/blocksim/blocksim.go @@ -121,7 +121,7 @@ func NewBlockSim( // last QC's range — the next batch then appends contiguously. Block bytes // are irrelevant here (this is a DB stress test), so the backfill writes // freshly generated blocks under the already-persisted QC. - qcRange := prevQC.GlobalRange(committee) + qcRange := prevQC.GlobalRange() lastQCNext := uint64(qcRange.Next) firstMissing := uint64(qcRange.First) if h, ok := highestOpt.Get(); ok { @@ -263,7 +263,7 @@ func buildCommittee(rng tmutils.Rng, size int) (*types.Committee, []types.Secret keys[i] = types.GenSecretKey(rng) replicas[i] = keys[i].Public() } - committee, err := types.NewRoundRobinElection(replicas, 0, genesisTime) + committee, err := types.NewRoundRobinElection(replicas) if err != nil { return nil, nil, fmt.Errorf("failed to build committee: %w", err) } diff --git a/sei-db/ledger_db/block/blocksim/resume_test.go b/sei-db/ledger_db/block/blocksim/resume_test.go index da982d6a9b..1621b03b49 100644 --- a/sei-db/ledger_db/block/blocksim/resume_test.go +++ b/sei-db/ledger_db/block/blocksim/resume_test.go @@ -67,8 +67,8 @@ func TestRecoverResumeState(t *testing.T) { prevQC, ok := prev.Get() require.True(t, ok, "recovered prev QC must be present") - require.Equal(t, last.first, prevQC.GlobalRange(committee).First, "recovered QC must be the last persisted QC") - require.Equal(t, last.next, prevQC.GlobalRange(committee).Next) + require.Equal(t, last.first, prevQC.GlobalRange().First, "recovered QC must be the last persisted QC") + require.Equal(t, last.next, prevQC.GlobalRange().Next) // Empty-store sanity: a fresh dir recovers nothing. empty, err := openBlockDB(&BlocksimConfig{Backend: "litt", DataDir: t.TempDir(), LittRetentionSeconds: 1}) diff --git a/sei-tendermint/autobahn/types/app_proposal.go b/sei-tendermint/autobahn/types/app_proposal.go index 5566acc7fe..271f867309 100644 --- a/sei-tendermint/autobahn/types/app_proposal.go +++ b/sei-tendermint/autobahn/types/app_proposal.go @@ -44,7 +44,7 @@ func (m *AppProposal) Verify(c *Committee, qc *CommitQC) error { if got, want := m.RoadIndex(), qc.Proposal().Index(); got != want { return fmt.Errorf("roadIndex() = %v, want %v", got, want) } - if got, want := m.GlobalNumber(), qc.GlobalRange(c); got < want.First || got >= want.Next { + if got, want := m.GlobalNumber(), qc.GlobalRange(); got < want.First || got >= want.Next { return fmt.Errorf("globalNumber() = %v, want in range [%v,%v)", got, want.First, want.Next) } return nil diff --git a/sei-tendermint/autobahn/types/commit_qc.go b/sei-tendermint/autobahn/types/commit_qc.go index 3f2562cc78..d782d4ca4c 100644 --- a/sei-tendermint/autobahn/types/commit_qc.go +++ b/sei-tendermint/autobahn/types/commit_qc.go @@ -41,8 +41,8 @@ func (m *CommitQC) LaneRange(lane LaneID) *LaneRange { } // GlobalRange returns the finalized global block range. -func (m *CommitQC) GlobalRange(c *Committee) GlobalRange { - return m.Proposal().GlobalRange(c) +func (m *CommitQC) GlobalRange() GlobalRange { + return m.Proposal().GlobalRange() } // Verify verifies the CommitQC against the committee. @@ -60,7 +60,7 @@ type FullCommitQC struct { // NewFullCommitQC constructs a new FullCommitQC. func NewFullCommitQC(qc *CommitQC, headers []*BlockHeader) *FullCommitQC { - if got, want := len(headers), int(qc.Proposal().globalRangeWithoutOffset.Len()); got != want { //nolint:gosec // total lane range len is a small bounded value representing block count in a QC + if got, want := len(headers), int(qc.GlobalRange().Len()); got != want { //nolint:gosec // total lane range len is a small bounded value representing block count in a QC panic(fmt.Sprintf("headers length %d != finalized blocks %d", got, want)) } return &FullCommitQC{qc: qc, headers: headers} @@ -83,7 +83,7 @@ func (m *FullCommitQC) Verify(c *Committee) error { return fmt.Errorf("qC: %w", err) } n := uint64(0) - if want, got := int(m.qc.GlobalRange(c).Len()), len(m.headers); want != got { //nolint:gosec // global range len is a small bounded value representing block count in a QC + if want, got := int(m.qc.GlobalRange().Len()), len(m.headers); want != got { //nolint:gosec // global range len is a small bounded value representing block count in a QC return fmt.Errorf("len(headers) = %d, want %d", got, want) } for lane := range c.Lanes().All() { diff --git a/sei-tendermint/autobahn/types/committee.go b/sei-tendermint/autobahn/types/committee.go index 5984febda9..34da1345b3 100644 --- a/sei-tendermint/autobahn/types/committee.go +++ b/sei-tendermint/autobahn/types/committee.go @@ -8,7 +8,6 @@ import ( "iter" "maps" "slices" - "time" "github.com/ethereum/go-ethereum/common" "github.com/holiman/uint256" @@ -27,14 +26,6 @@ type Committee struct { replicas ImSlice[PublicKey] weights map[PublicKey]uint64 totalWeight uint64 - // Number of the first block of the chain. - // TODO: firstBlock is not really a part of the committee, - // but it does belong to a chain spec (or epoch spec/genesis/etc.), - // which should be passed around to verify autobahn messages. - // Once we introduce the chain spec it should wrap Committee and firstBlock. - firstBlock GlobalBlockNumber - // timestamp at genesis. All blocks need to have a timestamp later than genesis. - genesisTimestamp time.Time } const MaxValidators = 100 @@ -55,12 +46,6 @@ func (c *Committee) Lanes() ImSlice[LaneID] { return c.replicas } // Replicas is the list of nodes which are eligible to participate in the consensus. func (c *Committee) Replicas() ImSlice[PublicKey] { return c.replicas } -// FirstBlock is the index of the first global block finalized by this committee. -func (c *Committee) FirstBlock() GlobalBlockNumber { return c.firstBlock } - -// GenesisTimestamp is the timestamp at genesis. -func (c *Committee) GenesisTimestamp() time.Time { return c.genesisTimestamp } - // Deterministic random oracle selecting a replica with probability proportional to the weight. func (c *Committee) randomReplica(seed []byte) PublicKey { h := sha256.Sum256(seed[:]) @@ -131,7 +116,7 @@ func (c *Committee) LaneQuorum() uint64 { return c.Faulty() + 1 } -func NewCommittee(weights map[PublicKey]uint64, firstBlock GlobalBlockNumber, genesisTimestamp time.Time) (*Committee, error) { +func NewCommittee(weights map[PublicKey]uint64) (*Committee, error) { weights = maps.Clone(weights) totalWeight := uint64(0) for k, w := range weights { @@ -151,19 +136,17 @@ func NewCommittee(weights map[PublicKey]uint64, firstBlock GlobalBlockNumber, ge } replicas := slices.SortedFunc(maps.Keys(weights), func(a, b PublicKey) int { return a.Compare(b) }) return &Committee{ - replicas: ImSlice[PublicKey]{replicas}, - weights: weights, - totalWeight: totalWeight, - firstBlock: firstBlock, - genesisTimestamp: genesisTimestamp, + replicas: ImSlice[PublicKey]{replicas}, + weights: weights, + totalWeight: totalWeight, }, nil } -// NewRoundRobinElection creates a Committee with round robin election starting at firstBlock. -func NewRoundRobinElection(replicas []PublicKey, firstBlock GlobalBlockNumber, genesisTimestamp time.Time) (*Committee, error) { +// 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, firstBlock, genesisTimestamp) + return NewCommittee(weights) } diff --git a/sei-tendermint/autobahn/types/committee_test.go b/sei-tendermint/autobahn/types/committee_test.go index c893c064d3..3543e63c59 100644 --- a/sei-tendermint/autobahn/types/committee_test.go +++ b/sei-tendermint/autobahn/types/committee_test.go @@ -3,7 +3,6 @@ package types import ( "math" "testing" - "time" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" @@ -11,15 +10,13 @@ import ( func TestNewCommittee_FiltersOutZeroWeightValidators(t *testing.T) { rng := utils.TestRng() - firstBlock := GenGlobalBlockNumber(rng) - genesisTimestamp := time.Now() zeroWeightKey := GenPublicKey(rng) nonZeroWeightKey := GenPublicKey(rng) committee, err := NewCommittee(map[PublicKey]uint64{ zeroWeightKey: 0, nonZeroWeightKey: 7, - }, firstBlock, genesisTimestamp) + }) if err != nil { t.Fatalf("NewCommittee(): %v", err) } @@ -40,13 +37,11 @@ func TestNewCommittee_FiltersOutZeroWeightValidators(t *testing.T) { func TestNewCommittee_RejectsZeroTotalWeight(t *testing.T) { rng := utils.TestRng() - firstBlock := GenGlobalBlockNumber(rng) - genesisTimestamp := time.Now() _, err := NewCommittee(map[PublicKey]uint64{ GenPublicKey(rng): 0, GenPublicKey(rng): 0, - }, firstBlock, genesisTimestamp) + }) if err == nil { t.Fatal("NewCommittee() succeeded, want error") } @@ -54,13 +49,11 @@ func TestNewCommittee_RejectsZeroTotalWeight(t *testing.T) { func TestNewCommittee_RejectsWeightOverflow(t *testing.T) { rng := utils.TestRng() - firstBlock := GenGlobalBlockNumber(rng) - genesisTimestamp := time.Now() _, err := NewCommittee(map[PublicKey]uint64{ GenPublicKey(rng): math.MaxUint64, GenPublicKey(rng): 1, - }, firstBlock, genesisTimestamp) + }) if err == nil { t.Fatal("NewCommittee() succeeded, want error") } @@ -68,15 +61,13 @@ func TestNewCommittee_RejectsWeightOverflow(t *testing.T) { func TestNewCommittee_RejectsTooManyValidators(t *testing.T) { rng := utils.TestRng() - firstBlock := GenGlobalBlockNumber(rng) - genesisTimestamp := time.Now() weights := map[PublicKey]uint64{} for range MaxValidators + 1 { weights[GenPublicKey(rng)] = 1 } - _, err := NewCommittee(weights, firstBlock, genesisTimestamp) + _, err := NewCommittee(weights) if err == nil { t.Fatal("NewCommittee() succeeded, want error") } @@ -92,7 +83,7 @@ func makeCommittee() (*Committee, []SecretKey) { keys[0].Public(): 5, keys[1].Public(): 1, keys[2].Public(): 1, - }, 0, time.Now())), keys + })), keys } func TestLaneQCVerifyChecksWeight(t *testing.T) { diff --git a/sei-tendermint/autobahn/types/opt.go b/sei-tendermint/autobahn/types/opt.go index b6da4556ea..7dea90a3f4 100644 --- a/sei-tendermint/autobahn/types/opt.go +++ b/sei-tendermint/autobahn/types/opt.go @@ -46,16 +46,6 @@ func LaneRangeOpt[T interface { return NewLaneRange(lane, 0, utils.None[*BlockHeader]()) } -// GlobalRangeOpt defaults to an empty initial range. -func GlobalRangeOpt[T interface { - GlobalRange(c *Committee) GlobalRange -}](mv utils.Option[T], c *Committee) GlobalRange { - if v, ok := mv.Get(); ok { - return v.GlobalRange(c) - } - return GlobalRange{First: c.FirstBlock(), Next: c.FirstBlock()} -} - // AppOpt defaults to None. func AppOpt[T interface { App() utils.Option[*AppProposal] diff --git a/sei-tendermint/autobahn/types/proposal.go b/sei-tendermint/autobahn/types/proposal.go index a0e8bee0a7..1cbcb50c2e 100644 --- a/sei-tendermint/autobahn/types/proposal.go +++ b/sei-tendermint/autobahn/types/proposal.go @@ -108,8 +108,18 @@ type ViewSpec struct { // WARNING: currently we have implicit assumption that // TimeoutQC.View().Index == CommitQC.Index.Next(), // I.e. that TimeoutQC comes from the expected consensus instance. - CommitQC utils.Option[*CommitQC] - TimeoutQC utils.Option[*TimeoutQC] + CommitQC utils.Option[*CommitQC] + TimeoutQC utils.Option[*TimeoutQC] + FirstBlock GlobalBlockNumber // genesis InitialHeight; added to lane-relative block numbers to produce absolute global block numbers +} + +// NextGlobalBlock returns the first global block number expected in the next proposal. +// When CommitQC is present it equals CommitQC.GlobalRange().Next; otherwise it equals FirstBlock. +func (vs *ViewSpec) NextGlobalBlock() GlobalBlockNumber { + if cQC, ok := vs.CommitQC.Get(); ok { + return cQC.GlobalRange().Next + } + return vs.FirstBlock } // View is the view justified by vs. @@ -121,11 +131,11 @@ func (vs *ViewSpec) View() View { return View{Index: idx, Number: 0} } -func (vs *ViewSpec) NextTimestamp(c *Committee) time.Time { +func (vs *ViewSpec) NextTimestamp(genesisTimestamp time.Time) time.Time { if cQC, ok := vs.CommitQC.Get(); ok { return cQC.Proposal().NextTimestamp() } - return c.GenesisTimestamp() + return genesisTimestamp } // Proposal is the road tipcut proposal. @@ -133,33 +143,33 @@ func (vs *ViewSpec) NextTimestamp(c *Committee) time.Time { // AppQC could be nil if we haven't reached any quorum state hash. type Proposal struct { utils.ReadOnly - view View - timestamp time.Time - laneRanges map[LaneID]*LaneRange - app utils.Option[*AppProposal] - // derived - // WARNING: this is not a valid global range, because - // it does not take into consideration committee.FirstBlock(). - // We keep it precomputed just to optimize the GlobalRange call. - globalRangeWithoutOffset GlobalRange + view View + timestamp time.Time + laneRanges map[LaneID]*LaneRange + app utils.Option[*AppProposal] + globalRange GlobalRange + firstBlock GlobalBlockNumber } -func newProposal(view View, timestamp time.Time, laneRanges []*LaneRange, app utils.Option[*AppProposal]) *Proposal { +func newProposal(view View, timestamp time.Time, laneRanges []*LaneRange, app utils.Option[*AppProposal], firstBlock GlobalBlockNumber) *Proposal { laneRangesM := map[LaneID]*LaneRange{} - globalRangeWithoutOffset := GlobalRange{} + gr := GlobalRange{} for _, r := range laneRanges { laneRangesM[r.Lane()] = r } for _, r := range laneRangesM { - globalRangeWithoutOffset.First += GlobalBlockNumber(r.First()) - globalRangeWithoutOffset.Next += GlobalBlockNumber(r.Next()) + gr.First += GlobalBlockNumber(r.First()) + gr.Next += GlobalBlockNumber(r.Next()) } + gr.First += firstBlock + gr.Next += firstBlock return &Proposal{ - view: view, - timestamp: timestamp, - laneRanges: laneRangesM, - globalRangeWithoutOffset: globalRangeWithoutOffset, - app: app, + view: view, + timestamp: timestamp, + laneRanges: laneRangesM, + globalRange: gr, + firstBlock: firstBlock, + app: app, } } @@ -176,23 +186,17 @@ func (m *Proposal) Timestamp() time.Time { return m.timestamp } func (m *Proposal) App() utils.Option[*AppProposal] { return m.app } // GlobalRange returns the proposed global block range. -// To compute GlobalRange from lane ranges in proposal, -// we need to know the global number of the first block -// of the chain (c.FirstBlock()). -func (m *Proposal) GlobalRange(c *Committee) GlobalRange { - gr := m.globalRangeWithoutOffset - gr.First += c.FirstBlock() - gr.Next += c.FirstBlock() - return gr +func (m *Proposal) GlobalRange() GlobalRange { + return m.globalRange } // Arbitrary deterministic minimal diff between consecutive blocks. const minTimestampDiff = time.Microsecond // Monotone timestamp assigned to each block of the proposal. -// Returns None, if n doed not belong to the proposal's global range. -func (m *Proposal) BlockTimestamp(c *Committee, n GlobalBlockNumber) utils.Option[time.Time] { - gr := m.GlobalRange(c) +// Returns None if n does not belong to the proposal's global range. +func (m *Proposal) BlockTimestamp(n GlobalBlockNumber) utils.Option[time.Time] { + gr := m.GlobalRange() if !gr.Has(n) { return utils.None[time.Time]() } @@ -203,7 +207,7 @@ func (m *Proposal) BlockTimestamp(c *Committee, n GlobalBlockNumber) utils.Optio // Lowest allowed timestamp for the next index proposal. func (m *Proposal) NextTimestamp() time.Time { //nolint:gosec // TODO: do stricter timestamp validation before running in prod. - return m.Timestamp().Add(time.Duration(m.globalRangeWithoutOffset.Len()) * minTimestampDiff) + return m.Timestamp().Add(time.Duration(m.globalRange.Len()) * minTimestampDiff) } // Verify checks that every present lane range belongs to the committee @@ -261,6 +265,8 @@ func NewProposal( key SecretKey, committee *Committee, viewSpec ViewSpec, + firstBlock GlobalBlockNumber, + genesisTimestamp time.Time, timestamp time.Time, laneQCs map[LaneID]*LaneQC, appQC utils.Option[*AppQC], @@ -291,15 +297,15 @@ func NewProposal( } // If the new appProposal is from the future (which may happen if this node is behind), then clear appQC. // The proposal will be useless in this case, but at least it will be valid. - if a, ok := app.Get(); ok && a.GlobalNumber() >= GlobalRangeOpt(viewSpec.CommitQC, committee).Next { + if a, ok := app.Get(); ok && a.GlobalNumber() >= viewSpec.NextGlobalBlock() { app = utils.None[*AppProposal]() appQC = utils.None[*AppQC]() } // Normalize the creation timestamp. - if wantMin := viewSpec.NextTimestamp(committee); timestamp.Before(wantMin) { + if wantMin := viewSpec.NextTimestamp(genesisTimestamp); timestamp.Before(wantMin) { timestamp = wantMin } - proposal := newProposal(viewSpec.View(), timestamp, laneRanges, app) + proposal := newProposal(viewSpec.View(), timestamp, laneRanges, app, firstBlock) return &FullProposal{ proposal: Sign(key, proposal), @@ -329,17 +335,17 @@ func (m *FullProposal) TimeoutQC() utils.Option[*TimeoutQC] { } // Verify verifies the FullProposal against the current view. -func (m *FullProposal) Verify(c *Committee, vs ViewSpec) error { +func (m *FullProposal) Verify(c *Committee, vs ViewSpec, genesisTimestamp time.Time) error { return scope.Parallel(func(s scope.ParallelScope) error { // Does the view match? if got, want := m.proposal.Msg().View(), vs.View(); got != want { return fmt.Errorf("view = %v, want %v", m.View(), vs.View()) } - if got, want := m.proposal.Msg().GlobalRange(c).First, GlobalRangeOpt(vs.CommitQC, c).Next; got != want { + if got, want := m.proposal.Msg().GlobalRange().First, vs.NextGlobalBlock(); got != want { return fmt.Errorf("proposal.GlobalRange().First = %v, want %v", got, want) } // Is the timestamp monotone? - if got, wantMin := m.proposal.Msg().Timestamp(), vs.NextTimestamp(c); got.Before(wantMin) { + if got, wantMin := m.proposal.Msg().Timestamp(), vs.NextTimestamp(genesisTimestamp); got.Before(wantMin) { return fmt.Errorf("proposal.Timestamp() = %v, want >= %v", got, wantMin) } // Is proposer valid? @@ -428,7 +434,7 @@ func (m *FullProposal) Verify(c *Committee, vs ViewSpec) error { } return nil }) - if got, want := appQC.Proposal().GlobalNumber(), GlobalRangeOpt(vs.CommitQC, c).Next; got >= want { + if got, want := appQC.Proposal().GlobalNumber(), vs.NextGlobalBlock(); got >= want { return fmt.Errorf("appQC for block %v, while only %v blocks were finalized", got, want) } } @@ -501,10 +507,11 @@ var ProposalConv = protoutils.Conv[*Proposal, *pb.Proposal]{ } sort.Slice(laneRanges, func(i, j int) bool { return laneRanges[i].Lane().Compare(laneRanges[j].Lane()) < 0 }) return &pb.Proposal{ - View: ViewConv.Encode(m.view), - Timestamp: TimeConv.Encode(m.timestamp), - LaneRanges: LaneRangeConv.EncodeSlice(laneRanges), - App: AppProposalConv.EncodeOpt(m.app), + View: ViewConv.Encode(m.view), + Timestamp: TimeConv.Encode(m.timestamp), + LaneRanges: LaneRangeConv.EncodeSlice(laneRanges), + App: AppProposalConv.EncodeOpt(m.app), + GlobalFirst: utils.Alloc(uint64(m.firstBlock)), } }, Decode: func(m *pb.Proposal) (*Proposal, error) { @@ -524,12 +531,10 @@ var ProposalConv = protoutils.Conv[*Proposal, *pb.Proposal]{ if err != nil { return nil, fmt.Errorf("appQC: %w", err) } - proposal := newProposal( - view, - timestamp, - laneRanges, - app, - ) + if m.GlobalFirst == nil { + return nil, fmt.Errorf("global_first: missing") + } + proposal := newProposal(view, timestamp, laneRanges, app, GlobalBlockNumber(m.GetGlobalFirst())) if len(proposal.laneRanges) != len(laneRanges) { return nil, fmt.Errorf("laneRanges: duplicate ranges") } diff --git a/sei-tendermint/autobahn/types/proposal_test.go b/sei-tendermint/autobahn/types/proposal_test.go index bbc856c773..b97fd5b7d2 100644 --- a/sei-tendermint/autobahn/types/proposal_test.go +++ b/sei-tendermint/autobahn/types/proposal_test.go @@ -1,7 +1,6 @@ package types import ( - "slices" "testing" "time" @@ -62,16 +61,20 @@ func makeAppQCFor(keys []SecretKey, globalNum GlobalBlockNumber, roadIdx RoadInd func TestProposalVerifyFreshEmptyRanges(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} vs := ViewSpec{} proposerKey := leaderKey(committee, keys, vs.View()) - fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now(), nil, utils.None[*AppQC]())) - require.NoError(t, fp.Verify(committee, vs)) + fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, firstBlock, genesisTimestamp, time.Now(), nil, utils.None[*AppQC]())) + require.NoError(t, fp.Verify(committee, vs, genesisTimestamp)) } func TestProposalVerifyFreshWithBlocks(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} vs := ViewSpec{} proposerKey := leaderKey(committee, keys, vs.View()) @@ -79,14 +82,16 @@ func TestProposalVerifyFreshWithBlocks(t *testing.T) { lane := proposerKey.Public() laneQC := makeLaneQC(rng, committee, keys, lane, 0, GenBlockHeaderHash(rng)) - fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now(), + fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, firstBlock, genesisTimestamp, time.Now(), map[LaneID]*LaneQC{lane: laneQC}, utils.None[*AppQC]())) - require.NoError(t, fp.Verify(committee, vs)) + require.NoError(t, fp.Verify(committee, vs, genesisTimestamp)) } func TestProposalBlockTimestampStrictlyMonotone(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} vs0 := ViewSpec{} proposer0 := leaderKey(committee, keys, vs0.View()) lane := proposer0.Public() @@ -95,6 +100,8 @@ func TestProposalBlockTimestampStrictlyMonotone(t *testing.T) { proposer0, committee, vs0, + firstBlock, + genesisTimestamp, time.Now(), map[LaneID]*LaneQC{ lane: makeLaneQC(rng, committee, keys, lane, 2, GenBlockHeaderHash(rng)), @@ -102,12 +109,12 @@ func TestProposalBlockTimestampStrictlyMonotone(t *testing.T) { utils.None[*AppQC](), )) p0 := firstProposal.Proposal().Msg() - gr0 := p0.GlobalRange(committee) - require.Equal(t, committee.FirstBlock(), gr0.First) - require.Equal(t, committee.FirstBlock()+3, gr0.Next) - first0 := p0.BlockTimestamp(committee, gr0.First).OrPanic("missing first block timestamp") - second0 := p0.BlockTimestamp(committee, gr0.First+1).OrPanic("missing second block timestamp") - third0 := p0.BlockTimestamp(committee, gr0.First+2).OrPanic("missing third block timestamp") + gr0 := p0.GlobalRange() + require.Equal(t, firstBlock, gr0.First) + require.Equal(t, firstBlock+3, gr0.Next) + first0 := p0.BlockTimestamp(gr0.First).OrPanic("missing first block timestamp") + second0 := p0.BlockTimestamp(gr0.First + 1).OrPanic("missing second block timestamp") + third0 := p0.BlockTimestamp(gr0.First + 2).OrPanic("missing third block timestamp") require.True(t, first0.Before(second0), "block timestamps within one proposal must be strictly increasing") require.True(t, second0.Before(third0), "block timestamps within one proposal must be strictly increasing") @@ -119,6 +126,8 @@ func TestProposalBlockTimestampStrictlyMonotone(t *testing.T) { proposer1, committee, vs1, + firstBlock, + genesisTimestamp, time.Now(), map[LaneID]*LaneQC{ lane: makeLaneQC(rng, committee, keys, lane, 3, GenBlockHeaderHash(rng)), @@ -126,10 +135,10 @@ func TestProposalBlockTimestampStrictlyMonotone(t *testing.T) { utils.None[*AppQC](), )) p1 := secondProposal.Proposal().Msg() - gr1 := p1.GlobalRange(committee) + gr1 := p1.GlobalRange() require.Equal(t, gr0.Next, gr1.First) - last0 := p0.BlockTimestamp(committee, gr0.Next-1).OrPanic("missing last block timestamp") - first1 := p1.BlockTimestamp(committee, gr1.First).OrPanic("missing first timestamp of next proposal") + last0 := p0.BlockTimestamp(gr0.Next - 1).OrPanic("missing last block timestamp") + first1 := p1.BlockTimestamp(gr1.First).OrPanic("missing first timestamp of next proposal") require.True(t, last0.Before(first1), "block timestamps across consecutive proposals must be strictly increasing") } @@ -137,22 +146,22 @@ func TestProposalVerifyRejectsNonMonotoneTimestamp(t *testing.T) { t.Run("wrt genesis timestamp", func(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Now() vs := ViewSpec{} k := leaderKey(committee, keys, vs.View()) - fp := utils.OrPanic1(NewProposal(k, committee, vs, committee.GenesisTimestamp(), nil, utils.None[*AppQC]())) - require.NoError(t, fp.Verify(committee, vs)) - - committee = utils.OrPanic1(NewRoundRobinElection( - slices.Collect(committee.Replicas().All()), - committee.FirstBlock(), - fp.Proposal().Msg().Timestamp().Add(time.Nanosecond)), - ) - require.Error(t, fp.Verify(committee, vs)) + fp := utils.OrPanic1(NewProposal(k, committee, vs, firstBlock, genesisTimestamp, genesisTimestamp, nil, utils.None[*AppQC]())) + require.NoError(t, fp.Verify(committee, vs, genesisTimestamp)) + + laterGenesis := fp.Proposal().Msg().Timestamp().Add(time.Nanosecond) + require.Error(t, fp.Verify(committee, vs, laterGenesis)) }) t.Run("wrt previous proposal", func(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} vs0 := ViewSpec{} proposer0 := leaderKey(committee, keys, vs0.View()) lane := proposer0.Public() @@ -162,6 +171,8 @@ func TestProposalVerifyRejectsNonMonotoneTimestamp(t *testing.T) { proposer0, committee, vs0, + firstBlock, + genesisTimestamp, time.Now(), map[LaneID]*LaneQC{lane: lQC}, utils.None[*AppQC](), @@ -170,6 +181,8 @@ func TestProposalVerifyRejectsNonMonotoneTimestamp(t *testing.T) { proposer0, committee, vs0, + firstBlock, + genesisTimestamp, fp0a.Proposal().Msg().NextTimestamp().Add(time.Hour), map[LaneID]*LaneQC{lane: lQC}, utils.None[*AppQC](), @@ -183,55 +196,63 @@ func TestProposalVerifyRejectsNonMonotoneTimestamp(t *testing.T) { proposer1, committee, vs1a, + firstBlock, + genesisTimestamp, fp0a.Proposal().Msg().NextTimestamp(), nil, utils.None[*AppQC](), )) - require.NoError(t, fp1a.Verify(committee, vs1a)) - require.Error(t, fp1a.Verify(committee, vs1b)) + require.NoError(t, fp1a.Verify(committee, vs1a, genesisTimestamp)) + require.Error(t, fp1a.Verify(committee, vs1b, genesisTimestamp)) }) } func TestProposalVerifyRejectsViewMismatch(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} // Build a valid proposal at genesis view (0, 0). vs0 := ViewSpec{} leader0 := leaderKey(committee, keys, vs0.View()) - fp := utils.OrPanic1(NewProposal(leader0, committee, vs0, time.Now(), nil, utils.None[*AppQC]())) + fp := utils.OrPanic1(NewProposal(leader0, committee, vs0, firstBlock, genesisTimestamp, time.Now(), nil, utils.None[*AppQC]())) // Verify it against a different ViewSpec (view 1, 0). commitQC := makeCommitQCFromProposal(keys, fp) vs1 := ViewSpec{CommitQC: utils.Some(commitQC)} - err := fp.Verify(committee, vs1) + err := fp.Verify(committee, vs1, genesisTimestamp) require.Error(t, err) } func TestProposalVerifyRejectsForgedSignature(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} vs := ViewSpec{} proposerKey := leaderKey(committee, keys, vs.View()) // Build two valid proposals with different timestamps. - fp1 := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now(), nil, utils.None[*AppQC]())) - fp2 := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now().Add(time.Hour), nil, utils.None[*AppQC]())) + fp1 := utils.OrPanic1(NewProposal(proposerKey, committee, vs, firstBlock, genesisTimestamp, time.Now(), nil, utils.None[*AppQC]())) + fp2 := utils.OrPanic1(NewProposal(proposerKey, committee, vs, firstBlock, genesisTimestamp, time.Now().Add(time.Hour), nil, utils.None[*AppQC]())) // Graft fp1's signature onto fp2 (different content). fp2.proposal.sig = fp1.proposal.sig - err := fp2.Verify(committee, vs) + err := fp2.Verify(committee, vs, genesisTimestamp) require.Error(t, err) } func TestProposalVerifyRejectsWrongProposer(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} vs := ViewSpec{} correctLeader := leaderKey(committee, keys, vs.View()) - fp := utils.OrPanic1(NewProposal(correctLeader, committee, vs, time.Now(), nil, utils.None[*AppQC]())) + fp := utils.OrPanic1(NewProposal(correctLeader, committee, vs, firstBlock, genesisTimestamp, time.Now(), nil, utils.None[*AppQC]())) // Re-sign the same proposal with a different (non-leader) key. var wrongKey SecretKey @@ -247,17 +268,19 @@ func TestProposalVerifyRejectsWrongProposer(t *testing.T) { appQC: fp.appQC, timeoutQC: fp.timeoutQC, } - err := tamperedFP.Verify(committee, vs) + err := tamperedFP.Verify(committee, vs, genesisTimestamp) require.Error(t, err) } func TestProposalVerifyRejectsInconsistentTimeoutQC(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} vs := ViewSpec{} // no timeoutQC proposerKey := leaderKey(committee, keys, vs.View()) - fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now(), nil, utils.None[*AppQC]())) + fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, firstBlock, genesisTimestamp, time.Now(), nil, utils.None[*AppQC]())) // Attach a timeoutQC that the ViewSpec doesn't expect. var timeoutVotes []*FullTimeoutVote @@ -272,17 +295,19 @@ func TestProposalVerifyRejectsInconsistentTimeoutQC(t *testing.T) { appQC: fp.appQC, timeoutQC: utils.Some(tQC), } - err := tamperedFP.Verify(committee, vs) + err := tamperedFP.Verify(committee, vs, genesisTimestamp) require.Error(t, err) } func TestProposalVerifyRejectsNonCommitteeLane(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} vs := ViewSpec{} proposerKey := leaderKey(committee, keys, vs.View()) - fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now(), nil, utils.None[*AppQC]())) + fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, firstBlock, genesisTimestamp, time.Now(), nil, utils.None[*AppQC]())) // Replace one committee lane with a non-committee lane. // E.g. committee = {A, B, C, D}, proposal = {A, B, C, X}. @@ -305,24 +330,26 @@ func TestProposalVerifyRejectsNonCommitteeLane(t *testing.T) { } } - tamperedProposal := newProposal(origProposal.view, origProposal.timestamp, tamperedRanges, origProposal.app) + tamperedProposal := newProposal(origProposal.view, origProposal.timestamp, tamperedRanges, origProposal.app, origProposal.globalRange.First) maliciousFP := &FullProposal{ proposal: Sign(proposerKey, tamperedProposal), laneQCs: fp.laneQCs, appQC: fp.appQC, timeoutQC: fp.timeoutQC, } - err := maliciousFP.Verify(committee, vs) + err := maliciousFP.Verify(committee, vs, genesisTimestamp) require.Error(t, err) } func TestProposalVerifyAcceptsImplicitLaneRange(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} vs := ViewSpec{} proposerKey := leaderKey(committee, keys, vs.View()) - fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now(), nil, utils.None[*AppQC]())) + fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, firstBlock, genesisTimestamp, time.Now(), nil, utils.None[*AppQC]())) // Drop one lane — the omitted lane gets an implicit [0, 0) range, // which matches the expected first=0 at genesis. @@ -337,20 +364,22 @@ func TestProposalVerifyAcceptsImplicitLaneRange(t *testing.T) { keptRanges = append(keptRanges, r) } - shortProposal := newProposal(origP.view, origP.timestamp, keptRanges, origP.app) + shortProposal := newProposal(origP.view, origP.timestamp, keptRanges, origP.app, origP.globalRange.First) shortFP := &FullProposal{ proposal: Sign(proposerKey, shortProposal), } - require.NoError(t, shortFP.Verify(committee, vs)) + require.NoError(t, shortFP.Verify(committee, vs, genesisTimestamp)) } func TestProposalVerifyAcceptsNonContiguousImplicitRanges(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} vs := ViewSpec{} proposerKey := leaderKey(committee, keys, vs.View()) - fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now(), nil, utils.None[*AppQC]())) + fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, firstBlock, genesisTimestamp, time.Now(), nil, utils.None[*AppQC]())) // Keep only every other lane (e.g. {A, C} out of {A, B, C, D}). origP := fp.Proposal().Msg() @@ -363,20 +392,22 @@ func TestProposalVerifyAcceptsNonContiguousImplicitRanges(t *testing.T) { i++ } - shortProposal := newProposal(origP.view, origP.timestamp, keptRanges, origP.app) + shortProposal := newProposal(origP.view, origP.timestamp, keptRanges, origP.app, origP.globalRange.First) shortFP := &FullProposal{ proposal: Sign(proposerKey, shortProposal), } - require.NoError(t, shortFP.Verify(committee, vs)) + require.NoError(t, shortFP.Verify(committee, vs, genesisTimestamp)) } func TestProposalVerifyRejectsLaneRangeFirstMismatch(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} vs := ViewSpec{} proposerKey := leaderKey(committee, keys, vs.View()) - fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now(), nil, utils.None[*AppQC]())) + fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, firstBlock, genesisTimestamp, time.Now(), nil, utils.None[*AppQC]())) // Tamper: change one lane's first to 5 (genesis expects 0). origP := fp.Proposal().Msg() @@ -389,17 +420,19 @@ func TestProposalVerifyRejectsLaneRangeFirstMismatch(t *testing.T) { tamperedRanges = append(tamperedRanges, r) } } - tamperedProposal := newProposal(origP.view, origP.timestamp, tamperedRanges, origP.app) + tamperedProposal := newProposal(origP.view, origP.timestamp, tamperedRanges, origP.app, origP.globalRange.First) tamperedFP := &FullProposal{ proposal: Sign(proposerKey, tamperedProposal), } - err := tamperedFP.Verify(committee, vs) + err := tamperedFP.Verify(committee, vs, genesisTimestamp) require.Error(t, err) } func TestProposalVerifyRejectsMissingLaneQC(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} vs := ViewSpec{} proposerKey := leaderKey(committee, keys, vs.View()) @@ -407,20 +440,22 @@ func TestProposalVerifyRejectsMissingLaneQC(t *testing.T) { laneQC := makeLaneQC(rng, committee, keys, lane, 0, GenBlockHeaderHash(rng)) // Build a valid proposal with a block, then strip the laneQC. - fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now(), + fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, firstBlock, genesisTimestamp, time.Now(), map[LaneID]*LaneQC{lane: laneQC}, utils.None[*AppQC]())) tamperedFP := &FullProposal{ proposal: fp.proposal, laneQCs: map[LaneID]*LaneQC{}, } - err := tamperedFP.Verify(committee, vs) + err := tamperedFP.Verify(committee, vs, genesisTimestamp) require.Error(t, err) } func TestProposalVerifyRejectsLaneQCBlockNumberMismatch(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} vs := ViewSpec{} proposerKey := leaderKey(committee, keys, vs.View()) @@ -428,7 +463,7 @@ func TestProposalVerifyRejectsLaneQCBlockNumberMismatch(t *testing.T) { // Build a valid proposal with a QC certifying block 1 (range [0, 2)). goodQC := makeLaneQC(rng, committee, keys, lane, 1, GenBlockHeaderHash(rng)) - fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now(), + fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, firstBlock, genesisTimestamp, time.Now(), map[LaneID]*LaneQC{lane: goodQC}, utils.None[*AppQC]())) // Swap in a QC certifying block 0 — range expects block 1. @@ -437,13 +472,15 @@ func TestProposalVerifyRejectsLaneQCBlockNumberMismatch(t *testing.T) { proposal: fp.proposal, laneQCs: map[LaneID]*LaneQC{lane: wrongQC}, } - err := tamperedFP.Verify(committee, vs) + err := tamperedFP.Verify(committee, vs, genesisTimestamp) require.Error(t, err) } func TestProposalVerifyRejectsInvalidLaneQCSignature(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} vs := ViewSpec{} proposerKey := leaderKey(committee, keys, vs.View()) @@ -462,10 +499,10 @@ func TestProposalVerifyRejectsInvalidLaneQCSignature(t *testing.T) { } badLaneQC := NewLaneQC(badVotes) - fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now(), + fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, firstBlock, genesisTimestamp, time.Now(), map[LaneID]*LaneQC{lane: badLaneQC}, utils.None[*AppQC]())) - err := fp.Verify(committee, vs) + err := fp.Verify(committee, vs, genesisTimestamp) require.Error(t, err) } @@ -484,6 +521,8 @@ func TestProposalConvDecode_RejectsDuplicateLaneRanges(t *testing.T) { func makeFullProposal( committee *Committee, keys []SecretKey, + firstBlock GlobalBlockNumber, + genesisTimestamp time.Time, prev utils.Option[*CommitQC], laneQCs map[LaneID]*LaneQC, appQC utils.Option[*AppQC], @@ -493,6 +532,8 @@ func makeFullProposal( leaderKey(committee, keys, vs.View()), committee, vs, + firstBlock, + genesisTimestamp, time.Now(), laneQCs, appQC, @@ -511,112 +552,120 @@ func makeCommitQC(keys []SecretKey, fullProposal *FullProposal) *CommitQC { func TestProposalVerifyRejectsAppProposalLowerThanPrevious(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} // 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() lQCs := map[LaneID]*LaneQC{l: makeLaneQC(rng, committee, keys, l, 0, GenBlockHeaderHash(rng))} - commitQC0 := makeCommitQC(keys, makeFullProposal(committee, keys, utils.None[*CommitQC](), lQCs, utils.None[*AppQC]())) - appQC0 := makeAppQCFor(keys, commitQC0.GlobalRange(committee).First, 0, GenAppHash(rng)) - commitQC1a := makeCommitQC(keys, makeFullProposal(committee, keys, utils.Some(commitQC0), nil, utils.Some(appQC0))) - commitQC1b := makeCommitQC(keys, makeFullProposal(committee, keys, utils.Some(commitQC0), nil, utils.None[*AppQC]())) - fp2a := makeFullProposal(committee, keys, utils.Some(commitQC1a), nil, utils.None[*AppQC]()) - fp2b := makeFullProposal(committee, keys, utils.Some(commitQC1b), nil, utils.None[*AppQC]()) + commitQC0 := makeCommitQC(keys, makeFullProposal(committee, keys, firstBlock, genesisTimestamp, utils.None[*CommitQC](), lQCs, utils.None[*AppQC]())) + appQC0 := makeAppQCFor(keys, commitQC0.GlobalRange().First, 0, GenAppHash(rng)) + commitQC1a := makeCommitQC(keys, makeFullProposal(committee, keys, firstBlock, genesisTimestamp, utils.Some(commitQC0), nil, utils.Some(appQC0))) + commitQC1b := makeCommitQC(keys, makeFullProposal(committee, keys, firstBlock, genesisTimestamp, utils.Some(commitQC0), nil, utils.None[*AppQC]())) + fp2a := makeFullProposal(committee, keys, firstBlock, genesisTimestamp, utils.Some(commitQC1a), nil, utils.None[*AppQC]()) + fp2b := makeFullProposal(committee, keys, firstBlock, genesisTimestamp, utils.Some(commitQC1b), nil, utils.None[*AppQC]()) // We construct the invalid proposal by constructing 2 alternative futures: one with appQC, one without. vs := ViewSpec{CommitQC: utils.Some(commitQC1a)} - require.NoError(t, fp2a.Verify(committee, vs)) - require.Error(t, fp2b.Verify(committee, vs)) + require.NoError(t, fp2a.Verify(committee, vs, genesisTimestamp)) + require.Error(t, fp2b.Verify(committee, vs, genesisTimestamp)) } func TestProposalVerifyRejectsUnnecessaryAppQC(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} // no previous commitQC, so app starts at None - initialBlock := committee.FirstBlock() + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} + vs := ViewSpec{FirstBlock: firstBlock} // no previous commitQC, so app starts at None leader := leaderKey(committee, keys, vs.View()) - fp := utils.OrPanic1(NewProposal(leader, committee, vs, time.Now(), nil, utils.None[*AppQC]())) + fp := utils.OrPanic1(NewProposal(leader, committee, vs, firstBlock, genesisTimestamp, time.Now(), nil, utils.None[*AppQC]())) // Attach an unrequested AppQC. - appQC := makeAppQCFor(keys, initialBlock, 0, GenAppHash(rng)) + appQC := makeAppQCFor(keys, firstBlock, 0, GenAppHash(rng)) tamperedFP := &FullProposal{ proposal: fp.proposal, laneQCs: fp.laneQCs, appQC: utils.Some(appQC), timeoutQC: fp.timeoutQC, } - err := tamperedFP.Verify(committee, vs) + err := tamperedFP.Verify(committee, vs, genesisTimestamp) require.Error(t, err) } func TestProposalVerifyRejectsMissingAppQC(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} // no previous commitQC + firstBlock := GlobalBlockNumber(1) // non-zero so firstBlock-1 is valid + genesisTimestamp := time.Time{} + vs := ViewSpec{FirstBlock: firstBlock} // no previous commitQC leader := leaderKey(committee, keys, vs.View()) - initialBlock := committee.FirstBlock() // Build a valid proposal with an AppQC, then strip it. - goodAppQC := makeAppQCFor(keys, initialBlock-1, 0, GenAppHash(rng)) - fp := utils.OrPanic1(NewProposal(leader, committee, vs, time.Now(), nil, utils.Some(goodAppQC))) + goodAppQC := makeAppQCFor(keys, firstBlock-1, 0, GenAppHash(rng)) + fp := utils.OrPanic1(NewProposal(leader, committee, vs, firstBlock, genesisTimestamp, time.Now(), nil, utils.Some(goodAppQC))) tamperedFP := &FullProposal{ proposal: fp.proposal, } - err := tamperedFP.Verify(committee, vs) + err := tamperedFP.Verify(committee, vs, genesisTimestamp) require.Error(t, err) } func TestProposalVerifyRejectsAppQCMismatch(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} vs := ViewSpec{} leader := leaderKey(committee, keys, vs.View()) - initialBlock := committee.FirstBlock() // Build a valid proposal with an AppQC, then swap in a different one. - goodAppQC := makeAppQCFor(keys, initialBlock, 0, GenAppHash(rng)) - fp := utils.OrPanic1(NewProposal(leader, committee, vs, time.Now(), nil, utils.Some(goodAppQC))) + goodAppQC := makeAppQCFor(keys, firstBlock, 0, GenAppHash(rng)) + fp := utils.OrPanic1(NewProposal(leader, committee, vs, firstBlock, genesisTimestamp, time.Now(), nil, utils.Some(goodAppQC))) - differentAppQC := makeAppQCFor(keys, initialBlock, 0, GenAppHash(rng)) + differentAppQC := makeAppQCFor(keys, firstBlock, 0, GenAppHash(rng)) tamperedFP := &FullProposal{ proposal: fp.proposal, appQC: utils.Some(differentAppQC), } - err := tamperedFP.Verify(committee, vs) + err := tamperedFP.Verify(committee, vs, genesisTimestamp) require.Error(t, err) } func TestProposalVerifyRejectsInvalidAppQCSignature(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} vs := ViewSpec{} leader := leaderKey(committee, keys, vs.View()) - initialBlock := committee.FirstBlock() appHash := GenAppHash(rng) - goodAppQC := makeAppQCFor(keys, initialBlock, 0, appHash) - fp := utils.OrPanic1(NewProposal(leader, committee, vs, time.Now(), nil, utils.Some(goodAppQC))) + goodAppQC := makeAppQCFor(keys, firstBlock, 0, appHash) + fp := utils.OrPanic1(NewProposal(leader, committee, vs, firstBlock, genesisTimestamp, time.Now(), nil, utils.Some(goodAppQC))) // Swap in an AppQC signed by NON-committee keys (same hash). otherKeys := make([]SecretKey, len(keys)) for i := range otherKeys { otherKeys[i] = GenSecretKey(rng) } - badAppQC := makeAppQCFor(otherKeys, initialBlock, 0, appHash) + badAppQC := makeAppQCFor(otherKeys, firstBlock, 0, appHash) tamperedFP := &FullProposal{ proposal: fp.proposal, appQC: utils.Some(badAppQC), } - err := tamperedFP.Verify(committee, vs) + err := tamperedFP.Verify(committee, vs, genesisTimestamp) require.Error(t, err) } func TestProposalVerifyRejectsLaneQCHeaderHashMismatch(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} vs := ViewSpec{} proposerKey := leaderKey(committee, keys, vs.View()) @@ -624,7 +673,7 @@ func TestProposalVerifyRejectsLaneQCHeaderHashMismatch(t *testing.T) { // Build a valid proposal with a QC for block 0. realQC := makeLaneQC(rng, committee, keys, lane, 0, GenBlockHeaderHash(rng)) - fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now(), + fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, firstBlock, genesisTimestamp, time.Now(), map[LaneID]*LaneQC{lane: realQC}, utils.None[*AppQC]())) // Swap in a different QC for block 0 (different payload → different hash). @@ -635,18 +684,25 @@ func TestProposalVerifyRejectsLaneQCHeaderHashMismatch(t *testing.T) { proposal: fp.proposal, laneQCs: map[LaneID]*LaneQC{lane: differentQC}, } - err := tamperedFP.Verify(committee, vs) + err := tamperedFP.Verify(committee, vs, genesisTimestamp) require.Error(t, err) } func TestProposalVerifyValidReproposal(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(100) + genesisTimestamp := time.Time{} - // First, create a valid proposal at view (0, 0) with a PrepareQC. - vs0 := ViewSpec{} + // Build a proposal at view (0, 0) with one lane block so sum(lane.First) > 0. + // firstBlock > 0 ensures a reproposal bug that passes GlobalRange().First + // (= sum(lane.First)+firstBlock) instead of firstBlock would be caught. + vs0 := ViewSpec{FirstBlock: firstBlock} leader0 := leaderKey(committee, keys, vs0.View()) - fp0 := utils.OrPanic1(NewProposal(leader0, committee, vs0, time.Now(), nil, utils.None[*AppQC]())) + lane := committee.Leader(vs0.View()) + laneQC0 := makeLaneQC(rng, committee, keys, lane, 0, GenBlockHeaderHash(rng)) + fp0 := utils.OrPanic1(NewProposal(leader0, committee, vs0, firstBlock, genesisTimestamp, time.Now(), + map[LaneID]*LaneQC{lane: laneQC0}, utils.None[*AppQC]())) // Build a PrepareQC for the proposal at (0, 0). var prepareVotes []*Signed[*PrepareVote] @@ -662,23 +718,27 @@ func TestProposalVerifyValidReproposal(t *testing.T) { } timeoutQC := NewTimeoutQC(timeoutVotes) - vs1 := ViewSpec{TimeoutQC: utils.Some(timeoutQC)} + vs1 := ViewSpec{TimeoutQC: utils.Some(timeoutQC), FirstBlock: firstBlock} require.Equal(t, View{Index: 0, Number: 1}, vs1.View()) leader1 := leaderKey(committee, keys, vs1.View()) - reproposal := utils.OrPanic1(NewProposal(leader1, committee, vs1, time.Now(), nil, utils.None[*AppQC]())) + reproposal := utils.OrPanic1(NewProposal(leader1, committee, vs1, firstBlock, genesisTimestamp, time.Now(), nil, utils.None[*AppQC]())) - require.NoError(t, reproposal.Verify(committee, vs1)) + // Reproposal must carry the same GlobalRange as the original. + require.Equal(t, fp0.Proposal().Msg().GlobalRange(), reproposal.Proposal().Msg().GlobalRange()) + require.NoError(t, reproposal.Verify(committee, vs1, genesisTimestamp)) } func TestProposalVerifyRejectsReproposalWithUnnecessaryData(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} // Build a PrepareQC at (0, 0). vs0 := ViewSpec{} leader0 := leaderKey(committee, keys, vs0.View()) - fp0 := utils.OrPanic1(NewProposal(leader0, committee, vs0, time.Now(), nil, utils.None[*AppQC]())) + fp0 := utils.OrPanic1(NewProposal(leader0, committee, vs0, firstBlock, genesisTimestamp, time.Now(), nil, utils.None[*AppQC]())) var prepareVotes []*Signed[*PrepareVote] for _, k := range keys { @@ -696,7 +756,7 @@ func TestProposalVerifyRejectsReproposalWithUnnecessaryData(t *testing.T) { leader1 := leaderKey(committee, keys, vs1.View()) // Create a valid reproposal, then tamper it with unnecessary laneQCs. - reproposal := utils.OrPanic1(NewProposal(leader1, committee, vs1, time.Now(), nil, utils.None[*AppQC]())) + reproposal := utils.OrPanic1(NewProposal(leader1, committee, vs1, firstBlock, genesisTimestamp, time.Now(), nil, utils.None[*AppQC]())) lane := keys[0].Public() laneQC := makeLaneQC(rng, committee, keys, lane, 0, GenBlockHeaderHash(rng)) @@ -705,18 +765,20 @@ func TestProposalVerifyRejectsReproposalWithUnnecessaryData(t *testing.T) { laneQCs: map[LaneID]*LaneQC{lane: laneQC}, timeoutQC: reproposal.timeoutQC, } - err := tamperedFP.Verify(committee, vs1) + err := tamperedFP.Verify(committee, vs1, genesisTimestamp) require.Error(t, err) } func TestProposalVerifyRejectsReproposalHashMismatch(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} // Build a PrepareQC at (0, 0). vs0 := ViewSpec{} leader0 := leaderKey(committee, keys, vs0.View()) - fp0 := utils.OrPanic1(NewProposal(leader0, committee, vs0, time.Now(), nil, utils.None[*AppQC]())) + fp0 := utils.OrPanic1(NewProposal(leader0, committee, vs0, firstBlock, genesisTimestamp, time.Now(), nil, utils.None[*AppQC]())) var prepareVotes []*Signed[*PrepareVote] for _, k := range keys { @@ -734,25 +796,27 @@ func TestProposalVerifyRejectsReproposalHashMismatch(t *testing.T) { leader1 := leaderKey(committee, keys, vs1.View()) // Build the valid reproposal, then tamper its timestamp to get a different hash. - reproposal := utils.OrPanic1(NewProposal(leader1, committee, vs1, time.Now(), nil, utils.None[*AppQC]())) + reproposal := utils.OrPanic1(NewProposal(leader1, committee, vs1, firstBlock, genesisTimestamp, time.Now(), nil, utils.None[*AppQC]())) origP := reproposal.Proposal().Msg() var ranges []*LaneRange for _, r := range origP.laneRanges { ranges = append(ranges, r) } - wrongP := newProposal(origP.view, time.Now().Add(time.Hour), ranges, origP.app) + wrongP := newProposal(origP.view, time.Now().Add(time.Hour), ranges, origP.app, origP.globalRange.First) wrongFP := &FullProposal{ proposal: Sign(leader1, wrongP), timeoutQC: reproposal.timeoutQC, } - err := wrongFP.Verify(committee, vs1) + err := wrongFP.Verify(committee, vs1, genesisTimestamp) require.Error(t, err) } func TestProposalVerifyRejectsInvalidTimeoutQCSignature(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + firstBlock := GlobalBlockNumber(0) + genesisTimestamp := time.Time{} // Build a TimeoutQC signed by NON-committee keys. otherKeys := make([]SecretKey, len(keys)) @@ -768,8 +832,8 @@ func TestProposalVerifyRejectsInvalidTimeoutQCSignature(t *testing.T) { vs := ViewSpec{TimeoutQC: utils.Some(badTimeoutQC)} leader := leaderKey(committee, keys, vs.View()) - fp := utils.OrPanic1(NewProposal(leader, committee, vs, time.Now(), nil, utils.None[*AppQC]())) + fp := utils.OrPanic1(NewProposal(leader, committee, vs, firstBlock, genesisTimestamp, time.Now(), nil, utils.None[*AppQC]())) - err := fp.Verify(committee, vs) + err := fp.Verify(committee, vs, genesisTimestamp) require.Error(t, err) } diff --git a/sei-tendermint/autobahn/types/testonly.go b/sei-tendermint/autobahn/types/testonly.go index f7ea9e03e3..1564fc4388 100644 --- a/sei-tendermint/autobahn/types/testonly.go +++ b/sei-tendermint/autobahn/types/testonly.go @@ -11,6 +11,34 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" ) +// BuildCommitQC builds a valid CommitQC from explicit lane QCs and an optional app QC. +// Use BuildFullCommitQC when you want random blocks generated automatically. +func BuildCommitQC( + committee *Committee, + keys []SecretKey, + prev utils.Option[*CommitQC], + firstBlock GlobalBlockNumber, + genesisTimestamp time.Time, + laneQCs map[LaneID]*LaneQC, + appQC utils.Option[*AppQC], +) *CommitQC { + vs := ViewSpec{CommitQC: prev, FirstBlock: firstBlock} + leader := committee.Leader(vs.View()) + var leaderKey SecretKey + for _, k := range keys { + if k.Public() == leader { + leaderKey = k + break + } + } + proposal := utils.OrPanic1(NewProposal(leaderKey, committee, vs, firstBlock, genesisTimestamp, time.Now(), laneQCs, appQC)) + votes := make([]*Signed[*CommitVote], 0, len(keys)) + for _, k := range keys { + votes = append(votes, Sign(k, NewCommitVote(proposal.Proposal().Msg()))) + } + return NewCommitQC(votes) +} + // GenNodeID generates a random NodeID. func GenNodeID(rng utils.Rng) NodeID { return NodeID(utils.GenString(rng, 10)) @@ -37,7 +65,7 @@ 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, GenGlobalBlockNumber(rng)%1000000, time.Now())), sks + return utils.OrPanic1(NewCommittee(pks)), sks } // TestKeysWithWeight returns a deterministic subset of keys whose committee weight reaches the requested threshold. @@ -168,12 +196,12 @@ func GenView(rng utils.Rng) View { // GenProposal generates a random Proposal. func GenProposal(rng utils.Rng) *Proposal { - return newProposal(GenView(rng), time.Now(), utils.GenSlice(rng, GenLaneRange), utils.Some(GenAppProposal(rng))) + return newProposal(GenView(rng), time.Now(), utils.GenSlice(rng, GenLaneRange), utils.Some(GenAppProposal(rng)), GlobalBlockNumber(rng.Uint64())) } // GenProposalAt generates a Proposal at a specific view. func GenProposalAt(rng utils.Rng, view View) *Proposal { - return newProposal(view, time.Now(), utils.GenSlice(rng, GenLaneRange), utils.Some(GenAppProposal(rng))) + return newProposal(view, time.Now(), utils.GenSlice(rng, GenLaneRange), utils.Some(GenAppProposal(rng)), GlobalBlockNumber(rng.Uint64())) } // GenAppHash generates a random AppHash. diff --git a/sei-tendermint/autobahn/types/timeout.go b/sei-tendermint/autobahn/types/timeout.go index 003ff64b26..8e0f310c4d 100644 --- a/sei-tendermint/autobahn/types/timeout.go +++ b/sei-tendermint/autobahn/types/timeout.go @@ -199,12 +199,7 @@ func (m *TimeoutQC) reproposal() (*Proposal, bool) { for _, l := range p.laneRanges { laneRanges = append(laneRanges, l) } - return newProposal( - m.View().Next(), - p.Timestamp(), - laneRanges, - p.App(), - ), true + return newProposal(m.View().Next(), p.Timestamp(), laneRanges, p.App(), p.firstBlock), true } // TimeoutVoteConv is the protobuf converter for TimeoutVote. diff --git a/sei-tendermint/autobahn/types/types_test.go b/sei-tendermint/autobahn/types/types_test.go index 79f48a7773..b1cbfe7df5 100644 --- a/sei-tendermint/autobahn/types/types_test.go +++ b/sei-tendermint/autobahn/types/types_test.go @@ -96,25 +96,6 @@ func TestMarshal(t *testing.T) { } } -func TestNewRoundRobinElection_GenesisTimestamp(t *testing.T) { - rng := utils.TestRng() - replicas := []PublicKey{GenPublicKey(rng), GenPublicKey(rng)} - firstBlock := GenGlobalBlockNumber(rng) - genesisTimestamp := time.Now() - - committee, err := NewRoundRobinElection(replicas, firstBlock, genesisTimestamp) - if err != nil { - t.Fatalf("NewRoundRobinElection(): %v", err) - } - - if got := committee.FirstBlock(); got != firstBlock { - t.Fatalf("FirstBlock() = %v, want %v", got, firstBlock) - } - if got := committee.GenesisTimestamp(); !got.Equal(genesisTimestamp) { - t.Fatalf("GenesisTimestamp() = %v, want %v", got, genesisTimestamp) - } -} - func makePrepareQC(keys []SecretKey, vote *PrepareVote) *PrepareQC { var votes []*Signed[*PrepareVote] for _, k := range keys { @@ -134,7 +115,7 @@ func TestNewTimeoutQC(t *testing.T) { Index: view.Index, Number: GenViewNumber(rng) % view.Number, } - p := newProposal(pView, time.Now(), utils.GenSlice(rng, GenLaneRange), utils.Some(GenAppProposal(rng))) + p := newProposal(pView, time.Now(), utils.GenSlice(rng, GenLaneRange), utils.Some(GenAppProposal(rng)), GlobalBlockNumber(rng.Uint64())) if wantView.Less(pView) { wantView = pView } @@ -144,6 +125,7 @@ func TestNewTimeoutQC(t *testing.T) { pQC, ok := tQC.LatestPrepareQC().Get() if !ok { t.Fatalf("tQC.LatestPrepareQC() missing") + } if gotView := pQC.View(); gotView != wantView { t.Fatalf("tQC.LatestPrepareQC().View() = %v, want %v", gotView, wantView) @@ -165,7 +147,7 @@ func TestNewTimeoutQC_MixedPrepareQCs(t *testing.T) { view := View{Index: 0, Number: 0} pqc := makePrepareQC(keys, NewPrepareVote( - newProposal(view, time.Now(), utils.GenSlice(rng, GenLaneRange), utils.Some(GenAppProposal(rng))), + newProposal(view, time.Now(), utils.GenSlice(rng, GenLaneRange), utils.Some(GenAppProposal(rng)), GlobalBlockNumber(rng.Uint64())), )) // Only keys[0] carries the PrepareQC; the rest carry None. @@ -219,7 +201,7 @@ func TestTimeoutQCVerify_HighestPrepareQCSelected(t *testing.T) { makePQCAt := func(vn ViewNumber) *PrepareQC { pView := View{Index: view.Index, Number: vn} return makePrepareQC(keys, NewPrepareVote( - newProposal(pView, time.Now(), utils.GenSlice(rng, GenLaneRange), utils.Some(GenAppProposal(rng))), + newProposal(pView, time.Now(), utils.GenSlice(rng, GenLaneRange), utils.Some(GenAppProposal(rng)), GlobalBlockNumber(rng.Uint64())), )) } diff --git a/sei-tendermint/autobahn/types/wireguard_test.go b/sei-tendermint/autobahn/types/wireguard_test.go index 7ac3fe0755..f5c296c92d 100644 --- a/sei-tendermint/autobahn/types/wireguard_test.go +++ b/sei-tendermint/autobahn/types/wireguard_test.go @@ -154,6 +154,8 @@ func TestFullProposalWireguardAcceptsMaxValidators(t *testing.T) { secretKeyFor(keys, committee.Leader(View{})), committee, ViewSpec{}, + 0, + time.Time{}, time.Unix(1, 2), laneQCs, utils.None[*AppQC](), diff --git a/sei-tendermint/internal/autobahn/autobahn.proto b/sei-tendermint/internal/autobahn/autobahn.proto index 24a6323daa..584273231d 100644 --- a/sei-tendermint/internal/autobahn/autobahn.proto +++ b/sei-tendermint/internal/autobahn/autobahn.proto @@ -130,6 +130,7 @@ message Proposal { optional Timestamp timestamp = 5; // required repeated LaneRange lane_ranges = 3 [(wireguard.max_count) = 100]; // Sorted by lane. optional AppProposal app = 4; // optional + optional uint64 global_first = 6; // genesis InitialHeight; added to lane block numbers to produce absolute global block numbers } message FullProposal { diff --git a/sei-tendermint/internal/autobahn/avail/block_votes.go b/sei-tendermint/internal/autobahn/avail/block_votes.go index ae64772803..4ab0f55bff 100644 --- a/sei-tendermint/internal/autobahn/avail/block_votes.go +++ b/sei-tendermint/internal/autobahn/avail/block_votes.go @@ -2,40 +2,62 @@ package avail import ( "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/epoch" ) +// blockHashEntry accumulates votes for a single block hash across epochs. +// votes holds every accepted vote (deduped by key across all epochs). +// epochWeight tracks the accumulated weight per epoch independently, so +// that quorum can be reached separately for each epoch's committee. +type blockHashEntry struct { + votes []*types.Signed[*types.LaneVote] + epochWeight map[epoch.Index]uint64 +} + type blockVotes struct { byKey map[types.PublicKey]*types.Signed[*types.LaneVote] - byHash map[types.BlockHeaderHash]*voteSet[*types.Signed[*types.LaneVote]] + byHash map[types.BlockHeaderHash]*blockHashEntry } func newBlockVotes() blockVotes { return blockVotes{ byKey: map[types.PublicKey]*types.Signed[*types.LaneVote]{}, - byHash: map[types.BlockHeaderHash]*voteSet[*types.Signed[*types.LaneVote]]{}, + byHash: map[types.BlockHeaderHash]*blockHashEntry{}, } } -// Returns true iff a new QC has been constructed. -func (bv blockVotes) pushVote(c *types.Committee, vote *types.Signed[*types.LaneVote]) (*types.LaneQC, bool) { +// pushVote records vote in every epoch where the voter is a member. +// Returns true the first time any epoch's accumulated weight reaches its LaneQuorum. +func (bv blockVotes) pushVote(window map[epoch.Index]*types.Committee, vote *types.Signed[*types.LaneVote]) bool { k := vote.Key() - h := vote.Msg().Header().Hash() if _, ok := bv.byKey[k]; ok { - return nil, false + return false } bv.byKey[k] = vote - byHash, ok := bv.byHash[h] + + h := vote.Msg().Header().Hash() + entry, ok := bv.byHash[h] if !ok { - byHash = &voteSet[*types.Signed[*types.LaneVote]]{} - bv.byHash[h] = byHash - } - if byHash.weight >= c.LaneQuorum() { - return nil, false + entry = &blockHashEntry{epochWeight: map[epoch.Index]uint64{}} + bv.byHash[h] = entry } - byHash.weight += c.Weight(k) - byHash.votes = append(byHash.votes, vote) - if byHash.weight >= c.LaneQuorum() { - return types.NewLaneQC(byHash.votes), true + entry.votes = append(entry.votes, vote) + + quorumReached := false + for e, c := range window { + w := c.Weight(k) + if w == 0 { + continue + } + prev := entry.epochWeight[e] + quorum := c.LaneQuorum() + if prev >= quorum { + continue + } + entry.epochWeight[e] = prev + w + if prev+w >= quorum { + quorumReached = true + } } - return nil, false + return quorumReached } diff --git a/sei-tendermint/internal/autobahn/avail/conv_test.go b/sei-tendermint/internal/autobahn/avail/conv_test.go index 2bec6a4042..fd765088b5 100644 --- a/sei-tendermint/internal/autobahn/avail/conv_test.go +++ b/sei-tendermint/internal/autobahn/avail/conv_test.go @@ -4,21 +4,22 @@ import ( "testing" "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/epoch" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/stretchr/testify/require" ) func TestPruneAnchorConv(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) lane := keys[0].Public() block := types.NewBlock(lane, 0, types.BlockHeaderHash{}, types.GenPayload(rng)) laneQCs := map[types.LaneID]*types.LaneQC{ lane: types.NewLaneQC(makeLaneVotes(keys, block.Header())), } - commitQC := makeCommitQC(committee, keys, utils.None[*types.CommitQC](), laneQCs, utils.None[*types.AppQC]()) - appProposal := types.NewAppProposal(commitQC.GlobalRange(committee).First, commitQC.Proposal().Index(), types.GenAppHash(rng)) + commitQC := makeCommitQC(registry, keys, utils.None[*types.CommitQC](), laneQCs, utils.None[*types.AppQC]()) + appProposal := types.NewAppProposal(commitQC.GlobalRange().First, commitQC.Proposal().Index(), types.GenAppHash(rng)) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) require.NoError(t, PruneAnchorConv.Test(&PruneAnchor{ diff --git a/sei-tendermint/internal/autobahn/avail/inner.go b/sei-tendermint/internal/autobahn/avail/inner.go index 601ca4611c..1d8fa5e8ce 100644 --- a/sei-tendermint/internal/autobahn/avail/inner.go +++ b/sei-tendermint/internal/autobahn/avail/inner.go @@ -6,6 +6,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" "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" ) @@ -55,12 +56,25 @@ type loadedAvailState struct { blocks map[types.LaneID][]persist.LoadedBlock } -func newInner(c *types.Committee, loaded utils.Option[*loadedAvailState]) (*inner, error) { +func newInner(registry *epoch.Registry, firstBlock types.GlobalBlockNumber, loaded utils.Option[*loadedAvailState]) (*inner, error) { + // Use the anchor's own committee for prune() so range calculations use the + // correct epoch's lane boundaries. Fall back to the latest known committee. + pruneCommittee := registry.LatestCommittee() + if l, ok := loaded.Get(); ok { + if anchor, ok := l.pruneAnchor.Get(); ok { + pruneCommittee = registry.CommitteeFor(anchor.CommitQC.Proposal().Index()) + } + } + votes := map[types.LaneID]*queue[types.BlockNumber, blockVotes]{} blocks := map[types.LaneID]*queue[types.BlockNumber, *types.Signed[*types.LaneProposal]]{} - for lane := range c.Lanes().All() { - votes[lane] = newQueue[types.BlockNumber, blockVotes]() - blocks[lane] = newQueue[types.BlockNumber, *types.Signed[*types.LaneProposal]]() + for _, c := range registry.EpochWindow() { + for lane := range c.Lanes().All() { + if _, ok := votes[lane]; !ok { + votes[lane] = newQueue[types.BlockNumber, blockVotes]() + blocks[lane] = newQueue[types.BlockNumber, *types.Signed[*types.LaneProposal]]() + } + } } i := &inner{ @@ -70,10 +84,10 @@ func newInner(c *types.Committee, loaded utils.Option[*loadedAvailState]) (*inne commitQCs: newQueue[types.RoadIndex, *types.CommitQC](), blocks: blocks, votes: votes, - nextBlockToPersist: make(map[types.LaneID]types.BlockNumber, c.Lanes().Len()), - persistedBlockStart: make(map[types.LaneID]types.BlockNumber, c.Lanes().Len()), + nextBlockToPersist: make(map[types.LaneID]types.BlockNumber, len(votes)), + persistedBlockStart: make(map[types.LaneID]types.BlockNumber, len(votes)), } - i.appVotes.prune(c.FirstBlock()) + i.appVotes.prune(firstBlock) l, ok := loaded.Get() if !ok { @@ -88,7 +102,7 @@ func newInner(c *types.Committee, loaded utils.Option[*loadedAvailState]) (*inne slog.Uint64("roadIndex", uint64(anchor.AppQC.Proposal().RoadIndex())), slog.Uint64("globalNumber", uint64(anchor.AppQC.Proposal().GlobalNumber())), ) - if _, err := i.prune(c, anchor.AppQC, anchor.CommitQC); err != nil { + if _, err := i.prune(pruneCommittee, firstBlock, anchor.AppQC, anchor.CommitQC); err != nil { return nil, fmt.Errorf("prune: %w", err) } for lane := range i.blocks { @@ -143,18 +157,32 @@ func newInner(c *types.Committee, loaded utils.Option[*loadedAvailState]) (*inne return i, nil } -func (i *inner) laneQC(c *types.Committee, lane types.LaneID, n types.BlockNumber) (*types.LaneQC, bool) { - for _, byHash := range i.votes[lane].q[n].byHash { - if byHash.weight >= c.LaneQuorum() { - return types.NewLaneQC(byHash.votes[:]), true +// laneQCs returns one LaneQC per epoch that has accumulated sufficient weight, +// filtering each QC to only the votes from that epoch's committee members. +func (i *inner) laneQCs(window map[epoch.Index]*types.Committee, lane types.LaneID, n types.BlockNumber) map[epoch.Index]*types.LaneQC { + result := map[epoch.Index]*types.LaneQC{} + for _, entry := range i.votes[lane].q[n].byHash { + for e, c := range window { + if _, ok := result[e]; ok { + continue // already found a quorum hash for this epoch + } + if entry.epochWeight[e] >= c.LaneQuorum() { + var qualified []*types.Signed[*types.LaneVote] + for _, v := range entry.votes { + if c.Weight(v.Key()) > 0 { + qualified = append(qualified, v) + } + } + result[e] = types.NewLaneQC(qualified) + } } } - return nil, false + return result } // prune advances the state to account for a new AppQC/CommitQC pair. // Returns true if pruning occurred, false if the QC was stale. -func (i *inner) prune(c *types.Committee, appQC *types.AppQC, commitQC *types.CommitQC) (bool, error) { +func (i *inner) prune(c *types.Committee, firstBlock types.GlobalBlockNumber, appQC *types.AppQC, commitQC *types.CommitQC) (bool, error) { idx := appQC.Proposal().RoadIndex() if idx != commitQC.Proposal().Index() { return false, fmt.Errorf("mismatched QCs: appQC index %v, commitQC index %v", idx, commitQC.Proposal().Index()) @@ -167,7 +195,7 @@ func (i *inner) prune(c *types.Committee, appQC *types.AppQC, commitQC *types.Co if i.commitQCs.next == idx { i.commitQCs.pushBack(commitQC) } - i.appVotes.prune(commitQC.GlobalRange(c).First) + i.appVotes.prune(commitQC.GlobalRange().First) for lane := range i.votes { lr := commitQC.LaneRange(lane) i.votes[lr.Lane()].prune(lr.First()) diff --git a/sei-tendermint/internal/autobahn/avail/inner_test.go b/sei-tendermint/internal/autobahn/avail/inner_test.go index 257641de63..1ce23d534e 100644 --- a/sei-tendermint/internal/autobahn/avail/inner_test.go +++ b/sei-tendermint/internal/autobahn/avail/inner_test.go @@ -7,13 +7,14 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" "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" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/stretchr/testify/require" ) func TestPruneMismatchedIndices(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) makeCommitQC := func(prev utils.Option[*types.CommitQC]) *types.CommitQC { l := keys[0].Public() @@ -22,10 +23,10 @@ func TestPruneMismatchedIndices(t *testing.T) { lqcs := map[types.LaneID]*types.LaneQC{ l: types.NewLaneQC(makeLaneVotes(keys, b.Header())), } - return makeCommitQC(committee, keys, prev, lqcs, utils.None[*types.AppQC]()) + return makeCommitQC(registry, keys, prev, lqcs, utils.None[*types.AppQC]()) } makeAppQC := func(qcForRange *types.CommitQC, qcForIndex *types.CommitQC) *types.AppQC { - gr := qcForRange.GlobalRange(committee) + gr := qcForRange.GlobalRange() require.True(t, gr.Len() > 0) ap := types.NewAppProposal(gr.First, qcForIndex.Index(), types.GenAppHash(rng)) return types.NewAppQC(makeAppVotes(keys, ap)) @@ -35,7 +36,7 @@ func TestPruneMismatchedIndices(t *testing.T) { qc1 := makeCommitQC(utils.Some(qc0)) t.Logf("test State.PushAppQC") - ds := utils.OrPanic1(data.NewState(&data.Config{Committee: committee}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + ds := utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) state, err := NewState(keys[0], ds, utils.None[string]()) require.NoError(t, err) require.Error(t, state.PushAppQC(makeAppQC(qc0, qc0), qc1), "bad range, bad index should fail") @@ -44,14 +45,14 @@ func TestPruneMismatchedIndices(t *testing.T) { require.NoError(t, state.PushAppQC(makeAppQC(qc1, qc1), qc1), "good range, good index should succeed") t.Logf("test inner.prune") - ds = utils.OrPanic1(data.NewState(&data.Config{Committee: committee}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + ds = utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) state, err = NewState(keys[0], ds, utils.None[string]()) require.NoError(t, err) for inner := range state.inner.Lock() { - _, err := inner.prune(committee, makeAppQC(qc1, qc0), qc1) + _, err := inner.prune(registry.LatestCommittee(), registry.FirstBlock(), makeAppQC(qc1, qc0), qc1) require.Error(t, err, "good range, bad index should fail") require.False(t, inner.latestAppQC.IsPresent(), "latestAppQC should not have been updated") - _, err = inner.prune(committee, makeAppQC(qc1, qc1), qc1) + _, err = inner.prune(registry.LatestCommittee(), registry.FirstBlock(), makeAppQC(qc1, qc1), qc1) require.NoError(t, err, "good range, good index should succeed") } } @@ -64,18 +65,18 @@ func testSignedBlock(key types.SecretKey, lane types.LaneID, n types.BlockNumber func TestNewInnerFreshStart(t *testing.T) { rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 4) + registry, _ := epoch.GenRegistry(rng, 4) - i, err := newInner(committee, utils.None[*loadedAvailState]()) + i, err := newInner(registry, registry.FirstBlock(), utils.None[*loadedAvailState]()) require.NoError(t, err) require.False(t, i.latestAppQC.IsPresent()) require.NotNil(t, i.nextBlockToPersist) require.Equal(t, types.RoadIndex(0), i.commitQCs.first) require.Equal(t, types.RoadIndex(0), i.commitQCs.next) - require.Equal(t, committee.FirstBlock(), i.appVotes.first) - require.Equal(t, committee.FirstBlock(), i.appVotes.next) - for lane := range committee.Lanes().All() { + require.Equal(t, registry.FirstBlock(), i.appVotes.first) + require.Equal(t, registry.FirstBlock(), i.appVotes.next) + for lane := range registry.LatestCommittee().Lanes().All() { require.Equal(t, types.BlockNumber(0), i.blocks[lane].first) require.Equal(t, types.BlockNumber(0), i.blocks[lane].next) require.Equal(t, types.BlockNumber(0), i.votes[lane].first) @@ -85,7 +86,7 @@ func TestNewInnerFreshStart(t *testing.T) { func TestDecodePruneAnchorIncomplete(t *testing.T) { rng := utils.TestRng() - _, keys := types.GenCommittee(rng, 4) + _, keys := epoch.GenRegistry(rng, 4) appProposal := types.NewAppProposal(42, 5, types.GenAppHash(rng)) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) @@ -99,22 +100,22 @@ func TestDecodePruneAnchorIncomplete(t *testing.T) { func TestNewInnerLoadedNoAnchor(t *testing.T) { rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 4) + registry, _ := epoch.GenRegistry(rng, 4) loaded := &loadedAvailState{} - i, err := newInner(committee, utils.Some(loaded)) + i, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.NoError(t, err) - // No anchor loaded, app votes should start at the committee's first block. + // No anchor loaded, app votes should start at the registry's first block. require.False(t, i.latestAppQC.IsPresent()) require.Equal(t, types.RoadIndex(0), i.commitQCs.first) - require.Equal(t, committee.FirstBlock(), i.appVotes.first) + require.Equal(t, registry.FirstBlock(), i.appVotes.first) } func TestNewInnerLoadedBlocksContiguous(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) lane := keys[0].Public() // Build 3 contiguous blocks: 0, 1, 2. @@ -130,7 +131,7 @@ func TestNewInnerLoadedBlocksContiguous(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - i, err := newInner(committee, utils.Some(loaded)) + i, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.NoError(t, err) q := i.blocks[lane] @@ -143,7 +144,7 @@ func TestNewInnerLoadedBlocksContiguous(t *testing.T) { // nextBlockToPersist: loaded lane at q.next, other lanes at 0 (map zero-value). require.NotNil(t, i.nextBlockToPersist) require.Equal(t, types.BlockNumber(3), i.nextBlockToPersist[lane]) - for other := range committee.Lanes().All() { + for other := range registry.LatestCommittee().Lanes().All() { if other != lane { require.Equal(t, types.BlockNumber(0), i.nextBlockToPersist[other]) } @@ -152,14 +153,14 @@ func TestNewInnerLoadedBlocksContiguous(t *testing.T) { func TestNewInnerLoadedBlocksEmptySlice(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) lane := keys[0].Public() loaded := &loadedAvailState{ blocks: map[types.LaneID][]persist.LoadedBlock{lane: {}}, } - i, err := newInner(committee, utils.Some(loaded)) + i, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.NoError(t, err) q := i.blocks[lane] @@ -169,7 +170,7 @@ func TestNewInnerLoadedBlocksEmptySlice(t *testing.T) { func TestNewInnerLoadedBlocksUnknownLane(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) unknownKey := types.GenSecretKey(rng) unknownLane := unknownKey.Public() @@ -179,10 +180,10 @@ func TestNewInnerLoadedBlocksUnknownLane(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{unknownLane: {{Number: 0, Proposal: b}}}, } - i, err := newInner(committee, utils.Some(loaded)) + i, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.NoError(t, err) - for lane := range committee.Lanes().All() { + for lane := range registry.LatestCommittee().Lanes().All() { q := i.blocks[lane] require.Equal(t, types.BlockNumber(0), q.first) require.Equal(t, types.BlockNumber(0), q.next) @@ -192,7 +193,7 @@ func TestNewInnerLoadedBlocksUnknownLane(t *testing.T) { func TestNewInnerLoadedBlocksMultipleLanes(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) lane0 := keys[0].Public() lane1 := keys[1].Public() @@ -216,7 +217,7 @@ func TestNewInnerLoadedBlocksMultipleLanes(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane0: bs0, lane1: bs1}, } - i, err := newInner(committee, utils.Some(loaded)) + i, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.NoError(t, err) q0 := i.blocks[lane0] @@ -234,13 +235,13 @@ func TestNewInnerLoadedBlocksMultipleLanes(t *testing.T) { func TestNewInnerLoadedCommitQCsNoAppQC(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) // Create 3 sequential CommitQCs. qcs := make([]*types.CommitQC, 3) prev := utils.None[*types.CommitQC]() for i := range qcs { - qcs[i] = makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + qcs[i] = makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(qcs[i]) } @@ -253,7 +254,7 @@ func TestNewInnerLoadedCommitQCsNoAppQC(t *testing.T) { commitQCs: loadedQCs, } - inner, err := newInner(committee, utils.Some(loaded)) + inner, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.NoError(t, err) // Without anchor, commitQCs.first = 0. All 3 should be restored. @@ -271,7 +272,7 @@ func TestNewInnerLoadedCommitQCsNoAppQC(t *testing.T) { func TestNewInnerLoadedCommitQCsWithAppQC(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) // AppQC at road index 2. roadIdx := types.RoadIndex(2) @@ -283,7 +284,7 @@ func TestNewInnerLoadedCommitQCsWithAppQC(t *testing.T) { qcs := make([]*types.CommitQC, 5) prev := utils.None[*types.CommitQC]() for i := range qcs { - qcs[i] = makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + qcs[i] = makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(qcs[i]) } @@ -299,7 +300,7 @@ func TestNewInnerLoadedCommitQCsWithAppQC(t *testing.T) { commitQCs: loadedQCs, } - inner, err := newInner(committee, utils.Some(loaded)) + inner, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.NoError(t, err) // latestAppQC should be set by prune. @@ -323,7 +324,7 @@ func TestNewInnerLoadedCommitQCsWithAppQC(t *testing.T) { func TestNewInnerLoadedAllThree(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) lane := keys[0].Public() // AppQC at road index 2. @@ -335,7 +336,7 @@ func TestNewInnerLoadedAllThree(t *testing.T) { qcs := make([]*types.CommitQC, 5) prev := utils.None[*types.CommitQC]() for i := range qcs { - qcs[i] = makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + qcs[i] = makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(qcs[i]) } // Pre-filtered: only commitQCs >= anchor road index (2). @@ -360,7 +361,7 @@ func TestNewInnerLoadedAllThree(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - inner, err := newInner(committee, utils.Some(loaded)) + inner, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.NoError(t, err) // AppQC restored. @@ -386,10 +387,10 @@ func TestNewInnerLoadedAllThree(t *testing.T) { func TestPruneAdvancesNextBlockToPersist(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) lane := keys[0].Public() - i, err := newInner(committee, utils.None[*loadedAvailState]()) + i, err := newInner(registry, registry.FirstBlock(), utils.None[*loadedAvailState]()) require.NoError(t, err) // Push blocks 0-4 on one lane. @@ -411,11 +412,11 @@ func TestPruneAdvancesNextBlockToPersist(t *testing.T) { h := i.blocks[lane].q[bn].Msg().Block().Header() laneQCs := map[types.LaneID]*types.LaneQC{ lane: types.NewLaneQC(makeLaneVotes( - types.TestKeysWithWeight(committee, keys, committee.LaneQuorum()), + types.TestKeysWithWeight(registry.LatestCommittee(), keys, registry.LatestCommittee().LaneQuorum()), h, )), } - qcs[j] = makeCommitQC(committee, keys, prev, laneQCs, utils.None[*types.AppQC]()) + qcs[j] = makeCommitQC(registry, keys, prev, laneQCs, utils.None[*types.AppQC]()) prev = utils.Some(qcs[j]) i.commitQCs.pushBack(qcs[j]) } @@ -429,7 +430,7 @@ func TestPruneAdvancesNextBlockToPersist(t *testing.T) { appProposal := types.NewAppProposal(10, 2, types.GenAppHash(rng)) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) - updated, err := i.prune(committee, appQC, qcs[2]) + updated, err := i.prune(registry.LatestCommittee(), 0, appQC, qcs[2]) require.NoError(t, err) require.True(t, updated) @@ -445,7 +446,7 @@ func TestPruneAdvancesNextBlockToPersist(t *testing.T) { func TestNewInnerLoadedCommitQCsAllBeforeAppQCArePruned(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) // Build 6 CommitQCs (indices 0-5). Anchor at index 5. // All stale commitQCs (0-4) were already filtered by loadPersistedState, @@ -453,7 +454,7 @@ func TestNewInnerLoadedCommitQCsAllBeforeAppQCArePruned(t *testing.T) { qcs := make([]*types.CommitQC, 6) prev := utils.None[*types.CommitQC]() for i := range qcs { - qcs[i] = makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + qcs[i] = makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(qcs[i]) } @@ -464,7 +465,7 @@ func TestNewInnerLoadedCommitQCsAllBeforeAppQCArePruned(t *testing.T) { pruneAnchor: utils.Some(&PruneAnchor{AppQC: appQC, CommitQC: qcs[5]}), } - inner, err := newInner(committee, utils.Some(loaded)) + inner, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.NoError(t, err) // prune() pushes the anchor's CommitQC into the queue. @@ -475,14 +476,14 @@ func TestNewInnerLoadedCommitQCsAllBeforeAppQCArePruned(t *testing.T) { func TestNewInnerAnchorWithNoCommitQCFiles(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) // Simulate crash between anchor write and CommitQC file write: // anchor has AppQC@3 + CommitQC@3, but no CommitQC files on disk. qcs := make([]*types.CommitQC, 4) prev := utils.None[*types.CommitQC]() for i := range qcs { - qcs[i] = makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + qcs[i] = makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(qcs[i]) } @@ -493,7 +494,7 @@ func TestNewInnerAnchorWithNoCommitQCFiles(t *testing.T) { pruneAnchor: utils.Some(&PruneAnchor{AppQC: appQC, CommitQC: qcs[3]}), } - inner, err := newInner(committee, utils.Some(loaded)) + inner, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.NoError(t, err) // prune() should push the anchor's CommitQC into the queue. @@ -507,7 +508,7 @@ func TestNewInnerAnchorWithNoCommitQCFiles(t *testing.T) { require.Equal(t, types.RoadIndex(3), aq.Proposal().RoadIndex()) // persistedBlockStart should be initialized from the anchor's CommitQC. - for lane := range committee.Lanes().All() { + for lane := range registry.LatestCommittee().Lanes().All() { expected := qcs[3].LaneRange(lane).First() require.Equal(t, expected, inner.persistedBlockStart[lane]) } @@ -515,12 +516,12 @@ func TestNewInnerAnchorWithNoCommitQCFiles(t *testing.T) { func TestNewInnerLoadedCommitQCsGapReturnsError(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) qcs := make([]*types.CommitQC, 3) prev := utils.None[*types.CommitQC]() for i := range qcs { - qcs[i] = makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + qcs[i] = makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(qcs[i]) } @@ -536,20 +537,20 @@ func TestNewInnerLoadedCommitQCsGapReturnsError(t *testing.T) { commitQCs: loadedQCs, } - _, err := newInner(committee, utils.Some(loaded)) + _, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.Error(t, err) require.Contains(t, err.Error(), "non-contiguous") } func TestNewInnerLoadedCommitQCsEmpty(t *testing.T) { rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 4) + registry, _ := epoch.GenRegistry(rng, 4) loaded := &loadedAvailState{ commitQCs: nil, } - inner, err := newInner(committee, utils.Some(loaded)) + inner, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.NoError(t, err) require.Equal(t, types.RoadIndex(0), inner.commitQCs.first) @@ -560,7 +561,7 @@ func TestNewInnerLoadedCommitQCsEmpty(t *testing.T) { func TestNewInnerLoadedCommitQCsGapWithAppQCAnchor(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) // Simulate crash scenario: disk had stale QCs [0,1,2] and a new QC at // index 10. loadPersistedState pre-filters stale entries, so newInner @@ -568,7 +569,7 @@ func TestNewInnerLoadedCommitQCsGapWithAppQCAnchor(t *testing.T) { qcs := make([]*types.CommitQC, 11) prev := utils.None[*types.CommitQC]() for i := range qcs { - qcs[i] = makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + qcs[i] = makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(qcs[i]) } @@ -584,7 +585,7 @@ func TestNewInnerLoadedCommitQCsGapWithAppQCAnchor(t *testing.T) { commitQCs: loadedQCs, } - inner, err := newInner(committee, utils.Some(loaded)) + inner, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.NoError(t, err) // Only QC@10 loaded. @@ -604,7 +605,7 @@ func TestNewInnerLoadedCommitQCsGapWithAppQCAnchor(t *testing.T) { func TestNewInnerLoadedCommitQCsBelowAnchorSkipped(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) // Build 6 CommitQCs (0-5). Anchor at index 3. // Loaded list includes stale entries [1, 2] below the anchor plus [3, 4, 5]. @@ -613,7 +614,7 @@ func TestNewInnerLoadedCommitQCsBelowAnchorSkipped(t *testing.T) { qcs := make([]*types.CommitQC, 6) prev := utils.None[*types.CommitQC]() for i := range qcs { - qcs[i] = makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + qcs[i] = makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(qcs[i]) } @@ -633,7 +634,7 @@ func TestNewInnerLoadedCommitQCsBelowAnchorSkipped(t *testing.T) { commitQCs: loadedQCs, } - inner, err := newInner(committee, utils.Some(loaded)) + inner, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.NoError(t, err) // prune(3) pushes QC@3 (next=4). Indices 1,2,3 are skipped. 4,5 pushed. @@ -646,7 +647,7 @@ func TestNewInnerLoadedCommitQCsBelowAnchorSkipped(t *testing.T) { func TestNewInnerLoadedCommitQCsGapAfterAnchorReturnsError(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) // Anchor at index 2. Loaded commitQCs are [2, 3, 5] — gap at 4. // After prune(2), next=3. Index 2 is skipped, 3 pushed (next=4), @@ -654,7 +655,7 @@ func TestNewInnerLoadedCommitQCsGapAfterAnchorReturnsError(t *testing.T) { qcs := make([]*types.CommitQC, 6) prev := utils.None[*types.CommitQC]() for i := range qcs { - qcs[i] = makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + qcs[i] = makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(qcs[i]) } @@ -672,14 +673,14 @@ func TestNewInnerLoadedCommitQCsGapAfterAnchorReturnsError(t *testing.T) { commitQCs: loadedQCs, } - _, err := newInner(committee, utils.Some(loaded)) + _, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.Error(t, err) require.Contains(t, err.Error(), "non-contiguous") } func TestNewInnerLoadedBlocksGapReturnsError(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) lane := keys[0].Public() // Blocks 3, 4, 6, 7 with no anchor — queue starts at 0, so block 3 @@ -696,14 +697,14 @@ func TestNewInnerLoadedBlocksGapReturnsError(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - _, err := newInner(committee, utils.Some(loaded)) + _, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.Error(t, err) require.Contains(t, err.Error(), "non-contiguous") } func TestNewInnerLoadedBlocksParentHashMismatchReturnsError(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) lane := keys[0].Public() // Build blocks 0, 1 with correct chaining, then block 2 with wrong parent. @@ -724,14 +725,14 @@ func TestNewInnerLoadedBlocksParentHashMismatchReturnsError(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - _, err := newInner(committee, utils.Some(loaded)) + _, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.Error(t, err) require.Contains(t, err.Error(), "parent hash mismatch") } func TestNewInnerLoadedBlocksOverCapacityReturnsError(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) lane := keys[0].Public() // Build BlocksPerLane + 5 contiguous blocks — more than the lane capacity. @@ -750,21 +751,21 @@ func TestNewInnerLoadedBlocksOverCapacityReturnsError(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - _, err := newInner(committee, utils.Some(loaded)) + _, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.Error(t, err) require.Contains(t, err.Error(), "exceeds capacity") } func TestNewInnerPruneAnchorPrunesBlockQueues(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) - initialBlock := committee.FirstBlock() + registry, keys := epoch.GenRegistry(rng, 4) + initialBlock := types.GlobalBlockNumber(0) // Build CommitQCs 0-2. qcs := make([]*types.CommitQC, 3) prev := utils.None[*types.CommitQC]() for i := range qcs { - qcs[i] = makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + qcs[i] = makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(qcs[i]) } @@ -793,11 +794,11 @@ func TestNewInnerPruneAnchorPrunesBlockQueues(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - i, err := newInner(committee, utils.Some(loaded)) + i, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.NoError(t, err) // prune() should advance block queue first to the prune anchor's lane range. - for l := range committee.Lanes().All() { + for l := range registry.LatestCommittee().Lanes().All() { expected := pruneQC.LaneRange(l).First() require.Equal(t, expected, i.blocks[l].first, "blocks[%v].first should be advanced by prune to prune anchor lane range", l) @@ -806,14 +807,14 @@ func TestNewInnerPruneAnchorPrunesBlockQueues(t *testing.T) { func TestNewInnerPruneAnchorCommitQCUsedForPrune(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) - initialBlock := committee.FirstBlock() + registry, keys := epoch.GenRegistry(rng, 4) + initialBlock := types.GlobalBlockNumber(0) // Build CommitQCs 0-2. qcs := make([]*types.CommitQC, 3) prev := utils.None[*types.CommitQC]() for i := range qcs { - qcs[i] = makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + qcs[i] = makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(qcs[i]) } @@ -829,7 +830,7 @@ func TestNewInnerPruneAnchorCommitQCUsedForPrune(t *testing.T) { }, } - i, err := newInner(committee, utils.Some(loaded)) + i, err := newInner(registry, registry.FirstBlock(), utils.Some(loaded)) require.NoError(t, err) // prune(appQC@1, pruneQC@1) should advance commitQCs.first to 1. diff --git a/sei-tendermint/internal/autobahn/avail/state.go b/sei-tendermint/internal/autobahn/avail/state.go index 383c4874bd..f9fbea57e1 100644 --- a/sei-tendermint/internal/autobahn/avail/state.go +++ b/sei-tendermint/internal/autobahn/avail/state.go @@ -9,6 +9,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" "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" @@ -156,7 +157,7 @@ func NewState(key types.SecretKey, data *data.State, stateDir utils.Option[strin return nil, err } - inner, err := newInner(data.Committee(), loaded) + inner, err := newInner(data.Registry(), data.Registry().FirstBlock(), loaded) if err != nil { return nil, err } @@ -165,7 +166,7 @@ func NewState(key types.SecretKey, data *data.State, stateDir utils.Option[strin // loadPersistedState. if ls, ok := loaded.Get(); ok { if anchor, ok := ls.pruneAnchor.Get(); ok { - for lane := range data.Committee().Lanes().All() { + for lane := range data.Registry().LatestCommittee().Lanes().All() { if err := pers.blocks.MaybePruneAndPersistLane(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) } @@ -261,7 +262,7 @@ func (s *State) PushCommitQC(ctx context.Context, qc *types.CommitQC) error { return err } } - if err := qc.Verify(s.data.Committee()); err != nil { + if err := qc.Verify(s.data.Registry().CommitteeFor(qc.Proposal().Index())); err != nil { return fmt.Errorf("qc.Verify(): %w", err) } for inner, ctrl := range s.inner.Lock() { @@ -280,10 +281,11 @@ func (s *State) PushCommitQC(ctx context.Context, qc *types.CommitQC) error { // PushAppVote pushes an AppVote to the state. func (s *State) PushAppVote(ctx context.Context, v *types.Signed[*types.AppVote]) error { - if err := v.VerifySig(s.data.Committee()); err != nil { + idx := v.Msg().Proposal().RoadIndex() + committee := s.data.Registry().CommitteeFor(idx) + if err := v.VerifySig(committee); err != nil { return fmt.Errorf("v.VerifySig(): %w", err) } - idx := v.Msg().Proposal().RoadIndex() // Wait for the corresponding commitQC. if err := s.waitForCommitQC(ctx, idx); err != nil { return err @@ -295,7 +297,7 @@ func (s *State) PushAppVote(ctx context.Context, v *types.Signed[*types.AppVote] } // Verify the vote against the CommitQC. qc := inner.commitQCs.q[idx] - if err := v.Msg().Proposal().Verify(s.data.Committee(), qc); err != nil { + if err := v.Msg().Proposal().Verify(committee, qc); err != nil { return fmt.Errorf("invalid vote: %w", err) } // Push the vote. @@ -304,11 +306,11 @@ func (s *State) PushAppVote(ctx context.Context, v *types.Signed[*types.AppVote] for q.next <= n { q.pushBack(newAppVotes()) } - appQC, ok := q.q[n].pushVote(s.data.Committee(), v) + appQC, ok := q.q[n].pushVote(committee, v) if !ok { return nil } - updated, err := inner.prune(s.data.Committee(), appQC, qc) + updated, err := inner.prune(committee, s.data.Registry().FirstBlock(), appQC, qc) if err != nil { return err } @@ -328,7 +330,7 @@ func (s *State) PushAppQC(appQC *types.AppQC, commitQC *types.CommitQC) error { return nil } } - c := s.data.Committee() + c := s.data.Registry().CommitteeFor(commitQC.Proposal().Index()) if err := appQC.Verify(c); err != nil { return fmt.Errorf("appQC.Verify(): %w", err) } @@ -340,11 +342,11 @@ func (s *State) PushAppQC(appQC *types.AppQC, commitQC *types.CommitQC) error { } // Defense-in-depth check, it should never happen that >f validators sign // a proposal which does not match the commitQC's global range. - if !commitQC.GlobalRange(c).Has(appQC.Proposal().GlobalNumber()) { + if !commitQC.GlobalRange().Has(appQC.Proposal().GlobalNumber()) { return fmt.Errorf("appQC GlobalNumber not in commitQC range") } for inner, ctrl := range s.inner.Lock() { - updated, err := inner.prune(s.data.Committee(), appQC, commitQC) + updated, err := inner.prune(c, s.data.Registry().FirstBlock(), appQC, commitQC) if err != nil { return err } @@ -392,12 +394,14 @@ func (s *State) PushBlock(ctx context.Context, p *types.Signed[*types.LanePropos if p.Key() != h.Lane() { return fmt.Errorf("signer %v does not match lane %v", p.Key(), h.Lane()) } - if err := p.Msg().Verify(s.data.Committee()); err != nil { + if err := s.data.Registry().VerifyInWindow(func(c *types.Committee) error { + if err := p.Msg().Verify(c); err != nil { + return err + } + return p.VerifySig(c) + }); err != nil { return fmt.Errorf("block.Verify(): %w", err) } - if err := p.VerifySig(s.data.Committee()); err != nil { - return fmt.Errorf("p.VerifySig(): %w", err) - } for inner, ctrl := range s.inner.Lock() { q, ok := inner.blocks[h.Lane()] if !ok { @@ -442,12 +446,15 @@ func (s *State) PushBlock(ctx context.Context, p *types.Signed[*types.LanePropos // Waits until the lane has enough capacity for the new vote. // It does NOT wait for the previous votes. func (s *State) PushVote(ctx context.Context, vote *types.Signed[*types.LaneVote]) error { - if err := vote.Msg().Verify(s.data.Committee()); err != nil { - return fmt.Errorf("vote.Msg().Verify(): %w", err) - } - if err := vote.VerifySig(s.data.Committee()); err != nil { - return fmt.Errorf("vote.VerifySig(): %w", err) + if err := s.data.Registry().VerifyInWindow(func(c *types.Committee) error { + if err := vote.Msg().Verify(c); err != nil { + return err + } + return vote.VerifySig(c) + }); err != nil { + return fmt.Errorf("vote.Verify(): %w", err) } + window := s.data.Registry().EpochWindow() h := vote.Msg().Header() for inner, ctrl := range s.inner.Lock() { q, ok := inner.votes[h.Lane()] @@ -465,7 +472,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(s.data.Committee(), vote); ok { + if q.q[h.BlockNumber()].pushVote(window, vote) { ctrl.Updated() } } @@ -490,8 +497,8 @@ func (s *State) headers(ctx context.Context, lr *types.LaneRange) ([]*types.Bloc return nil, data.ErrPruned } // Check if we have the header. - if byHash, ok := q.q[n].byHash[want]; ok { - h := byHash.votes[0].Msg().Header() + if entry, ok := q.q[n].byHash[want]; ok { + h := entry.votes[0].Msg().Header() want = h.ParentHash() headers[len(headers)-i-1] = h break @@ -514,7 +521,7 @@ func (s *State) fullCommitQC(ctx context.Context, n types.RoadIndex) (*types.Ful } // Collect the headers from the votes. var commitHeaders []*types.BlockHeader - for lane := range s.data.Committee().Lanes().All() { + for lane := range s.data.Registry().CommitteeFor(qc.Proposal().Index()).Lanes().All() { headers, err := s.headers(ctx, qc.LaneRange(lane)) if err != nil { return nil, err @@ -537,18 +544,20 @@ func (s *State) WaitForLocalCapacity(ctx context.Context, toProduce types.BlockN return nil } -// WaitForLaneQCs waits until there is at least 1 LaneQC with a block not finalized by prev. +// WaitForLaneQCs waits until there is at least 1 LaneQC (for the given epoch) +// with a block not finalized by prev. func (s *State) WaitForLaneQCs( - ctx context.Context, prev utils.Option[*types.CommitQC], + ctx context.Context, prev utils.Option[*types.CommitQC], e epoch.Index, ) (map[types.LaneID]*types.LaneQC, error) { - c := s.data.Committee() + window := s.data.Registry().EpochWindow() for inner, ctrl := range s.inner.Lock() { laneQCs := map[types.LaneID]*types.LaneQC{} for { - for lane := range c.Lanes().All() { + for lane := range inner.blocks { first := types.LaneRangeOpt(prev, lane).Next() for i := range types.BlockNumber(BlocksPerLanePerCommit) { - if qc, ok := inner.laneQC(c, lane, first+i); ok { + qcs := inner.laneQCs(window, lane, first+i) + if qc, ok := qcs[e]; ok { laneQCs[lane] = qc } else { break @@ -612,7 +621,6 @@ func (s *State) Run(ctx context.Context) error { }) // Task inserting FullCommitQCs and local blocks to data state. scope.SpawnNamed("s.data.PushQC", func() error { - c := s.data.Committee() for n := types.RoadIndex(0); ; n = max(n+1, s.FirstCommitQC()) { qc, err := s.fullCommitQC(ctx, n) if err != nil { @@ -623,6 +631,7 @@ func (s *State) Run(ctx context.Context) error { } // Collect the blocks we have locally. + c := s.data.Registry().CommitteeFor(qc.QC().Proposal().Index()) var blocks []*types.Block for inner := range s.inner.Lock() { for lane := range c.Lanes().All() { @@ -685,7 +694,7 @@ 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], s.data.Committee().Lanes().Len()) + 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) @@ -699,11 +708,18 @@ func (s *State) runPersist(ctx context.Context, pers persisters) error { s.markCommitQCsPersisted(qc) })) }) - for lane := range s.data.Committee().Lanes().All() { - proposals := blocksByLane[lane] - ps.Spawn(func() error { - return pers.blocks.MaybePruneAndPersistLane(lane, anchorQC, proposals, utils.Some(markBlock)) - }) + seenLane := map[types.LaneID]struct{}{} + for _, c := range s.data.Registry().EpochWindow() { + for lane := range c.Lanes().All() { + if _, ok := seenLane[lane]; ok { + continue + } + seenLane[lane] = struct{}{} + proposals := blocksByLane[lane] + ps.Spawn(func() error { + return pers.blocks.MaybePruneAndPersistLane(lane, anchorQC, proposals, utils.Some(markBlock)) + }) + } } return nil }); err != nil { diff --git a/sei-tendermint/internal/autobahn/avail/state_test.go b/sei-tendermint/internal/autobahn/avail/state_test.go index 4bb2d3e2fa..8bddd57c9a 100644 --- a/sei-tendermint/internal/autobahn/avail/state_test.go +++ b/sei-tendermint/internal/autobahn/avail/state_test.go @@ -12,6 +12,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" "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/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/scope" @@ -53,27 +54,15 @@ func leaderKey(committee *types.Committee, keys []types.SecretKey, view types.Vi } func makeCommitQC( - committee *types.Committee, + registry *epoch.Registry, keys []types.SecretKey, prev utils.Option[*types.CommitQC], laneQCs map[types.LaneID]*types.LaneQC, appQC utils.Option[*types.AppQC], ) *types.CommitQC { - vs := types.ViewSpec{CommitQC: prev} - fullProposal := utils.OrPanic1(types.NewProposal( - leaderKey(committee, keys, vs.View()), - committee, - vs, - time.Now(), - laneQCs, - appQC, - )) - vote := types.NewCommitVote(fullProposal.Proposal().Msg()) - var votes []*types.Signed[*types.CommitVote] - for _, k := range keys { - votes = append(votes, types.Sign(k, vote)) - } - return types.NewCommitQC(votes) + vs := types.ViewSpec{CommitQC: prev, FirstBlock: registry.FirstBlock()} + committee := registry.CommitteeFor(vs.View().Index) + return types.BuildCommitQC(committee, keys, prev, registry.FirstBlock(), time.Time{}, laneQCs, appQC) } func qcPayloadHashes(qc *types.FullCommitQC) byLane[types.PayloadHash] { @@ -102,12 +91,13 @@ func testState(t *testing.T, stateDir utils.Option[string]) { t.Helper() ctx := t.Context() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) + committee := registry.LatestCommittee() if err := scope.Run(ctx, func(ctx context.Context, s scope.Scope) error { ds := utils.OrPanic1(data.NewState(&data.Config{ - Committee: committee, - }, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + Registry: registry, + }, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) s.SpawnBgNamed("data.State.Run()", func() error { return utils.IgnoreCancel(ds.Run(ctx)) }) @@ -154,17 +144,17 @@ func testState(t *testing.T, stateDir utils.Option[string]) { } t.Logf("Push a commit QC.") - laneQCs, err := state.WaitForLaneQCs(ctx, prev) + laneQCs, err := state.WaitForLaneQCs(ctx, prev, 0) if err != nil { return fmt.Errorf("state.WaitForNewLaneQCs(): %w", err) } - qc := makeCommitQC(committee, keys, prev, laneQCs, state.LastAppQC()) + qc := makeCommitQC(registry, keys, prev, laneQCs, state.LastAppQC()) if err := state.PushCommitQC(ctx, qc); err != nil { return fmt.Errorf("state.PushCommitQC(): %w", err) } t.Logf("Push app votes.") - appProposal := types.NewAppProposal(qc.GlobalRange(committee).Next-1, qc.Proposal().Index(), types.GenAppHash(rng)) + appProposal := types.NewAppProposal(qc.GlobalRange().Next-1, qc.Proposal().Index(), types.GenAppHash(rng)) for _, vote := range makeAppVotes(keys, appProposal) { if err := state.PushAppVote(ctx, vote); err != nil { return fmt.Errorf("state.PushAppVote(): %w", err) @@ -200,7 +190,7 @@ func testState(t *testing.T, stateDir utils.Option[string]) { } t.Logf("Check that the blocks were successfully pushed to data state.") - gr := got.QC().GlobalRange(committee) + gr := got.QC().GlobalRange() for i := gr.First; i < gr.Next; i++ { b, err := ds.Block(ctx, i) if err != nil { @@ -228,7 +218,8 @@ func testState(t *testing.T, stateDir utils.Option[string]) { // loadPersistedState (stale entries below the prune anchor are discarded). func TestStateRestartFromPersisted(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) + committee := registry.LatestCommittee() dir := t.TempDir() // Phase 1: Run state with persistence through 2 iterations. @@ -236,7 +227,7 @@ func TestStateRestartFromPersisted(t *testing.T) { var wantNextBlocks map[types.LaneID]types.BlockNumber require.NoError(t, scope.Run(t.Context(), func(ctx context.Context, s scope.Scope) error { - ds := utils.OrPanic1(data.NewState(&data.Config{Committee: committee}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + ds := utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) s.SpawnBgNamed("data.Run", func() error { return utils.IgnoreCancel(ds.Run(ctx)) }) @@ -274,16 +265,16 @@ func TestStateRestartFromPersisted(t *testing.T) { } } - laneQCs, err := state.WaitForLaneQCs(ctx, prev) + laneQCs, err := state.WaitForLaneQCs(ctx, prev, 0) if err != nil { return fmt.Errorf("WaitForLaneQCs: %w", err) } - qc := makeCommitQC(committee, keys, prev, laneQCs, state.LastAppQC()) + qc := makeCommitQC(registry, keys, prev, laneQCs, state.LastAppQC()) if err := state.PushCommitQC(ctx, qc); err != nil { return fmt.Errorf("PushCommitQC: %w", err) } - appProposal := types.NewAppProposal(qc.GlobalRange(committee).Next-1, qc.Proposal().Index(), types.GenAppHash(rng)) + appProposal := types.NewAppProposal(qc.GlobalRange().Next-1, qc.Proposal().Index(), types.GenAppHash(rng)) for _, vote := range makeAppVotes(keys, appProposal) { if err := state.PushAppVote(ctx, vote); err != nil { return fmt.Errorf("PushAppVote: %w", err) @@ -311,7 +302,7 @@ func TestStateRestartFromPersisted(t *testing.T) { })) // Phase 2: Restart from the same directory. - ds2 := utils.OrPanic1(data.NewState(&data.Config{Committee: committee}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + ds2 := utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) state2, err := NewState(keys[0], ds2, utils.Some(dir)) require.NoError(t, err) @@ -332,22 +323,25 @@ func TestStateRestartFromPersisted(t *testing.T) { func TestStateMismatchedQCs(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) - initialBlock := committee.FirstBlock() + registry, keys := epoch.GenRegistry(rng, 4) + committee := registry.LatestCommittee() + initialBlock := registry.FirstBlock() ds := utils.OrPanic1(data.NewState(&data.Config{ - Committee: committee, - }, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + Registry: registry, + }, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) state, err := NewState(keys[0], ds, utils.None[string]()) require.NoError(t, err) // Helper to create a CommitQC for a specific index makeQC := func(prev utils.Option[*types.CommitQC], laneQCs map[types.LaneID]*types.LaneQC) *types.CommitQC { - vs := types.ViewSpec{CommitQC: prev} + vs := types.ViewSpec{CommitQC: prev, FirstBlock: initialBlock} fullProposal := utils.OrPanic1(types.NewProposal( leaderKey(committee, keys, vs.View()), committee, vs, + initialBlock, + time.Time{}, time.Now(), laneQCs, utils.None[*types.AppQC](), @@ -374,8 +368,8 @@ func TestStateMismatchedQCs(t *testing.T) { // 3. Create CommitQC for index 0 (finalizes block 0) qc0 := makeQC(utils.None[*types.CommitQC](), map[types.LaneID]*types.LaneQC{lane: laneQC}) - require.Equal(t, initialBlock, qc0.GlobalRange(committee).First) - require.Equal(t, initialBlock+1, qc0.GlobalRange(committee).Next) + require.Equal(t, initialBlock, qc0.GlobalRange().First) + require.Equal(t, initialBlock+1, qc0.GlobalRange().Next) t.Run("PushAppQC mismatch", func(t *testing.T) { require := require.New(t) @@ -391,11 +385,11 @@ func TestStateMismatchedQCs(t *testing.T) { func TestPushBlockRejectsBadParentHash(t *testing.T) { ctx := t.Context() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) ds := utils.OrPanic1(data.NewState(&data.Config{ - Committee: committee, - }, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + Registry: registry, + }, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) state := utils.OrPanic1(NewState(keys[0], ds, utils.None[string]())) // Produce a valid first block on our lane. @@ -416,11 +410,11 @@ func TestPushBlockRejectsBadParentHash(t *testing.T) { func TestPushBlockRejectsWrongSigner(t *testing.T) { ctx := t.Context() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) ds := utils.OrPanic1(data.NewState(&data.Config{ - Committee: committee, - }, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + Registry: registry, + }, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) state := utils.OrPanic1(NewState(keys[0], ds, utils.None[string]())) // Create a block on keys[0]'s lane but sign it with keys[1]. @@ -434,12 +428,12 @@ func TestPushBlockRejectsWrongSigner(t *testing.T) { func TestNewStateWithPersistence(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) - initialBlock := committee.FirstBlock() + registry, keys := epoch.GenRegistry(rng, 4) + initialBlock := types.GlobalBlockNumber(0) t.Run("empty dir loads fresh state", func(t *testing.T) { dir := t.TempDir() - ds := utils.OrPanic1(data.NewState(&data.Config{Committee: committee}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + ds := utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) state, err := NewState(keys[0], ds, utils.Some(dir)) require.NoError(t, err) @@ -452,7 +446,7 @@ func TestNewStateWithPersistence(t *testing.T) { t.Run("loads persisted AppQC", func(t *testing.T) { dir := t.TempDir() - ds := utils.OrPanic1(data.NewState(&data.Config{Committee: committee}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + ds := utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) roadIdx := types.RoadIndex(7) globalNum := types.GlobalBlockNumber(50) @@ -465,7 +459,7 @@ func TestNewStateWithPersistence(t *testing.T) { prev := utils.None[*types.CommitQC]() var pruneQC *types.CommitQC for i := types.RoadIndex(0); i <= roadIdx; i++ { - qc := makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + qc := makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(qc) require.NoError(t, cp.MaybePruneAndPersist(utils.None[*types.CommitQC](), []*types.CommitQC{qc}, noCommitQCCB)) pruneQC = qc @@ -493,7 +487,7 @@ func TestNewStateWithPersistence(t *testing.T) { t.Run("loads persisted blocks", func(t *testing.T) { dir := t.TempDir() - ds := utils.OrPanic1(data.NewState(&data.Config{Committee: committee}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + ds := utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) lane := keys[0].Public() // Persist blocks using BlockPersister. @@ -517,7 +511,7 @@ func TestNewStateWithPersistence(t *testing.T) { t.Run("loads persisted AppQC and blocks together", func(t *testing.T) { dir := t.TempDir() - ds := utils.OrPanic1(data.NewState(&data.Config{Committee: committee}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + ds := utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) lane := keys[0].Public() roadIdx := types.RoadIndex(2) @@ -531,7 +525,7 @@ func TestNewStateWithPersistence(t *testing.T) { prev := utils.None[*types.CommitQC]() var pruneQC *types.CommitQC for range roadIdx + 1 { - qc := makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + qc := makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(qc) require.NoError(t, cp.MaybePruneAndPersist(utils.None[*types.CommitQC](), []*types.CommitQC{qc}, noCommitQCCB)) pruneQC = qc @@ -570,7 +564,7 @@ func TestNewStateWithPersistence(t *testing.T) { t.Run("loads persisted commitQCs", func(t *testing.T) { dir := t.TempDir() - ds := utils.OrPanic1(data.NewState(&data.Config{Committee: committee}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + ds := utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) // Persist CommitQCs to disk. cp, _, err := persist.NewCommitQCPersister(utils.Some(dir)) @@ -579,7 +573,7 @@ func TestNewStateWithPersistence(t *testing.T) { qcs := make([]*types.CommitQC, 3) prev := utils.None[*types.CommitQC]() for i := range qcs { - qcs[i] = makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + qcs[i] = makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(qcs[i]) require.NoError(t, cp.MaybePruneAndPersist(utils.None[*types.CommitQC](), []*types.CommitQC{qcs[i]}, noCommitQCCB)) } @@ -595,7 +589,7 @@ func TestNewStateWithPersistence(t *testing.T) { t.Run("loads persisted commitQCs with AppQC", func(t *testing.T) { dir := t.TempDir() - ds := utils.OrPanic1(data.NewState(&data.Config{Committee: committee}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + ds := utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) // Persist AppQC at road index 1. roadIdx := types.RoadIndex(1) @@ -610,7 +604,7 @@ func TestNewStateWithPersistence(t *testing.T) { qcs := make([]*types.CommitQC, 5) prev := utils.None[*types.CommitQC]() for i := range qcs { - qcs[i] = makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + qcs[i] = makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(qcs[i]) require.NoError(t, cp.MaybePruneAndPersist(utils.None[*types.CommitQC](), []*types.CommitQC{qcs[i]}, noCommitQCCB)) } @@ -638,7 +632,7 @@ func TestNewStateWithPersistence(t *testing.T) { allQCs := make([]*types.CommitQC, 6) prev := utils.None[*types.CommitQC]() for i := range allQCs { - allQCs[i] = makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + allQCs[i] = makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(allQCs[i]) } @@ -668,13 +662,13 @@ func TestNewStateWithPersistence(t *testing.T) { t.Run("anchor past all persisted commitQCs truncates WAL", func(t *testing.T) { dir := t.TempDir() - ds := utils.OrPanic1(data.NewState(&data.Config{Committee: committee}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + ds := utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) // Build a chain of 10 CommitQCs (indices 0-9). qcs := make([]*types.CommitQC, 10) prev := utils.None[*types.CommitQC]() for i := range qcs { - qcs[i] = makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + qcs[i] = makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(qcs[i]) } @@ -711,7 +705,7 @@ func TestNewStateWithPersistence(t *testing.T) { t.Run("anchor past all persisted blocks truncates lane WAL", func(t *testing.T) { dir := t.TempDir() - ds := utils.OrPanic1(data.NewState(&data.Config{Committee: committee}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + ds := utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) lane := keys[0].Public() // Persist commitQCs 0-9 and blocks 0-2 for one lane. @@ -720,7 +714,7 @@ func TestNewStateWithPersistence(t *testing.T) { cp, _, err := persist.NewCommitQCPersister(utils.Some(dir)) require.NoError(t, err) for i := range qcs { - qcs[i] = makeCommitQC(committee, keys, prev, nil, utils.None[*types.AppQC]()) + qcs[i] = makeCommitQC(registry, keys, prev, nil, utils.None[*types.AppQC]()) prev = utils.Some(qcs[i]) require.NoError(t, cp.MaybePruneAndPersist(utils.None[*types.CommitQC](), []*types.CommitQC{qcs[i]}, noCommitQCCB)) } @@ -759,7 +753,7 @@ func TestNewStateWithPersistence(t *testing.T) { t.Run("corrupt AppQC data returns error", func(t *testing.T) { dir := t.TempDir() - ds := utils.OrPanic1(data.NewState(&data.Config{Committee: committee}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + ds := utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) // Create a throwaway persister to discover the A/B filenames, // then corrupt them so NewState fails on load. diff --git a/sei-tendermint/internal/autobahn/consensus/inner.go b/sei-tendermint/internal/autobahn/consensus/inner.go index 6cc197cbbd..1efbd85f25 100644 --- a/sei-tendermint/internal/autobahn/consensus/inner.go +++ b/sei-tendermint/internal/autobahn/consensus/inner.go @@ -81,6 +81,7 @@ import ( "fmt" "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/epoch" "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/seilog" @@ -98,7 +99,7 @@ type inner struct { // newInner creates the inner state from persisted data loaded by NewPersister. // data is None on fresh start (persistence disabled or no prior state). // Returns error if persisted state is corrupt (see persistedInner.validate). -func newInner(data utils.Option[*pb.PersistedInner], committee *types.Committee) (inner, error) { +func newInner(data utils.Option[*pb.PersistedInner], registry *epoch.Registry) (inner, error) { var persisted persistedInner if p, ok := data.Get(); ok { @@ -109,7 +110,7 @@ func newInner(data utils.Option[*pb.PersistedInner], committee *types.Committee) persisted = *decoded } - if err := persisted.validate(committee); err != nil { + if err := persisted.validate(registry); err != nil { return inner{}, err } @@ -122,7 +123,7 @@ func (s *State) pushCommitQC(qc *types.CommitQC) error { if i := s.innerRecv.Load(); qc.Proposal().Index() < i.View().Index { return nil } - if err := qc.Verify(s.Data().Committee()); err != nil { + if err := qc.Verify(s.Data().Registry().CommitteeFor(qc.Proposal().Index())); err != nil { return fmt.Errorf("qc.Verify(): %w", err) } for iSend := range s.inner.Lock() { @@ -151,7 +152,7 @@ func (s *State) pushTimeoutQC(ctx context.Context, qc *types.TimeoutQC) error { return nil } // Verify checks the invariant: TimeoutQC.View().Index == CommitQC.Index + 1 - if err := qc.Verify(s.Data().Committee(), i.CommitQC); err != nil { + if err := qc.Verify(s.Data().Registry().CommitteeFor(qc.View().Index), i.CommitQC); err != nil { return fmt.Errorf("qc.Verify(): %w", err) } for isend := range s.inner.Lock() { @@ -179,7 +180,7 @@ func (s *State) pushProposal(ctx context.Context, proposal *types.FullProposal) if vs.View() != proposal.View() { return nil } - if err := proposal.Verify(s.Data().Committee(), vs); err != nil { + if err := proposal.Verify(s.Data().Registry().CommitteeFor(vs.View().Index), vs, s.Data().Registry().GenesisTimestamp()); err != nil { return fmt.Errorf("proposal.Verify(): %w", err) } // Update. @@ -205,7 +206,7 @@ func (s *State) pushPrepareQC(ctx context.Context, qc *types.PrepareQC) error { if vs.View() != qc.Proposal().View() { return nil } - if err := qc.Verify(s.Data().Committee()); err != nil { + if err := qc.Verify(s.Data().Registry().CommitteeFor(qc.Proposal().View().Index)); err != nil { return fmt.Errorf("qc.Verify(): %w", err) } // Update. diff --git a/sei-tendermint/internal/autobahn/consensus/inner_test.go b/sei-tendermint/internal/autobahn/consensus/inner_test.go index 761e528d36..a4f118b555 100644 --- a/sei-tendermint/internal/autobahn/consensus/inner_test.go +++ b/sei-tendermint/internal/autobahn/consensus/inner_test.go @@ -9,6 +9,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" "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" "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" @@ -28,12 +29,12 @@ func seedPersistedInner(dir string, state *persistedInner) { // loadInner is a test helper that loads persisted data and creates inner. // Mirrors what NewState does: NewPersister → newInner. -func loadInner(dir string, committee *types.Committee) (inner, error) { +func loadInner(dir string, registry *epoch.Registry) (inner, error) { _, data, err := persist.NewPersister[*pb.PersistedInner](utils.Some(dir), innerFile) if err != nil { return inner{}, err } - return newInner(data, committee) + return newInner(data, registry) } // makePrepareQC creates a PrepareQC with valid signatures from the given keys. @@ -47,9 +48,9 @@ func makePrepareQC(keys []types.SecretKey, proposal *types.Proposal) *types.Prep func TestNewInnerEmpty(t *testing.T) { rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 1) + registry, _ := epoch.GenRegistry(rng, 1) // No data should return empty inner (persistence disabled / fresh start) - i, err := newInner(utils.None[*pb.PersistedInner](), committee) + i, err := newInner(utils.None[*pb.PersistedInner](), registry) require.NoError(t, err) require.False(t, i.PrepareVote.IsPresent(), "prepareVote should be None") require.False(t, i.CommitVote.IsPresent(), "commitVote should be None") @@ -61,7 +62,7 @@ func TestNewInnerPrepareVote(t *testing.T) { dir := t.TempDir() // Create and persist a prepare vote at genesis view (0, 0) - committee, keys := types.GenCommittee(rng, 1) + registry, keys := epoch.GenRegistry(rng, 1) key := keys[0] genesisProposal := types.GenProposalAt(rng, types.View{Index: 0, Number: 0}) vote := types.Sign(key, types.NewPrepareVote(genesisProposal)) @@ -71,7 +72,7 @@ func TestNewInnerPrepareVote(t *testing.T) { }) // Load and verify - i, err := loadInner(dir, committee) + i, err := loadInner(dir, registry) require.NoError(t, err) loaded, ok := i.PrepareVote.Get() require.True(t, ok, "prepareVote should be Some") @@ -83,7 +84,7 @@ func TestNewInnerCommitVote(t *testing.T) { dir := t.TempDir() // Create and persist a commit vote at genesis view (0, 0) - committee, keys := types.GenCommittee(rng, 1) + registry, keys := epoch.GenRegistry(rng, 1) key := keys[0] genesisProposal := types.GenProposalAt(rng, types.View{Index: 0, Number: 0}) prepareQC := makePrepareQC([]types.SecretKey{key}, genesisProposal) @@ -95,7 +96,7 @@ func TestNewInnerCommitVote(t *testing.T) { }) // Load and verify - i, err := loadInner(dir, committee) + i, err := loadInner(dir, registry) require.NoError(t, err) loaded, ok := i.CommitVote.Get() require.True(t, ok, "commitVote should be Some") @@ -107,7 +108,7 @@ func TestNewInnerTimeoutVote(t *testing.T) { dir := t.TempDir() // Create and persist a timeout vote at genesis view (0, 0) - committee, keys := types.GenCommittee(rng, 1) + registry, keys := epoch.GenRegistry(rng, 1) key := keys[0] vote := types.NewFullTimeoutVote(key, types.View{Index: 0, Number: 0}, utils.None[*types.PrepareQC]()) @@ -116,7 +117,7 @@ func TestNewInnerTimeoutVote(t *testing.T) { }) // Load and verify - i, err := loadInner(dir, committee) + i, err := loadInner(dir, registry) require.NoError(t, err) loaded, ok := i.TimeoutVote.Get() require.True(t, ok, "timeoutVote should be Some") @@ -128,7 +129,7 @@ func TestNewInnerAllVotes(t *testing.T) { dir := t.TempDir() // Create all vote types at genesis view (0, 0) - committee, keys := types.GenCommittee(rng, 1) + registry, keys := epoch.GenRegistry(rng, 1) key := keys[0] genesisProposal := types.GenProposalAt(rng, types.View{Index: 0, Number: 0}) prepareQC := makePrepareQC([]types.SecretKey{key}, genesisProposal) @@ -144,7 +145,7 @@ func TestNewInnerAllVotes(t *testing.T) { }) // Load and verify all - i, err := loadInner(dir, committee) + i, err := loadInner(dir, registry) require.NoError(t, err) require.True(t, i.PrepareVote.IsPresent(), "prepareVote should be Some") require.True(t, i.CommitVote.IsPresent(), "commitVote should be Some") @@ -156,7 +157,7 @@ func TestNewInnerPartialState(t *testing.T) { dir := t.TempDir() // Only persist prepareVote - committee, keys := types.GenCommittee(rng, 1) + registry, keys := epoch.GenRegistry(rng, 1) key := keys[0] genesisProposal := types.GenProposalAt(rng, types.View{Index: 0, Number: 0}) prepareVote := types.Sign(key, types.NewPrepareVote(genesisProposal)) @@ -166,7 +167,7 @@ func TestNewInnerPartialState(t *testing.T) { }) // Load - only prepareVote should be present - i, err := loadInner(dir, committee) + i, err := loadInner(dir, registry) require.NoError(t, err) require.True(t, i.PrepareVote.IsPresent(), "prepareVote should be Some") require.False(t, i.CommitVote.IsPresent(), "commitVote should be None") @@ -176,7 +177,7 @@ func TestNewInnerPartialState(t *testing.T) { func TestNewInnerCommitQC(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create a CommitQC at index 5 proposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) @@ -192,7 +193,7 @@ func TestNewInnerCommitQC(t *testing.T) { }) // Load and verify - i, err := loadInner(dir, committee) + i, err := loadInner(dir, registry) require.NoError(t, err) require.True(t, i.CommitQC.IsPresent(), "CommitQC should be loaded") loadedQC, ok := i.CommitQC.Get() @@ -205,7 +206,7 @@ func TestNewInnerCommitQC(t *testing.T) { func TestNewInnerTimeoutQC(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create a CommitQC at index 5 (required for TimeoutQC at index 6) qcProposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) @@ -229,7 +230,7 @@ func TestNewInnerTimeoutQC(t *testing.T) { }) // Load and verify - i, err := loadInner(dir, committee) + i, err := loadInner(dir, registry) require.NoError(t, err) require.True(t, i.TimeoutQC.IsPresent(), "TimeoutQC should be loaded") // View should be (6, 3) since TimeoutQC at (6, 2) advances to (6, 3) @@ -239,7 +240,7 @@ func TestNewInnerTimeoutQC(t *testing.T) { func TestNewInnerTimeoutQCOnlyGenesis(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create TimeoutQC at (0, 2) - no CommitQC needed for index 0 var timeoutVotes []*types.FullTimeoutVote @@ -253,7 +254,7 @@ func TestNewInnerTimeoutQCOnlyGenesis(t *testing.T) { }) // Load and verify - should work without CommitQC since index is 0 - i, err := loadInner(dir, committee) + i, err := loadInner(dir, registry) require.NoError(t, err) require.True(t, i.TimeoutQC.IsPresent(), "TimeoutQC should be loaded") require.Equal(t, types.View{Index: 0, Number: 3}, i.View()) @@ -262,7 +263,7 @@ func TestNewInnerTimeoutQCOnlyGenesis(t *testing.T) { func TestNewInnerTimeoutQCWithoutCommitQCError(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create TimeoutQC at index 6 WITHOUT CommitQC at index 5 var timeoutVotes []*types.FullTimeoutVote @@ -276,7 +277,7 @@ func TestNewInnerTimeoutQCWithoutCommitQCError(t *testing.T) { }) // Should return error - TimeoutQC at index 6 requires CommitQC at index 5 - _, err := loadInner(dir, committee) + _, err := loadInner(dir, registry) require.Error(t, err) require.Contains(t, err.Error(), "corrupt persisted state") } @@ -284,7 +285,7 @@ func TestNewInnerTimeoutQCWithoutCommitQCError(t *testing.T) { func TestNewInnerTimeoutQCAheadOfCommitQCError(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create CommitQC at index 5 qcProposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) @@ -308,7 +309,7 @@ func TestNewInnerTimeoutQCAheadOfCommitQCError(t *testing.T) { }) // Should return error - TimeoutQC index must equal CommitQC.Index + 1 - _, err := loadInner(dir, committee) + _, err := loadInner(dir, registry) require.Error(t, err) require.Contains(t, err.Error(), "corrupt persisted state") } @@ -316,7 +317,7 @@ func TestNewInnerTimeoutQCAheadOfCommitQCError(t *testing.T) { func TestNewInnerViewSpecStaleTimeoutQC(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create CommitQC at index 10 qcProposal := types.GenProposalAt(rng, types.View{Index: 10, Number: 0}) @@ -341,7 +342,7 @@ func TestNewInnerViewSpecStaleTimeoutQC(t *testing.T) { }) // Load - stale TimeoutQC should be treated as corrupt state - _, err := loadInner(dir, committee) + _, err := loadInner(dir, registry) require.Error(t, err) require.Contains(t, err.Error(), "corrupt persisted state") } @@ -349,7 +350,7 @@ func TestNewInnerViewSpecStaleTimeoutQC(t *testing.T) { func TestNewInnerViewSpecValidBothQCs(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create CommitQC at index 5 qcProposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) @@ -373,7 +374,7 @@ func TestNewInnerViewSpecValidBothQCs(t *testing.T) { }) // Load - both should be present - i, err := loadInner(dir, committee) + i, err := loadInner(dir, registry) require.NoError(t, err) require.True(t, i.CommitQC.IsPresent(), "CommitQC should be loaded") require.True(t, i.TimeoutQC.IsPresent(), "TimeoutQC should be loaded") @@ -384,7 +385,7 @@ func TestNewInnerViewSpecValidBothQCs(t *testing.T) { func TestNewInnerStaleVoteError(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create CommitQC at index 5 -> current view is (6, 0) qcProposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) @@ -405,7 +406,7 @@ func TestNewInnerStaleVoteError(t *testing.T) { PrepareVote: utils.Some(staleVote), }) - _, err := loadInner(dir, committee) + _, err := loadInner(dir, registry) require.Error(t, err) require.Contains(t, err.Error(), "corrupt persisted state") } @@ -413,7 +414,7 @@ func TestNewInnerStaleVoteError(t *testing.T) { func TestNewInnerFuturePrepareVoteError(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create CommitQC at index 5 -> current view is (6, 0) qcProposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) @@ -434,7 +435,7 @@ func TestNewInnerFuturePrepareVoteError(t *testing.T) { }) // Should return error - future votes indicate corrupt state - _, err := loadInner(dir, committee) + _, err := loadInner(dir, registry) require.Error(t, err) require.Contains(t, err.Error(), "corrupt persisted state") } @@ -442,7 +443,7 @@ func TestNewInnerFuturePrepareVoteError(t *testing.T) { func TestNewInnerFutureCommitVoteError(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create CommitQC at index 5 -> current view is (6, 0) qcProposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) @@ -463,7 +464,7 @@ func TestNewInnerFutureCommitVoteError(t *testing.T) { }) // Should return error - _, err := loadInner(dir, committee) + _, err := loadInner(dir, registry) require.Error(t, err) require.Contains(t, err.Error(), "corrupt persisted state") } @@ -471,7 +472,7 @@ func TestNewInnerFutureCommitVoteError(t *testing.T) { func TestNewInnerFutureTimeoutVoteError(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create CommitQC at index 5 -> current view is (6, 0) qcProposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) @@ -491,7 +492,7 @@ func TestNewInnerFutureTimeoutVoteError(t *testing.T) { }) // Should return error - _, err := loadInner(dir, committee) + _, err := loadInner(dir, registry) require.Error(t, err) require.Contains(t, err.Error(), "corrupt persisted state") } @@ -499,7 +500,7 @@ func TestNewInnerFutureTimeoutVoteError(t *testing.T) { func TestNewInnerCurrentViewVoteOk(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create CommitQC at index 5 -> current view is (6, 0) qcProposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) @@ -520,7 +521,7 @@ func TestNewInnerCurrentViewVoteOk(t *testing.T) { }) // Should succeed - current view votes are valid - i, err := loadInner(dir, committee) + i, err := loadInner(dir, registry) require.NoError(t, err) require.True(t, i.PrepareVote.IsPresent(), "current view vote should be loaded") } @@ -528,7 +529,7 @@ func TestNewInnerCurrentViewVoteOk(t *testing.T) { func TestNewInnerCommitQCInvalidSignatureError(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, _ := types.GenCommittee(rng, 3) + registry, _ := epoch.GenRegistry(rng, 3) // Create CommitQC signed by keys NOT in committee otherKeys := make([]types.SecretKey, 3) @@ -548,7 +549,7 @@ func TestNewInnerCommitQCInvalidSignatureError(t *testing.T) { }) // Should return error - invalid signatures - _, err := loadInner(dir, committee) + _, err := loadInner(dir, registry) require.Error(t, err) require.Contains(t, err.Error(), "corrupt persisted state") } @@ -556,7 +557,7 @@ func TestNewInnerCommitQCInvalidSignatureError(t *testing.T) { func TestNewInnerTimeoutQCInvalidSignatureError(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create valid CommitQC at index 5 qcProposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) @@ -584,7 +585,7 @@ func TestNewInnerTimeoutQCInvalidSignatureError(t *testing.T) { }) // Should return error - invalid signatures on TimeoutQC - _, err := loadInner(dir, committee) + _, err := loadInner(dir, registry) require.Error(t, err) require.Contains(t, err.Error(), "corrupt persisted state") } @@ -592,7 +593,7 @@ func TestNewInnerTimeoutQCInvalidSignatureError(t *testing.T) { func TestNewInnerCurrentViewVoteInvalidSignatureError(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create valid CommitQC at index 5 -> current view is (6, 0) qcProposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) @@ -614,7 +615,7 @@ func TestNewInnerCurrentViewVoteInvalidSignatureError(t *testing.T) { }) // Should return error - current view votes must have valid signatures - _, err := loadInner(dir, committee) + _, err := loadInner(dir, registry) require.Error(t, err) require.Contains(t, err.Error(), "corrupt persisted state") } @@ -622,7 +623,7 @@ func TestNewInnerCurrentViewVoteInvalidSignatureError(t *testing.T) { func TestNewInnerStaleVoteInvalidSignatureError(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create valid CommitQC at index 5 -> current view is (6, 0) qcProposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) @@ -644,7 +645,7 @@ func TestNewInnerStaleVoteInvalidSignatureError(t *testing.T) { PrepareVote: utils.Some(badVote), }) - _, err := loadInner(dir, committee) + _, err := loadInner(dir, registry) require.Error(t, err) require.Contains(t, err.Error(), "corrupt persisted state") } @@ -652,7 +653,7 @@ func TestNewInnerStaleVoteInvalidSignatureError(t *testing.T) { func TestNewInnerPrepareQC(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create prepareQC at genesis view (0, 0) proposal := types.GenProposalAt(rng, types.View{Index: 0, Number: 0}) @@ -663,7 +664,7 @@ func TestNewInnerPrepareQC(t *testing.T) { }) // Load and verify - i, err := loadInner(dir, committee) + i, err := loadInner(dir, registry) require.NoError(t, err) require.True(t, i.PrepareQC.IsPresent(), "prepareQC should be loaded") } @@ -671,7 +672,7 @@ func TestNewInnerPrepareQC(t *testing.T) { func TestNewInnerStalePrepareQCError(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create CommitQC at index 5 -> current view is (6, 0) qcProposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) @@ -692,7 +693,7 @@ func TestNewInnerStalePrepareQCError(t *testing.T) { PrepareQC: utils.Some(stalePrepareQC), }) - _, err := loadInner(dir, committee) + _, err := loadInner(dir, registry) require.Error(t, err) require.Contains(t, err.Error(), "corrupt persisted state") } @@ -700,7 +701,7 @@ func TestNewInnerStalePrepareQCError(t *testing.T) { func TestNewInnerCommitVoteWithoutPrepareQCError(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Current view is (0, 0) (no CommitQC or TimeoutQC). // CommitVote requires PrepareQC justification. @@ -711,7 +712,7 @@ func TestNewInnerCommitVoteWithoutPrepareQCError(t *testing.T) { CommitVote: utils.Some(commitVote), }) - _, err := loadInner(dir, committee) + _, err := loadInner(dir, registry) require.Error(t, err) require.Contains(t, err.Error(), "CommitVote present without PrepareQC") } @@ -719,7 +720,7 @@ func TestNewInnerCommitVoteWithoutPrepareQCError(t *testing.T) { func TestNewInnerFuturePrepareQCError(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create CommitQC at index 5 -> current view is (6, 0) qcProposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) @@ -740,7 +741,7 @@ func TestNewInnerFuturePrepareQCError(t *testing.T) { }) // Should return error - future prepareQC indicates corrupt state - _, err := loadInner(dir, committee) + _, err := loadInner(dir, registry) require.Error(t, err) require.Contains(t, err.Error(), "corrupt persisted state") } @@ -748,7 +749,7 @@ func TestNewInnerFuturePrepareQCError(t *testing.T) { func TestNewInnerCurrentViewPrepareQCOk(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create CommitQC at index 5 -> current view is (6, 0) qcProposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) @@ -769,7 +770,7 @@ func TestNewInnerCurrentViewPrepareQCOk(t *testing.T) { }) // Should succeed - current view prepareQC is valid - i, err := loadInner(dir, committee) + i, err := loadInner(dir, registry) require.NoError(t, err) require.True(t, i.PrepareQC.IsPresent(), "current view prepareQC should be loaded") } @@ -777,7 +778,7 @@ func TestNewInnerCurrentViewPrepareQCOk(t *testing.T) { func TestNewInnerCurrentViewPrepareQCInvalidSignatureError(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Create CommitQC at index 5 -> current view is (6, 0) qcProposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) @@ -802,7 +803,7 @@ func TestNewInnerCurrentViewPrepareQCInvalidSignatureError(t *testing.T) { }) // Should return error - current view prepareQC has invalid signatures - _, err := loadInner(dir, committee) + _, err := loadInner(dir, registry) require.Error(t, err) require.Contains(t, err.Error(), "corrupt persisted state") } @@ -810,7 +811,8 @@ func TestNewInnerCurrentViewPrepareQCInvalidSignatureError(t *testing.T) { func TestNewInnerPrepareQCIncludedInTimeoutVote(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) + committee := registry.LatestCommittee() voteKey := keys[0] // Create CommitQC at index 5 -> current view is (6, 0) @@ -832,7 +834,7 @@ func TestNewInnerPrepareQCIncludedInTimeoutVote(t *testing.T) { }) // Load state - i, err := loadInner(dir, committee) + i, err := loadInner(dir, registry) require.NoError(t, err) require.True(t, i.PrepareQC.IsPresent(), "prepareQC should be loaded") @@ -855,7 +857,7 @@ func TestNewInnerPrepareQCIncludedInTimeoutVote(t *testing.T) { func TestPushTimeoutQCClearsStaleState(t *testing.T) { rng := utils.TestRng() dir := t.TempDir() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) // Setup: Create CommitQC at index 5 -> current view is (6, 0) qcProposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) @@ -884,7 +886,7 @@ func TestPushTimeoutQCClearsStaleState(t *testing.T) { }) // Load initial state and verify everything is present - i, err := loadInner(dir, committee) + i, err := loadInner(dir, registry) require.NoError(t, err) require.True(t, i.PrepareQC.IsPresent(), "prepareQC should be loaded") require.True(t, i.PrepareVote.IsPresent(), "prepareVote should be loaded") @@ -924,8 +926,8 @@ func TestRunOutputsPersistErrorPropagates(t *testing.T) { // Verify that a persist error in runOutputs propagates // and terminates the consensus component (instead of panicking). rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) - ds := utils.OrPanic1(data.NewState(&data.Config{Committee: committee}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) + registry, keys := epoch.GenRegistry(rng, 4) + ds := utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) wantErr := errors.New("disk on fire") pers := utils.Some[persist.Persister[*pb.PersistedInner]](failPersister[*pb.PersistedInner]{err: wantErr}) diff --git a/sei-tendermint/internal/autobahn/consensus/persist/commitqcs_test.go b/sei-tendermint/internal/autobahn/consensus/persist/commitqcs_test.go index 1bbcadeb4a..3920049cfa 100644 --- a/sei-tendermint/internal/autobahn/consensus/persist/commitqcs_test.go +++ b/sei-tendermint/internal/autobahn/consensus/persist/commitqcs_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" + "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" ) @@ -34,6 +35,8 @@ func testCommitQC( leaderKey, committee, vs, + 0, + time.Time{}, time.Now(), laneQCs, appQC, @@ -106,7 +109,8 @@ func TestNewCommitQCPersisterEmptyDir(t *testing.T) { func TestPersistCommitQCAndLoad(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) + committee := registry.LatestCommittee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 3) @@ -134,7 +138,8 @@ func TestPersistCommitQCAndLoad(t *testing.T) { func TestCommitQCDeleteBeforeRemovesOldKeepsNew(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) + committee := registry.LatestCommittee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 5) @@ -156,7 +161,8 @@ func TestCommitQCDeleteBeforeRemovesOldKeepsNew(t *testing.T) { func TestCommitQCDeleteBeforeZero(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) + committee := registry.LatestCommittee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 3) @@ -183,7 +189,8 @@ func TestCommitQCDeleteBeforeZero(t *testing.T) { func TestCommitQCPersistDuplicateIsNoOp(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) + committee := registry.LatestCommittee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 3) @@ -200,7 +207,8 @@ func TestCommitQCPersistDuplicateIsNoOp(t *testing.T) { func TestCommitQCPersistGapRejected(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) + committee := registry.LatestCommittee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 5) @@ -218,7 +226,8 @@ func TestCommitQCPersistGapRejected(t *testing.T) { func TestLoadAllDetectsCommitQCGap(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) + committee := registry.LatestCommittee() dir := t.TempDir() // Build 3 sequential CommitQCs (indices 0, 1, 2). @@ -241,7 +250,8 @@ func TestLoadAllDetectsCommitQCGap(t *testing.T) { func TestNoOpCommitQCPersister(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) + committee := registry.LatestCommittee() qcs := makeSequentialCommitQCs(committee, keys, 11) // Fresh no-op persister: prune with anchor at index 0 (idx==0, @@ -274,7 +284,8 @@ func TestNoOpCommitQCPersister(t *testing.T) { func TestCommitQCDeleteBeforePastAll(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) + committee := registry.LatestCommittee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 12) @@ -305,7 +316,8 @@ func TestCommitQCDeleteBeforePastAll(t *testing.T) { // must re-establish the cursor so subsequent persists succeed. func TestCommitQCDeleteBeforePastAllCrashRecovery(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) + committee := registry.LatestCommittee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 12) @@ -348,7 +360,8 @@ func TestCommitQCDeleteBeforePastAllCrashRecovery(t *testing.T) { // re-establishes the cursor for subsequent writes. func TestCommitQCDeleteBeforeWithAnchorRecovers(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) + committee := registry.LatestCommittee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 5) @@ -386,7 +399,8 @@ func TestCommitQCDeleteBeforeWithAnchorRecovers(t *testing.T) { func TestCommitQCDeleteBeforeThenPersistMore(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) + committee := registry.LatestCommittee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 6) @@ -411,7 +425,8 @@ func TestCommitQCDeleteBeforeThenPersistMore(t *testing.T) { func TestCommitQCDeleteBeforeAlreadyPruned(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) + committee := registry.LatestCommittee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 5) @@ -439,7 +454,8 @@ func TestCommitQCDeleteBeforeAlreadyPruned(t *testing.T) { func TestCommitQCProgressiveDeleteBefore(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) + committee := registry.LatestCommittee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 8) diff --git a/sei-tendermint/internal/autobahn/consensus/persist/fullcommitqcs.go b/sei-tendermint/internal/autobahn/consensus/persist/fullcommitqcs.go index f4ff1df34e..9c383760ae 100644 --- a/sei-tendermint/internal/autobahn/consensus/persist/fullcommitqcs.go +++ b/sei-tendermint/internal/autobahn/consensus/persist/fullcommitqcs.go @@ -12,14 +12,14 @@ const fullCommitQCsDir = "fullcommitqcs" // fullCommitQCState is the mutable state protected by FullCommitQCPersister's mutex. type fullCommitQCState struct { - iw utils.Option[*indexedWAL[*types.FullCommitQC]] - committee *types.Committee - next types.GlobalBlockNumber // next expected GlobalRange().First == last QC's GlobalRange().Next - loaded []*types.FullCommitQC + iw utils.Option[*indexedWAL[*types.FullCommitQC]] + firstBlock types.GlobalBlockNumber + next types.GlobalBlockNumber // next expected GlobalRange().First == last QC's GlobalRange().Next + loaded []*types.FullCommitQC } func (s *fullCommitQCState) persistQC(qc *types.FullCommitQC) error { - gr := qc.QC().GlobalRange(s.committee) + gr := qc.QC().GlobalRange() if gr.First < s.next { return nil } @@ -51,7 +51,7 @@ func (s *fullCommitQCState) truncateBefore(n types.GlobalBlockNumber) error { // per prune call because pruning advances one block at a time while // each QC covers many blocks. if err := iw.TruncateWhile(func(entry *types.FullCommitQC) bool { - return entry.QC().GlobalRange(s.committee).Next <= n + return entry.QC().GlobalRange().Next <= n }); err != nil { return fmt.Errorf("truncate full commitqc WAL: %w", err) } @@ -69,10 +69,10 @@ type FullCommitQCPersister struct { // NewFullCommitQCPersister opens (or creates) a WAL in the fullcommitqcs/ // subdir and replays all persisted entries. Loaded QCs are available via // ConsumeLoaded. When stateDir is None, returns a no-op persister. -func NewFullCommitQCPersister(stateDir utils.Option[string], committee *types.Committee) (*FullCommitQCPersister, error) { +func NewFullCommitQCPersister(stateDir utils.Option[string], firstBlock types.GlobalBlockNumber) (*FullCommitQCPersister, error) { sd, ok := stateDir.Get() if !ok { - return &FullCommitQCPersister{state: utils.NewMutex(&fullCommitQCState{committee: committee, next: committee.FirstBlock()})}, nil + return &FullCommitQCPersister{state: utils.NewMutex(&fullCommitQCState{firstBlock: firstBlock, next: firstBlock})}, nil } dir := filepath.Join(sd, fullCommitQCsDir) iw, err := openIndexedWAL(dir, types.FullCommitQCConv) @@ -80,14 +80,14 @@ func NewFullCommitQCPersister(stateDir utils.Option[string], committee *types.Co return nil, fmt.Errorf("open full commitqc WAL in %s: %w", dir, err) } - s := &fullCommitQCState{iw: utils.Some(iw), committee: committee, next: committee.FirstBlock()} + s := &fullCommitQCState{iw: utils.Some(iw), firstBlock: firstBlock, next: firstBlock} loaded, err := s.loadAll() if err != nil { _ = iw.Close() return nil, err } if len(loaded) > 0 { - s.next = loaded[len(loaded)-1].QC().GlobalRange(s.committee).Next + s.next = loaded[len(loaded)-1].QC().GlobalRange().Next } s.loaded = loaded return &FullCommitQCPersister{ @@ -105,13 +105,13 @@ func (gp *FullCommitQCPersister) Next() types.GlobalBlockNumber { } // LoadedFirst returns the first global block number of the first loaded QC, -// or committee.FirstBlock() if empty. +// or firstBlock if empty. func (gp *FullCommitQCPersister) LoadedFirst() types.GlobalBlockNumber { for s := range gp.state.Lock() { if len(s.loaded) > 0 { - return s.loaded[0].QC().GlobalRange(s.committee).First + return s.loaded[0].QC().GlobalRange().First } - return s.committee.FirstBlock() + return s.firstBlock } panic("unreachable") } diff --git a/sei-tendermint/internal/autobahn/consensus/persist/fullcommitqcs_test.go b/sei-tendermint/internal/autobahn/consensus/persist/fullcommitqcs_test.go index 8932bcab76..fd7212ee25 100644 --- a/sei-tendermint/internal/autobahn/consensus/persist/fullcommitqcs_test.go +++ b/sei-tendermint/internal/autobahn/consensus/persist/fullcommitqcs_test.go @@ -5,96 +5,53 @@ import ( "time" "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" + "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 makeSequentialFullCommitQCs( rng utils.Rng, - committee *types.Committee, + registry *epoch.Registry, keys []types.SecretKey, n int, ) []*types.FullCommitQC { qcs := make([]*types.FullCommitQC, n) prev := utils.None[*types.CommitQC]() for i := range n { - blocks := map[types.LaneID][]*types.Block{} - for range 3 { - lane := committee.Lanes().At(rng.Intn(committee.Lanes().Len())) - var b *types.Block - if bs := blocks[lane]; len(bs) > 0 { - parent := bs[len(bs)-1] - b = types.NewBlock(lane, parent.Header().Next(), parent.Header().Hash(), types.GenPayload(rng)) - } else { - b = types.NewBlock( - lane, - types.LaneRangeOpt(prev, lane).Next(), - types.GenBlockHeaderHash(rng), - types.GenPayload(rng), - ) - } - blocks[lane] = append(blocks[lane], b) - } - laneQCs := map[types.LaneID]*types.LaneQC{} - var headers []*types.BlockHeader - for lane := range committee.Lanes().All() { - if bs := blocks[lane]; len(bs) > 0 { - votes := make([]*types.Signed[*types.LaneVote], 0, len(keys)) - for _, k := range keys { - votes = append(votes, types.Sign(k, types.NewLaneVote(bs[len(bs)-1].Header()))) - } - laneQCs[lane] = types.NewLaneQC(votes) - for _, b := range bs { - headers = append(headers, b.Header()) - } - } - } - viewSpec := types.ViewSpec{CommitQC: prev} - leader := committee.Leader(viewSpec.View()) - var leaderKey types.SecretKey + vs := types.ViewSpec{CommitQC: prev, FirstBlock: registry.FirstBlock()} + committee := registry.CommitteeFor(vs.View().Index) + lane := committee.Lanes().At(rng.Intn(committee.Lanes().Len())) + b := types.NewBlock(lane, types.LaneRangeOpt(prev, lane).Next(), types.GenBlockHeaderHash(rng), types.GenPayload(rng)) + lv := types.NewLaneVote(b.Header()) + lvotes := make([]*types.Signed[*types.LaneVote], 0, len(keys)) for _, k := range keys { - if k.Public() == leader { - leaderKey = k - break - } + lvotes = append(lvotes, types.Sign(k, lv)) } - proposal := utils.OrPanic1(types.NewProposal( - leaderKey, - committee, - viewSpec, - time.Now(), - laneQCs, - utils.None[*types.AppQC](), - )) - votes := make([]*types.Signed[*types.CommitVote], 0, len(keys)) - for _, k := range keys { - votes = append(votes, types.Sign(k, types.NewCommitVote(proposal.Proposal().Msg()))) - } - cqc := types.NewCommitQC(votes) - qcs[i] = types.NewFullCommitQC(cqc, headers) - prev = utils.Some(cqc) + laneQCs := map[types.LaneID]*types.LaneQC{lane: types.NewLaneQC(lvotes)} + cqc := types.BuildCommitQC(committee, keys, prev, registry.FirstBlock(), time.Time{}, laneQCs, utils.None[*types.AppQC]()) + qcs[i] = types.NewFullCommitQC(cqc, []*types.BlockHeader{b.Header()}) + prev = utils.Some(qcs[i].QC()) } return qcs } func TestNewFullCommitQCPersisterEmptyDir(t *testing.T) { - rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 3) dir := t.TempDir() - gp, err := NewFullCommitQCPersister(utils.Some(dir), committee) + gp, err := NewFullCommitQCPersister(utils.Some(dir), 0) require.NoError(t, err) require.NotNil(t, gp) require.Equal(t, 0, len(gp.ConsumeLoaded())) - require.Equal(t, committee.FirstBlock(), gp.Next()) + require.Equal(t, types.GlobalBlockNumber(0), gp.Next()) require.NoError(t, gp.Close()) } func TestNewFullCommitQCPersisterNoop(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) - qcs := makeSequentialFullCommitQCs(rng, committee, keys, 5) + registry, keys := epoch.GenRegistry(rng, 3) + qcs := makeSequentialFullCommitQCs(rng, registry, keys, 5) - gp, err := NewFullCommitQCPersister(utils.None[string](), committee) + gp, err := NewFullCommitQCPersister(utils.None[string](), registry.FirstBlock()) require.NoError(t, err) require.NotNil(t, gp) require.Equal(t, 0, len(gp.ConsumeLoaded())) @@ -102,7 +59,7 @@ func TestNewFullCommitQCPersisterNoop(t *testing.T) { for _, qc := range qcs { require.NoError(t, gp.PersistQC(qc)) } - lastNext := qcs[len(qcs)-1].QC().GlobalRange(committee).Next + lastNext := qcs[len(qcs)-1].QC().GlobalRange().Next require.Equal(t, lastNext, gp.Next()) // Truncate past everything in no-op mode advances cursor. @@ -115,24 +72,24 @@ func TestNewFullCommitQCPersisterNoop(t *testing.T) { func TestFullCommitQCPersistAndReload(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) - qcs := makeSequentialFullCommitQCs(rng, committee, keys, 5) + registry, keys := epoch.GenRegistry(rng, 3) + qcs := makeSequentialFullCommitQCs(rng, registry, keys, 5) - gp, err := NewFullCommitQCPersister(utils.Some(dir), committee) + gp, err := NewFullCommitQCPersister(utils.Some(dir), registry.FirstBlock()) require.NoError(t, err) for _, qc := range qcs { require.NoError(t, gp.PersistQC(qc)) } - lastNext := qcs[len(qcs)-1].QC().GlobalRange(committee).Next + lastNext := qcs[len(qcs)-1].QC().GlobalRange().Next require.Equal(t, lastNext, gp.Next()) require.NoError(t, gp.Close()) - gp2, err := NewFullCommitQCPersister(utils.Some(dir), committee) + gp2, err := NewFullCommitQCPersister(utils.Some(dir), registry.FirstBlock()) require.NoError(t, err) loaded := gp2.ConsumeLoaded() require.Equal(t, len(qcs), len(loaded)) for i, lqc := range loaded { - require.Equal(t, qcs[i].QC().GlobalRange(committee).First, lqc.QC().GlobalRange(committee).First) + require.Equal(t, qcs[i].QC().GlobalRange().First, lqc.QC().GlobalRange().First) } require.Equal(t, lastNext, gp2.Next()) require.NoError(t, gp2.Close()) @@ -141,46 +98,46 @@ func TestFullCommitQCPersistAndReload(t *testing.T) { func TestFullCommitQCTruncateAndReload(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) - qcs := makeSequentialFullCommitQCs(rng, committee, keys, 5) + registry, keys := epoch.GenRegistry(rng, 3) + qcs := makeSequentialFullCommitQCs(rng, registry, keys, 5) - gp, err := NewFullCommitQCPersister(utils.Some(dir), committee) + gp, err := NewFullCommitQCPersister(utils.Some(dir), registry.FirstBlock()) require.NoError(t, err) for _, qc := range qcs { require.NoError(t, gp.PersistQC(qc)) } // Truncate before the third QC's range start, which should remove // all QCs whose range is fully below that point. - truncPoint := qcs[2].QC().GlobalRange(committee).First + truncPoint := qcs[2].QC().GlobalRange().First require.NoError(t, gp.TruncateBefore(truncPoint)) require.NoError(t, gp.Close()) - gp2, err := NewFullCommitQCPersister(utils.Some(dir), committee) + gp2, err := NewFullCommitQCPersister(utils.Some(dir), registry.FirstBlock()) require.NoError(t, err) loaded := gp2.ConsumeLoaded() // QCs 0 and 1 should be gone (their ranges are fully before truncPoint). // QC 2 should be the first one remaining. require.GreaterOrEqual(t, len(loaded), 1) - require.Equal(t, qcs[2].QC().GlobalRange(committee).First, loaded[0].QC().GlobalRange(committee).First) + require.Equal(t, qcs[2].QC().GlobalRange().First, loaded[0].QC().GlobalRange().First) require.NoError(t, gp2.Close()) } func TestFullCommitQCTruncateAll(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) - qcs := makeSequentialFullCommitQCs(rng, committee, keys, 3) + registry, keys := epoch.GenRegistry(rng, 3) + qcs := makeSequentialFullCommitQCs(rng, registry, keys, 3) - gp, err := NewFullCommitQCPersister(utils.Some(dir), committee) + gp, err := NewFullCommitQCPersister(utils.Some(dir), registry.FirstBlock()) require.NoError(t, err) for _, qc := range qcs { require.NoError(t, gp.PersistQC(qc)) } - lastNext := qcs[len(qcs)-1].QC().GlobalRange(committee).Next + lastNext := qcs[len(qcs)-1].QC().GlobalRange().Next require.NoError(t, gp.TruncateBefore(lastNext+100)) require.NoError(t, gp.Close()) - gp2, err := NewFullCommitQCPersister(utils.Some(dir), committee) + gp2, err := NewFullCommitQCPersister(utils.Some(dir), registry.FirstBlock()) require.NoError(t, err) require.Equal(t, 0, len(gp2.ConsumeLoaded())) require.NoError(t, gp2.Close()) @@ -189,24 +146,24 @@ func TestFullCommitQCTruncateAll(t *testing.T) { func TestFullCommitQCDuplicateIgnored(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) - qcs := makeSequentialFullCommitQCs(rng, committee, keys, 2) + registry, keys := epoch.GenRegistry(rng, 3) + qcs := makeSequentialFullCommitQCs(rng, registry, keys, 2) - gp, err := NewFullCommitQCPersister(utils.Some(dir), committee) + gp, err := NewFullCommitQCPersister(utils.Some(dir), registry.FirstBlock()) require.NoError(t, err) require.NoError(t, gp.PersistQC(qcs[0])) require.NoError(t, gp.PersistQC(qcs[0])) // duplicate - require.Equal(t, qcs[0].QC().GlobalRange(committee).Next, gp.Next()) + require.Equal(t, qcs[0].QC().GlobalRange().Next, gp.Next()) require.NoError(t, gp.Close()) } func TestFullCommitQCGapError(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) - qcs := makeSequentialFullCommitQCs(rng, committee, keys, 3) + registry, keys := epoch.GenRegistry(rng, 3) + qcs := makeSequentialFullCommitQCs(rng, registry, keys, 3) - gp, err := NewFullCommitQCPersister(utils.Some(dir), committee) + gp, err := NewFullCommitQCPersister(utils.Some(dir), registry.FirstBlock()) require.NoError(t, err) // Skip qcs[0] and try to persist qcs[1] directly. err = gp.PersistQC(qcs[1]) @@ -218,10 +175,10 @@ func TestFullCommitQCGapError(t *testing.T) { func TestFullCommitQCTruncateBeforeNoop(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) - qcs := makeSequentialFullCommitQCs(rng, committee, keys, 3) + registry, keys := epoch.GenRegistry(rng, 3) + qcs := makeSequentialFullCommitQCs(rng, registry, keys, 3) - gp, err := NewFullCommitQCPersister(utils.Some(dir), committee) + gp, err := NewFullCommitQCPersister(utils.Some(dir), registry.FirstBlock()) require.NoError(t, err) for _, qc := range qcs { require.NoError(t, gp.PersistQC(qc)) @@ -230,7 +187,7 @@ func TestFullCommitQCTruncateBeforeNoop(t *testing.T) { require.NoError(t, gp.TruncateBefore(0)) require.NoError(t, gp.Close()) - gp2, err := NewFullCommitQCPersister(utils.Some(dir), committee) + gp2, err := NewFullCommitQCPersister(utils.Some(dir), registry.FirstBlock()) require.NoError(t, err) require.Equal(t, 3, len(gp2.ConsumeLoaded())) require.NoError(t, gp2.Close()) @@ -239,26 +196,26 @@ func TestFullCommitQCTruncateBeforeNoop(t *testing.T) { func TestFullCommitQCContinueAfterReload(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) - qcs := makeSequentialFullCommitQCs(rng, committee, keys, 6) + registry, keys := epoch.GenRegistry(rng, 3) + qcs := makeSequentialFullCommitQCs(rng, registry, keys, 6) - gp, err := NewFullCommitQCPersister(utils.Some(dir), committee) + gp, err := NewFullCommitQCPersister(utils.Some(dir), registry.FirstBlock()) require.NoError(t, err) for _, qc := range qcs[:3] { require.NoError(t, gp.PersistQC(qc)) } require.NoError(t, gp.Close()) - gp2, err := NewFullCommitQCPersister(utils.Some(dir), committee) + gp2, err := NewFullCommitQCPersister(utils.Some(dir), registry.FirstBlock()) require.NoError(t, err) require.Equal(t, 3, len(gp2.ConsumeLoaded())) for _, qc := range qcs[3:] { require.NoError(t, gp2.PersistQC(qc)) } - require.Equal(t, qcs[5].QC().GlobalRange(committee).Next, gp2.Next()) + require.Equal(t, qcs[5].QC().GlobalRange().Next, gp2.Next()) require.NoError(t, gp2.Close()) - gp3, err := NewFullCommitQCPersister(utils.Some(dir), committee) + gp3, err := NewFullCommitQCPersister(utils.Some(dir), registry.FirstBlock()) require.NoError(t, err) require.Equal(t, 6, len(gp3.ConsumeLoaded())) require.NoError(t, gp3.Close()) @@ -267,23 +224,23 @@ func TestFullCommitQCContinueAfterReload(t *testing.T) { func TestFullCommitQCTruncateMidRange(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) - qcs := makeSequentialFullCommitQCs(rng, committee, keys, 5) + registry, keys := epoch.GenRegistry(rng, 3) + qcs := makeSequentialFullCommitQCs(rng, registry, keys, 5) - gp, err := NewFullCommitQCPersister(utils.Some(dir), committee) + gp, err := NewFullCommitQCPersister(utils.Some(dir), registry.FirstBlock()) require.NoError(t, err) for _, qc := range qcs { require.NoError(t, gp.PersistQC(qc)) } // Truncate at a point inside the first QC's range. // The first QC should be kept because its range extends past truncPoint. - gr0 := qcs[0].QC().GlobalRange(committee) + gr0 := qcs[0].QC().GlobalRange() if gr0.Len() > 1 { midPoint := gr0.First + types.GlobalBlockNumber(gr0.Len()/2) require.NoError(t, gp.TruncateBefore(midPoint)) require.NoError(t, gp.Close()) - gp2, err := NewFullCommitQCPersister(utils.Some(dir), committee) + gp2, err := NewFullCommitQCPersister(utils.Some(dir), registry.FirstBlock()) require.NoError(t, err) require.Equal(t, 5, len(gp2.ConsumeLoaded())) require.NoError(t, gp2.Close()) diff --git a/sei-tendermint/internal/autobahn/consensus/persist/globalblocks.go b/sei-tendermint/internal/autobahn/consensus/persist/globalblocks.go index 9471994e3c..86e9e9b918 100644 --- a/sei-tendermint/internal/autobahn/consensus/persist/globalblocks.go +++ b/sei-tendermint/internal/autobahn/consensus/persist/globalblocks.go @@ -45,10 +45,10 @@ func (loadedGlobalBlockCodec) Unmarshal(raw []byte) (LoadedGlobalBlock, error) { // globalBlockState is the mutable state protected by GlobalBlockPersister's mutex. type globalBlockState struct { - iw utils.Option[*indexedWAL[LoadedGlobalBlock]] - committee *types.Committee - next types.GlobalBlockNumber - loaded []LoadedGlobalBlock + iw utils.Option[*indexedWAL[LoadedGlobalBlock]] + firstBlock types.GlobalBlockNumber + next types.GlobalBlockNumber + loaded []LoadedGlobalBlock } func (s *globalBlockState) persistBlock(n types.GlobalBlockNumber, block *types.Block) error { @@ -105,10 +105,10 @@ type GlobalBlockPersister struct { // NewGlobalBlockPersister opens (or creates) a WAL in the globalblocks/ subdir // and replays all persisted entries. Loaded blocks are available via // ConsumeLoaded. When stateDir is None, returns a no-op persister. -func NewGlobalBlockPersister(stateDir utils.Option[string], committee *types.Committee) (*GlobalBlockPersister, error) { +func NewGlobalBlockPersister(stateDir utils.Option[string], firstBlock types.GlobalBlockNumber) (*GlobalBlockPersister, error) { sd, ok := stateDir.Get() if !ok { - return &GlobalBlockPersister{state: utils.NewMutex(&globalBlockState{committee: committee, next: committee.FirstBlock()})}, nil + return &GlobalBlockPersister{state: utils.NewMutex(&globalBlockState{firstBlock: firstBlock, next: firstBlock})}, nil } dir := filepath.Join(sd, globalBlocksDir) iw, err := openIndexedWAL(dir, loadedGlobalBlockCodec{}) @@ -116,7 +116,7 @@ func NewGlobalBlockPersister(stateDir utils.Option[string], committee *types.Com return nil, fmt.Errorf("open global block WAL in %s: %w", dir, err) } - s := &globalBlockState{iw: utils.Some(iw), committee: committee, next: committee.FirstBlock()} + s := &globalBlockState{iw: utils.Some(iw), firstBlock: firstBlock, next: firstBlock} // TODO: avoid loading all blocks on startup; cache only the last N blocks // (e.g. 1000) in memory instead. loaded, err := s.loadAll() @@ -141,13 +141,13 @@ func (gp *GlobalBlockPersister) Next() types.GlobalBlockNumber { panic("unreachable") } -// LoadedFirst returns the first loaded block number, or committee.FirstBlock() if empty. +// LoadedFirst returns the first loaded block number, or firstBlock if empty. func (gp *GlobalBlockPersister) LoadedFirst() types.GlobalBlockNumber { for s := range gp.state.Lock() { if len(s.loaded) > 0 { return s.loaded[0].Number } - return s.committee.FirstBlock() + return s.firstBlock } panic("unreachable") } diff --git a/sei-tendermint/internal/autobahn/consensus/persist/globalblocks_test.go b/sei-tendermint/internal/autobahn/consensus/persist/globalblocks_test.go index 85e50feb7c..c5e366e256 100644 --- a/sei-tendermint/internal/autobahn/consensus/persist/globalblocks_test.go +++ b/sei-tendermint/internal/autobahn/consensus/persist/globalblocks_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" + "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" ) @@ -18,25 +19,21 @@ func makeGlobalBlocks(rng utils.Rng, n int) []*types.Block { func TestNewGlobalBlockPersisterEmptyDir(t *testing.T) { dir := t.TempDir() - rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 3) - fb := committee.FirstBlock() - - gp, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) require.NotNil(t, gp) require.Equal(t, 0, len(gp.ConsumeLoaded())) - require.Equal(t, fb, gp.Next()) + require.Equal(t, types.GlobalBlockNumber(0), gp.Next()) require.NoError(t, gp.Close()) } func TestNewGlobalBlockPersisterNoop(t *testing.T) { rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 3) - fb := committee.FirstBlock() + _, _ = epoch.GenRegistry(rng, 3) + fb := types.GlobalBlockNumber(0) blocks := makeGlobalBlocks(rng, 5) - gp, err := NewGlobalBlockPersister(utils.None[string](), committee) + gp, err := NewGlobalBlockPersister(utils.None[string](), 0) require.NoError(t, err) require.NotNil(t, gp) require.Equal(t, 0, len(gp.ConsumeLoaded())) @@ -61,11 +58,11 @@ func TestNewGlobalBlockPersisterNoop(t *testing.T) { func TestGlobalBlockPersistAndReload(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 3) - fb := committee.FirstBlock() + _, _ = epoch.GenRegistry(rng, 3) + fb := types.GlobalBlockNumber(0) blocks := makeGlobalBlocks(rng, 5) - gp, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) for i, b := range blocks { require.NoError(t, gp.PersistBlock(fb+types.GlobalBlockNumber(i), b)) @@ -73,7 +70,7 @@ func TestGlobalBlockPersistAndReload(t *testing.T) { require.Equal(t, fb+5, gp.Next()) require.NoError(t, gp.Close()) - gp2, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp2, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) loaded := gp2.ConsumeLoaded() require.Equal(t, 5, len(loaded)) @@ -87,11 +84,11 @@ func TestGlobalBlockPersistAndReload(t *testing.T) { func TestGlobalBlockTruncateAndReload(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 3) - fb := committee.FirstBlock() + _, _ = epoch.GenRegistry(rng, 3) + fb := types.GlobalBlockNumber(0) blocks := makeGlobalBlocks(rng, 10) - gp, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) for i, b := range blocks { require.NoError(t, gp.PersistBlock(fb+types.GlobalBlockNumber(i), b)) @@ -99,7 +96,7 @@ func TestGlobalBlockTruncateAndReload(t *testing.T) { require.NoError(t, gp.TruncateBefore(fb+5)) require.NoError(t, gp.Close()) - gp2, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp2, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) loaded := gp2.ConsumeLoaded() require.Equal(t, 5, len(loaded)) @@ -112,11 +109,11 @@ func TestGlobalBlockTruncateAndReload(t *testing.T) { func TestGlobalBlockTruncateAll(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 3) - fb := committee.FirstBlock() + _, _ = epoch.GenRegistry(rng, 3) + fb := types.GlobalBlockNumber(0) blocks := makeGlobalBlocks(rng, 5) - gp, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) for i, b := range blocks { require.NoError(t, gp.PersistBlock(fb+types.GlobalBlockNumber(i), b)) @@ -125,7 +122,7 @@ func TestGlobalBlockTruncateAll(t *testing.T) { require.Equal(t, fb+10, gp.Next()) require.NoError(t, gp.Close()) - gp2, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp2, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) require.Equal(t, 0, len(gp2.ConsumeLoaded())) require.Equal(t, fb, gp2.Next()) @@ -135,11 +132,11 @@ func TestGlobalBlockTruncateAll(t *testing.T) { func TestGlobalBlockDuplicateIgnored(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 3) - fb := committee.FirstBlock() + _, _ = epoch.GenRegistry(rng, 3) + fb := types.GlobalBlockNumber(0) block := types.GenBlock(rng) - gp, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) require.NoError(t, gp.PersistBlock(fb, block)) require.NoError(t, gp.PersistBlock(fb, block)) @@ -150,11 +147,11 @@ func TestGlobalBlockDuplicateIgnored(t *testing.T) { func TestGlobalBlockGapError(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 3) - fb := committee.FirstBlock() + _, _ = epoch.GenRegistry(rng, 3) + fb := types.GlobalBlockNumber(0) block := types.GenBlock(rng) - gp, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) err = gp.PersistBlock(fb+2, block) require.Error(t, err) @@ -165,11 +162,11 @@ func TestGlobalBlockGapError(t *testing.T) { func TestGlobalBlockTruncateBeforeNoop(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 3) - fb := committee.FirstBlock() + _, _ = epoch.GenRegistry(rng, 3) + fb := types.GlobalBlockNumber(0) blocks := makeGlobalBlocks(rng, 5) - gp, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) for i, b := range blocks { require.NoError(t, gp.PersistBlock(fb+types.GlobalBlockNumber(i), b)) @@ -178,7 +175,7 @@ func TestGlobalBlockTruncateBeforeNoop(t *testing.T) { require.Equal(t, fb+5, gp.Next()) require.NoError(t, gp.Close()) - gp2, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp2, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) require.Equal(t, 5, len(gp2.ConsumeLoaded())) require.NoError(t, gp2.Close()) @@ -187,18 +184,18 @@ func TestGlobalBlockTruncateBeforeNoop(t *testing.T) { func TestGlobalBlockContinueAfterReload(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 3) - fb := committee.FirstBlock() + _, _ = epoch.GenRegistry(rng, 3) + fb := types.GlobalBlockNumber(0) blocks := makeGlobalBlocks(rng, 10) - gp, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) for i := range 5 { require.NoError(t, gp.PersistBlock(fb+types.GlobalBlockNumber(i), blocks[i])) } require.NoError(t, gp.Close()) - gp2, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp2, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) require.Equal(t, 5, len(gp2.ConsumeLoaded())) for i := 5; i < 10; i++ { @@ -207,7 +204,7 @@ func TestGlobalBlockContinueAfterReload(t *testing.T) { require.Equal(t, fb+10, gp2.Next()) require.NoError(t, gp2.Close()) - gp3, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp3, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) require.Equal(t, 10, len(gp3.ConsumeLoaded())) require.NoError(t, gp3.Close()) @@ -216,12 +213,12 @@ func TestGlobalBlockContinueAfterReload(t *testing.T) { func TestGlobalBlockTruncateAfterMiddle(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 3) - fb := committee.FirstBlock() + _, _ = epoch.GenRegistry(rng, 3) + fb := types.GlobalBlockNumber(0) blocks := makeGlobalBlocks(rng, 5) // First session: persist 5 blocks. - gp, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) for i, b := range blocks { require.NoError(t, gp.PersistBlock(fb+types.GlobalBlockNumber(i), b)) @@ -229,7 +226,7 @@ func TestGlobalBlockTruncateAfterMiddle(t *testing.T) { require.NoError(t, gp.Close()) // Second session: reload, then TruncateAfter trims WAL and loaded. - gp2, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp2, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) require.NoError(t, gp2.TruncateAfter(fb+3)) require.Equal(t, fb+3, gp2.Next()) @@ -240,7 +237,7 @@ func TestGlobalBlockTruncateAfterMiddle(t *testing.T) { require.NoError(t, gp2.Close()) // Third session: verify WAL persistence. - gp3, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp3, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) loaded = gp3.ConsumeLoaded() require.Equal(t, 3, len(loaded)) @@ -252,11 +249,11 @@ func TestGlobalBlockTruncateAfterMiddle(t *testing.T) { func TestGlobalBlockTruncateAfterNoop(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 3) - fb := committee.FirstBlock() + _, _ = epoch.GenRegistry(rng, 3) + fb := types.GlobalBlockNumber(0) blocks := makeGlobalBlocks(rng, 3) - gp, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) for i, b := range blocks { require.NoError(t, gp.PersistBlock(fb+types.GlobalBlockNumber(i), b)) @@ -270,11 +267,11 @@ func TestGlobalBlockTruncateAfterNoop(t *testing.T) { func TestGlobalBlockTruncateAfterBeforeFirst(t *testing.T) { dir := t.TempDir() rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 3) - fb := committee.FirstBlock() + _, _ = epoch.GenRegistry(rng, 3) + fb := types.GlobalBlockNumber(0) blocks := makeGlobalBlocks(rng, 5) - gp, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) for i, b := range blocks { require.NoError(t, gp.PersistBlock(fb+types.GlobalBlockNumber(i), b)) @@ -289,7 +286,7 @@ func TestGlobalBlockTruncateAfterBeforeFirst(t *testing.T) { require.NoError(t, gp.Close()) // Reload — should be empty. - gp2, err := NewGlobalBlockPersister(utils.Some(dir), committee) + gp2, err := NewGlobalBlockPersister(utils.Some(dir), 0) require.NoError(t, err) require.Equal(t, 0, len(gp2.ConsumeLoaded())) require.NoError(t, gp2.Close()) diff --git a/sei-tendermint/internal/autobahn/consensus/persisted_inner.go b/sei-tendermint/internal/autobahn/consensus/persisted_inner.go index 8a05a04714..6aa988153a 100644 --- a/sei-tendermint/internal/autobahn/consensus/persisted_inner.go +++ b/sei-tendermint/internal/autobahn/consensus/persisted_inner.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/epoch" "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" @@ -81,9 +82,15 @@ func (p *persistedInner) View() types.View { // validate checks internal consistency and cryptographic signatures of persisted state. // Returns error on corrupt state. -func (p *persistedInner) validate(committee *types.Committee) error { +func (p *persistedInner) validate(registry *epoch.Registry) error { + // CommitQC was produced under its own epoch's committee. + // All other fields (votes, PrepareQC, TimeoutQC) belong to the active view, + // which may be one epoch ahead when a TimeoutQC bumped the index. + activeCommittee := registry.CommitteeFor(p.View().Index) + if cqc, ok := p.CommitQC.Get(); ok { - if err := cqc.Verify(committee); err != nil { + commitCommittee := registry.CommitteeFor(cqc.Index()) + if err := cqc.Verify(commitCommittee); err != nil { return fmt.Errorf("corrupt persisted state: CommitQC failed verification: %w", err) } } @@ -96,7 +103,7 @@ func (p *persistedInner) validate(committee *types.Committee) error { if tqcIndex != expectedIndex { return fmt.Errorf("corrupt persisted state: TimeoutQC has index %d but expected %d", tqcIndex, expectedIndex) } - if err := tqc.Verify(committee, p.CommitQC); err != nil { + if err := tqc.Verify(activeCommittee, p.CommitQC); err != nil { return fmt.Errorf("corrupt persisted state: TimeoutQC failed verification: %w", err) } } @@ -117,24 +124,24 @@ func (p *persistedInner) validate(committee *types.Committee) error { // PrepareQC is required when CommitVote is present (CommitVote requires PrepareQC justification). if pqc, ok := p.PrepareQC.Get(); ok { - if err := checkViewAndSig("PrepareQC", pqc.Proposal().View(), pqc.Verify(committee)); err != nil { + if err := checkViewAndSig("PrepareQC", pqc.Proposal().View(), pqc.Verify(activeCommittee)); err != nil { return err } } else if p.CommitVote.IsPresent() { return fmt.Errorf("corrupt persisted state: CommitVote present without PrepareQC") } if v, ok := p.CommitVote.Get(); ok { - if err := checkViewAndSig("CommitVote", v.Msg().Proposal().View(), v.VerifySig(committee)); err != nil { + if err := checkViewAndSig("CommitVote", v.Msg().Proposal().View(), v.VerifySig(activeCommittee)); err != nil { return err } } if v, ok := p.PrepareVote.Get(); ok { - if err := checkViewAndSig("PrepareVote", v.Msg().Proposal().View(), v.VerifySig(committee)); err != nil { + if err := checkViewAndSig("PrepareVote", v.Msg().Proposal().View(), v.VerifySig(activeCommittee)); err != nil { return err } } if v, ok := p.TimeoutVote.Get(); ok { - if err := checkViewAndSig("TimeoutVote", v.View(), v.Verify(committee)); err != nil { + if err := checkViewAndSig("TimeoutVote", v.View(), v.Verify(activeCommittee)); err != nil { return err } } diff --git a/sei-tendermint/internal/autobahn/consensus/state.go b/sei-tendermint/internal/autobahn/consensus/state.go index 75374a5ba4..b458651018 100644 --- a/sei-tendermint/internal/autobahn/consensus/state.go +++ b/sei-tendermint/internal/autobahn/consensus/state.go @@ -100,7 +100,7 @@ func newState( pers utils.Option[persist.Persister[*pb.PersistedInner]], persistedData utils.Option[*pb.PersistedInner], ) (*State, error) { - initialInner, err := newInner(persistedData, data.Committee()) + initialInner, err := newInner(persistedData, data.Registry()) if err != nil { return nil, fmt.Errorf("newInner: %w", err) } @@ -123,7 +123,7 @@ func newState( prepareVotes: utils.NewMutex(newPrepareVotes()), commitVotes: utils.NewMutex(newCommitVotes()), - myView: utils.NewAtomicSend(types.ViewSpec{}), + myView: utils.NewAtomicSend(types.ViewSpec{FirstBlock: data.Registry().FirstBlock()}), myProposal: utils.NewAtomicSend(utils.None[*types.FullProposal]()), myPrepareVote: utils.NewAtomicSend(utils.None[*types.ConsensusReqPrepareVote]()), myCommitVote: utils.NewAtomicSend(utils.None[*types.ConsensusReqCommitVote]()), @@ -166,33 +166,36 @@ func (s *State) PushTimeoutQC(ctx context.Context, qc *types.TimeoutQC) error { // PushPrepareVote processes an unverified Prepare vote message. func (s *State) PushPrepareVote(vote *types.Signed[*types.PrepareVote]) error { - if err := vote.VerifySig(s.Data().Committee()); err != nil { + committee := s.Data().Registry().CommitteeFor(vote.Msg().Proposal().Index()) + if err := vote.VerifySig(committee); err != nil { return fmt.Errorf("vote.VerifySig(): %w", err) } for pv := range s.prepareVotes.Lock() { - pv.pushVote(s.Data().Committee(), vote) + pv.pushVote(committee, vote) } return nil } // PushCommitVote processes an unverified CommitVote message. func (s *State) PushCommitVote(vote *types.Signed[*types.CommitVote]) error { - if err := vote.VerifySig(s.Data().Committee()); err != nil { + committee := s.Data().Registry().CommitteeFor(vote.Msg().Proposal().Index()) + if err := vote.VerifySig(committee); err != nil { return fmt.Errorf("vote.VerifySig(): %w", err) } for cv := range s.commitVotes.Lock() { - cv.pushVote(s.Data().Committee(), vote) + cv.pushVote(committee, vote) } return nil } // PushTimeoutVote processes an unverified FullTimeoutVote message. func (s *State) PushTimeoutVote(vote *types.FullTimeoutVote) error { - if err := vote.Verify(s.Data().Committee()); err != nil { + committee := s.Data().Registry().CommitteeFor(vote.View().Index) + if err := vote.Verify(committee); err != nil { return fmt.Errorf("vote.Verify(): %w", err) } for tv := range s.timeoutVotes.Lock() { - tv.pushVote(s.Data().Committee(), vote) + tv.pushVote(committee, vote) } return nil } @@ -203,8 +206,8 @@ func (s *State) Avail() *avail.State { return s.avail } // Constructs new proposals. func (s *State) runPropose(ctx context.Context) error { - committee := s.Data().Committee() return s.myView.Iter(ctx, func(ctx context.Context, vs types.ViewSpec) error { + committee := s.Data().Registry().CommitteeFor(vs.View().Index) if committee.Leader(vs.View()) != s.cfg.Key.Public() { return nil // not the leader. } @@ -214,7 +217,7 @@ func (s *State) runPropose(ctx context.Context) error { return nil } // Wait for laneQCs. - laneQCsMap, err := s.avail.WaitForLaneQCs(ctx, vs.CommitQC) + laneQCsMap, err := s.avail.WaitForLaneQCs(ctx, vs.CommitQC, s.Data().Registry().EpochFor(vs.View().Index)) if err != nil { return fmt.Errorf("s.avail.WaitForLaneQCs(): %w", err) } @@ -223,6 +226,8 @@ func (s *State) runPropose(ctx context.Context) error { s.cfg.Key, committee, vs, + s.Data().Registry().FirstBlock(), + s.Data().Registry().GenesisTimestamp(), time.Now(), laneQCsMap, s.avail.LastAppQC(), @@ -249,7 +254,7 @@ func updateOutput[T types.ConsensusReq](w *utils.AtomicSend[utils.Option[T]], v // timers, neither of which constitutes a vote. func (s *State) runOutputs(ctx context.Context) error { return s.innerRecv.Iter(ctx, func(ctx context.Context, i inner) error { - vs := types.ViewSpec{CommitQC: i.CommitQC, TimeoutQC: i.TimeoutQC} + vs := types.ViewSpec{CommitQC: i.CommitQC, TimeoutQC: i.TimeoutQC, FirstBlock: s.Data().Registry().FirstBlock()} old := s.myView.Load() if old.View().Less(vs.View()) { s.myView.Store(vs) diff --git a/sei-tendermint/internal/autobahn/consensus/state_test.go b/sei-tendermint/internal/autobahn/consensus/state_test.go index 43cdb6ef61..41f755a542 100644 --- a/sei-tendermint/internal/autobahn/consensus/state_test.go +++ b/sei-tendermint/internal/autobahn/consensus/state_test.go @@ -8,6 +8,7 @@ import ( "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" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/scope" @@ -17,10 +18,10 @@ import ( // view timeout (so voteTimeout is only triggered explicitly). // keys[0] is used as the node's signing key. func newTestState(rng utils.Rng) (*State, []types.SecretKey) { - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) dataState := utils.OrPanic1(data.NewState( - &data.Config{Committee: committee}, - utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)), + &data.Config{Registry: registry}, + utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())), )) s := utils.OrPanic1(NewState(&Config{ Key: keys[0], @@ -258,7 +259,7 @@ func TestVoteTimeoutPrepareQC_CurrentViewPresentInheritedNone(t *testing.T) { // voteTimeout still inherits the PrepareQC from the persisted TimeoutQC. func TestVoteTimeoutPrepareQC_PersistedRestart(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) dir := t.TempDir() makeCfg := func() *Config { @@ -270,8 +271,8 @@ func TestVoteTimeoutPrepareQC_PersistedRestart(t *testing.T) { } makeDataState := func() *data.State { return utils.OrPanic1(data.NewState( - &data.Config{Committee: committee}, - utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)), + &data.Config{Registry: registry}, + utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())), )) } diff --git a/sei-tendermint/internal/autobahn/data/state.go b/sei-tendermint/internal/autobahn/data/state.go index 5aba4e17c1..15fff9e232 100644 --- a/sei-tendermint/internal/autobahn/data/state.go +++ b/sei-tendermint/internal/autobahn/data/state.go @@ -10,6 +10,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" "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" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/scope" ) @@ -24,8 +25,8 @@ var ErrPruned = errors.New("pruned") // Config is the config for the data State. type Config struct { - // Committee. - Committee *types.Committee + // Registry is the authoritative source of committee and stake information. + Registry *epoch.Registry // PruneAfter is the duration after which the state prunes executed blocks. PruneAfter utils.Option[time.Duration] } @@ -90,8 +91,8 @@ func (dw *DataWAL) TruncateBefore(n types.GlobalBlockNumber) error { // 6 [a,b] [X,Y) Prune crash: QCs ahead (a=Y) Tail: truncate blocks to Y // 8 [a,b] [X,Y) QCs ahead (normal, b fb { @@ -115,12 +116,12 @@ func (dw *DataWAL) reconcile(committee *types.Committee) error { // NewDataWAL constructs both global-block and global-commitqc WALs. // When stateDir is None, the returned persisters are no-ops. -func NewDataWAL(stateDir utils.Option[string], committee *types.Committee) (*DataWAL, error) { - blocks, err := persist.NewGlobalBlockPersister(stateDir, committee) +func NewDataWAL(stateDir utils.Option[string], firstBlock types.GlobalBlockNumber) (*DataWAL, error) { + blocks, err := persist.NewGlobalBlockPersister(stateDir, firstBlock) if err != nil { return nil, fmt.Errorf("global block WAL: %w", err) } - commitQCs, err := persist.NewFullCommitQCPersister(stateDir, committee) + commitQCs, err := persist.NewFullCommitQCPersister(stateDir, firstBlock) if err != nil { _ = blocks.Close() return nil, fmt.Errorf("full commitqc WAL: %w", err) @@ -132,7 +133,7 @@ func NewDataWAL(stateDir utils.Option[string], committee *types.Committee) (*Dat // Reconcile cursor inconsistency: a crash between the two parallel // TruncateBefore calls can leave one WAL truncated while the other // still has stale entries. Advance both to the max starting point. - if err := dw.reconcile(committee); err != nil { + if err := dw.reconcile(firstBlock); err != nil { _ = dw.Close() return nil, fmt.Errorf("reconcile WALs: %w", err) } @@ -174,8 +175,8 @@ type inner struct { nextQC types.GlobalBlockNumber } -func newInner(committee *types.Committee) *inner { - first := committee.FirstBlock() +func newInner(firstBlock types.GlobalBlockNumber) *inner { + first := firstBlock return &inner{ qcs: map[types.GlobalBlockNumber]*types.FullCommitQC{}, blocks: map[types.GlobalBlockNumber]*types.Block{}, @@ -207,7 +208,7 @@ func (i *inner) insertQC(committee *types.Committee, qc *types.FullCommitQC) err if err := qc.Verify(committee); err != nil { return fmt.Errorf("qc.Verify(): %w", err) } - gr := qc.QC().GlobalRange(committee) + gr := qc.QC().GlobalRange() if gr.Next <= i.nextQC { return nil // fully behind, skip } @@ -237,7 +238,7 @@ func (i *inner) insertBlock(committee *types.Committee, n types.GlobalBlockNumbe return nil // already have it } qc := i.qcs[n] - storedGR := qc.QC().GlobalRange(committee) + storedGR := qc.QC().GlobalRange() want := qc.Headers()[n-storedGR.First].Hash() got := block.Header().Hash() if want != got { @@ -287,7 +288,7 @@ type State struct { // dataWAL persists blocks and QCs to WALs for crash recovery and provides // preloaded data from the previous run. Use NewDataWAL to construct it. func NewState(cfg *Config, dataWAL *DataWAL) (*State, error) { - inner := newInner(cfg.Committee) + inner := newInner(cfg.Registry.FirstBlock()) // Fast-forward cursors to where data starts. Use blocks as golden: // per-block pruning may split a QC range, so blocks determine where // useful data starts. QCs before that are kept for verification but @@ -295,13 +296,14 @@ func NewState(cfg *Config, dataWAL *DataWAL) (*State, error) { blocksFirst := dataWAL.Blocks.LoadedFirst() qcFirst := dataWAL.CommitQCs.LoadedFirst() dataFirst := max(blocksFirst, qcFirst) - if dataFirst > cfg.Committee.FirstBlock() { + if dataFirst > cfg.Registry.FirstBlock() { inner.skipTo(dataFirst) } // Restore QCs. insertQC handles partially pruned QCs (range starts // before inner.first) by skipping the pruned prefix. for _, qc := range dataWAL.CommitQCs.ConsumeLoaded() { - if err := inner.insertQC(cfg.Committee, qc); err != nil { + committee := cfg.Registry.CommitteeFor(qc.QC().Proposal().Index()) + if err := inner.insertQC(committee, qc); err != nil { return nil, fmt.Errorf("load QC from WAL: %w", err) } } @@ -315,10 +317,12 @@ func NewState(cfg *Config, dataWAL *DataWAL) (*State, error) { return nil, fmt.Errorf("block gap in WAL: expected %d, got %d", expectedBlock, lb.Number) } expectedBlock = lb.Number + 1 - if err := lb.Block.Verify(cfg.Committee); err != nil { + qc := inner.qcs[lb.Number] + committee := cfg.Registry.CommitteeFor(qc.QC().Proposal().Index()) + if err := lb.Block.Verify(committee); err != nil { return nil, fmt.Errorf("load block %d from WAL: %w", lb.Number, err) } - if err := inner.insertBlock(cfg.Committee, lb.Number, lb.Block); err != nil { + if err := inner.insertBlock(committee, lb.Number, lb.Block); err != nil { return nil, fmt.Errorf("load block %d from WAL: %w", lb.Number, err) } } @@ -342,15 +346,16 @@ func NewState(cfg *Config, dataWAL *DataWAL) (*State, error) { }, nil } -// Committee returns the committee. -func (s *State) Committee() *types.Committee { return s.cfg.Committee } +// Registry returns the epoch registry. +func (s *State) Registry() *epoch.Registry { return s.cfg.Registry } // PushQC pushes FullCommitQC and a subset of blocks that were finalized by it. // Pushing the qc and blocks is atomic, so that no unnecessary GetBlock RPCs are issued. // Even if the qc was already pushed earlier, the blocks are pushed anyway. func (s *State) PushQC(ctx context.Context, qc *types.FullCommitQC, blocks []*types.Block) error { // Wait until QC is needed. - gr := qc.QC().GlobalRange(s.cfg.Committee) + committee := s.cfg.Registry.CommitteeFor(qc.QC().Proposal().Index()) + gr := qc.QC().GlobalRange() needQC, err := func() (bool, error) { for inner, ctrl := range s.inner.Lock() { if err := ctrl.WaitUntil(ctx, func() bool { @@ -367,14 +372,14 @@ func (s *State) PushQC(ctx context.Context, qc *types.FullCommitQC, blocks []*ty } // Verify data. if needQC { - if err := qc.Verify(s.cfg.Committee); err != nil { + if err := qc.Verify(committee); err != nil { return fmt.Errorf("qc.Verify(): %w", err) } } byHash := map[types.BlockHeaderHash]*types.Block{} for _, b := range blocks { byHash[b.Header().Hash()] = b - if err := b.Verify(s.cfg.Committee); err != nil { + if err := b.Verify(committee); err != nil { return fmt.Errorf("b.Verify(): %w", err) } } @@ -393,9 +398,10 @@ func (s *State) PushQC(ctx context.Context, qc *types.FullCommitQC, blocks []*ty // Match blocks against stored (already verified) QC headers. for n := max(inner.nextBlock, gr.First); n < min(gr.Next, inner.nextQC); n += 1 { storedQC := inner.qcs[n] - storedGR := storedQC.QC().GlobalRange(s.cfg.Committee) + storedGR := storedQC.QC().GlobalRange() + storedCommittee := s.cfg.Registry.CommitteeFor(storedQC.QC().Proposal().Index()) if b, ok := byHash[storedQC.Headers()[n-storedGR.First].Hash()]; ok { - if err := inner.insertBlock(s.cfg.Committee, n, b); err != nil { + if err := inner.insertBlock(storedCommittee, n, b); err != nil { return err } } @@ -425,15 +431,22 @@ func (s *State) QC(ctx context.Context, n types.GlobalBlockNumber) (*types.FullC // PushBlock pushes block to the state. // Waits until the block header is available. func (s *State) PushBlock(ctx context.Context, n types.GlobalBlockNumber, block *types.Block) error { - // Verify outside the lock to avoid holding it during expensive crypto. - if err := block.Verify(s.cfg.Committee); err != nil { + // Pre-verify outside the lock with the epoch window committee. Blocks from + // validators in adjacent epochs are accepted here; the precise committee + // check inside the lock provides the authoritative gate. + if err := s.cfg.Registry.VerifyInWindow(block.Verify); err != nil { return fmt.Errorf("block.Verify(): %w", err) } for inner, ctrl := range s.inner.Lock() { if err := ctrl.WaitUntil(ctx, func() bool { return n < inner.nextQC }); err != nil { return err } - if err := inner.insertBlock(s.cfg.Committee, n, block); err != nil { + qc := inner.qcs[n] + committee := s.cfg.Registry.CommitteeFor(qc.QC().Proposal().Index()) + if err := block.Verify(committee); err != nil { + return fmt.Errorf("block.Verify(): %w", err) + } + if err := inner.insertBlock(committee, n, block); err != nil { return err } inner.updateNextBlock(s.metrics) @@ -472,7 +485,7 @@ func (s *State) GlobalBlockByHash(hash types.BlockHeaderHash) (utils.Option[*typ if !ok { return utils.None[*types.GlobalBlock](), nil } - return utils.Some(inner.globalBlockAt(s.Committee(), n)), nil + return utils.Some(inner.globalBlockAt(s.cfg.Registry, n)), nil } panic("unreachable") } @@ -515,12 +528,12 @@ func (s *State) TryBlock(n types.GlobalBlockNumber) (*types.Block, error) { // globalBlockAt assembles the GlobalBlock at height n from inner state. // Caller must have verified n is in [inner.first, inner.nextBlock); n // outside that range nil-derefs on inner.blocks[n] / inner.qcs[n]. -func (i *inner) globalBlockAt(c *types.Committee, n types.GlobalBlockNumber) *types.GlobalBlock { +func (i *inner) globalBlockAt(registry *epoch.Registry, n types.GlobalBlockNumber) *types.GlobalBlock { b := i.blocks[n] qc := i.qcs[n].QC() return &types.GlobalBlock{ GlobalNumber: n, - Timestamp: qc.Proposal().BlockTimestamp(c, n).OrPanic("global block not in QC"), + Timestamp: qc.Proposal().BlockTimestamp(n).OrPanic("global block not in QC"), Header: b.Header(), Payload: b.Payload(), FinalAppState: qc.Proposal().App(), @@ -539,7 +552,7 @@ func (s *State) GlobalBlock(ctx context.Context, n types.GlobalBlockNumber) (*ty if n < inner.first { return nil, ErrPruned } - return inner.globalBlockAt(s.Committee(), n), nil + return inner.globalBlockAt(s.cfg.Registry, n), nil } panic("unreachable") } @@ -698,7 +711,7 @@ func (s *State) runPersist(ctx context.Context) error { seen := map[types.GlobalBlockNumber]bool{} for n := persistedQC; n < inner.nextQC; n++ { qc := inner.qcs[n] - first := qc.QC().GlobalRange(s.cfg.Committee).First + first := qc.QC().GlobalRange().First if !seen[first] { seen[first] = true b.qcs = append(b.qcs, qc) diff --git a/sei-tendermint/internal/autobahn/data/state_test.go b/sei-tendermint/internal/autobahn/data/state_test.go index 84ac54d594..946d22f235 100644 --- a/sei-tendermint/internal/autobahn/data/state_test.go +++ b/sei-tendermint/internal/autobahn/data/state_test.go @@ -13,6 +13,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" "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" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/scope" @@ -50,11 +51,12 @@ func snapshot(s *State) Snapshot { func TestState(t *testing.T) { ctx := t.Context() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) + committee := registry.LatestCommittee() if err := scope.Run(ctx, func(ctx context.Context, s scope.Scope) error { state := utils.OrPanic1(NewState(&Config{ - Committee: committee, - }, utils.OrPanic1(NewDataWAL(utils.None[string](), committee)))) + Registry: registry, + }, utils.OrPanic1(NewDataWAL(utils.None[string](), registry.FirstBlock())))) s.SpawnBgNamed("state.Run()", func() error { return utils.IgnoreCancel(state.Run(ctx)) }) @@ -63,12 +65,12 @@ func TestState(t *testing.T) { prev := utils.None[*types.CommitQC]() for i := range 3 { t.Logf("iteration %v", i) - qc, blocks := TestCommitQC(rng, committee, keys, prev) + qc, blocks := TestCommitQC(rng, committee, keys, prev, registry.FirstBlock(), time.Time{}) prev = utils.Some(qc.QC()) if err := state.PushQC(ctx, qc, blocks); err != nil { return fmt.Errorf("state.PushQC(): %w", err) } - gr := qc.QC().GlobalRange(committee) + gr := qc.QC().GlobalRange() for n := gr.First; n < gr.Next; n += 1 { want.QCs[n] = qc want.Blocks[n] = blocks[n-gr.First] @@ -96,7 +98,7 @@ func TestState(t *testing.T) { wantG := &types.GlobalBlock{ GlobalNumber: n, - Timestamp: want.QCs[n].QC().Proposal().BlockTimestamp(committee, n).OrPanic("global block not in QC"), + Timestamp: want.QCs[n].QC().Proposal().BlockTimestamp(n).OrPanic("global block not in QC"), Header: wantB.Header(), Payload: wantB.Payload(), FinalAppState: want.QCs[n].QC().Proposal().App(), @@ -118,15 +120,16 @@ func TestState(t *testing.T) { func TestPushQCStaleQCDoesNotCorruptState(t *testing.T) { ctx := t.Context() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) + committee := registry.LatestCommittee() state := utils.OrPanic1(NewState(&Config{ - Committee: committee, - }, utils.OrPanic1(NewDataWAL(utils.None[string](), committee)))) + Registry: registry, + }, utils.OrPanic1(NewDataWAL(utils.None[string](), registry.FirstBlock())))) // Push a valid QC to advance inner.nextQC. - qc1, blocks1 := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC]()) + qc1, blocks1 := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC](), registry.FirstBlock(), time.Time{}) require.NoError(t, state.PushQC(ctx, qc1, blocks1)) - nextQC := qc1.QC().GlobalRange(committee).Next + nextQC := qc1.QC().GlobalRange().Next // Construct a malicious QC signed by non-committee keys. // It starts from block 0 (stale) but extends beyond nextQC. @@ -172,11 +175,13 @@ func TestPushQCStaleQCDoesNotCorruptState(t *testing.T) { leaderKey, committee, viewSpec, + 0, + time.Time{}, time.Now(), laneQCs, utils.None[*types.AppQC](), )) - malGR := proposal.Proposal().Msg().GlobalRange(committee) + malGR := proposal.Proposal().Msg().GlobalRange() require.Less(t, malGR.First, nextQC, "test setup: malicious gr.First must be < nextQC") require.Greater(t, malGR.Next, nextQC, "test setup: malicious gr.Next must be > nextQC") @@ -193,7 +198,7 @@ func TestPushQCStaleQCDoesNotCorruptState(t *testing.T) { _ = state.PushQC(ctx, maliciousQC, malBlocks) // Verify state was not corrupted: all previously pushed QCs and blocks are intact. - gr1 := qc1.QC().GlobalRange(committee) + gr1 := qc1.QC().GlobalRange() for n := gr1.First; n < gr1.Next; n++ { got, err := state.QC(ctx, n) require.NoError(t, err) @@ -211,9 +216,9 @@ func TestPushQCStaleQCDoesNotCorruptState(t *testing.T) { } // Verify state is still functional: the next valid QC is accepted and visible. - qc2, blocks2 := TestCommitQC(rng, committee, keys, utils.Some(qc1.QC())) + qc2, blocks2 := TestCommitQC(rng, committee, keys, utils.Some(qc1.QC()), registry.FirstBlock(), time.Time{}) require.NoError(t, state.PushQC(ctx, qc2, blocks2)) - gr2 := qc2.QC().GlobalRange(committee) + gr2 := qc2.QC().GlobalRange() for n := gr2.First; n < gr2.Next; n++ { got, err := state.QC(ctx, n) require.NoError(t, err) @@ -224,15 +229,16 @@ func TestPushQCStaleQCDoesNotCorruptState(t *testing.T) { func TestPushQCIgnoresBlocksMatchingUnverifiedHeaders(t *testing.T) { ctx := t.Context() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) + committee := registry.LatestCommittee() state := utils.OrPanic1(NewState(&Config{ - Committee: committee, - }, utils.OrPanic1(NewDataWAL(utils.None[string](), committee)))) + Registry: registry, + }, utils.OrPanic1(NewDataWAL(utils.None[string](), registry.FirstBlock())))) // Push qc1 with NO blocks — only the QC is stored. - qc1, blocks1 := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC]()) + qc1, blocks1 := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC](), registry.FirstBlock(), time.Time{}) require.NoError(t, state.PushQC(ctx, qc1, nil)) - gr := qc1.QC().GlobalRange(committee) + gr := qc1.QC().GlobalRange() // Build a tampered FullCommitQC: same CommitQC (same range) but with // different block headers (different payloads → different hashes). @@ -269,11 +275,12 @@ func TestPushQCIgnoresBlocksMatchingUnverifiedHeaders(t *testing.T) { func TestExecution(t *testing.T) { ctx := t.Context() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) + committee := registry.LatestCommittee() if err := scope.Run(ctx, func(ctx context.Context, s scope.Scope) error { state := utils.OrPanic1(NewState(&Config{ - Committee: committee, - }, utils.OrPanic1(NewDataWAL(utils.None[string](), committee)))) + Registry: registry, + }, utils.OrPanic1(NewDataWAL(utils.None[string](), registry.FirstBlock())))) s.SpawnBgNamed("state.Run()", func() error { return utils.IgnoreCancel(state.Run(ctx)) }) @@ -281,12 +288,12 @@ func TestExecution(t *testing.T) { prev := utils.None[*types.CommitQC]() for i := range 3 { t.Logf("iteration %v", i) - qc, blocks := TestCommitQC(rng, committee, keys, prev) + qc, blocks := TestCommitQC(rng, committee, keys, prev, registry.FirstBlock(), time.Time{}) if err := state.PushQC(ctx, qc, blocks); err != nil { return fmt.Errorf("state.PushQC(): %w", err) } prev = utils.Some(qc.QC()) - gr := qc.QC().GlobalRange(committee) + gr := qc.QC().GlobalRange() // PushAppHash for a block beyond nextBlock should not succeed: // it waits for persistence which never happens for unfinalised blocks. shortCtx, cancel := context.WithTimeout(ctx, 10*time.Millisecond) @@ -313,16 +320,17 @@ func TestExecution(t *testing.T) { func TestPushBlockAcceptsBlockWithQC(t *testing.T) { ctx := t.Context() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) + committee := registry.LatestCommittee() state := utils.OrPanic1(NewState(&Config{ - Committee: committee, - }, utils.OrPanic1(NewDataWAL(utils.None[string](), committee)))) + Registry: registry, + }, utils.OrPanic1(NewDataWAL(utils.None[string](), registry.FirstBlock())))) // Push QC without blocks. - qc, blocks := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC]()) + qc, blocks := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC](), registry.FirstBlock(), time.Time{}) require.NoError(t, state.PushQC(ctx, qc, nil)) - gr := qc.QC().GlobalRange(committee) + gr := qc.QC().GlobalRange() // PushBlock for a block whose QC is already present succeeds immediately. require.NoError(t, state.PushBlock(ctx, gr.First, blocks[0])) @@ -346,15 +354,16 @@ func TestPushBlockAcceptsBlockWithQC(t *testing.T) { func TestGlobalBlockByHash(t *testing.T) { ctx := t.Context() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) + committee := registry.LatestCommittee() state := utils.OrPanic1(NewState(&Config{ - Committee: committee, - }, utils.OrPanic1(NewDataWAL(utils.None[string](), committee)))) + Registry: registry, + }, utils.OrPanic1(NewDataWAL(utils.None[string](), registry.FirstBlock())))) - qc, blocks := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC]()) + qc, blocks := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC](), registry.FirstBlock(), time.Time{}) require.NoError(t, state.PushQC(ctx, qc, blocks)) - gr := qc.QC().GlobalRange(committee) + gr := qc.QC().GlobalRange() n := gr.First wantBlock := blocks[0] wantHash := wantBlock.Header().Hash() @@ -391,12 +400,12 @@ func TestGlobalBlockByHash(t *testing.T) { func TestReconcileCase1Empty(t *testing.T) { t.Log("Reconcile case 1: Fresh start (empty/empty)") rng := utils.TestRng() - committee, _ := types.GenCommittee(rng, 3) + registry, _ := epoch.GenRegistry(rng, 3) dir := t.TempDir() - fb := committee.FirstBlock() + fb := registry.FirstBlock() - dw := utils.OrPanic1(NewDataWAL(utils.Some(dir), committee)) - state := utils.OrPanic1(NewState(&Config{Committee: committee}, dw)) + dw := utils.OrPanic1(NewDataWAL(utils.Some(dir), registry.FirstBlock())) + state := utils.OrPanic1(NewState(&Config{Registry: registry}, dw)) for inner := range state.inner.Lock() { require.Equal(t, fb, inner.first) @@ -411,14 +420,15 @@ func TestReconcileCase1Empty(t *testing.T) { func TestReconcileCase2Corrupted(t *testing.T) { t.Log("Reconcile case 2: QCs lost (corruption), returns error") rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) + committee := registry.LatestCommittee() dir := t.TempDir() // Persist blocks and QCs normally. - qc1, blocks1 := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC]()) - gr1 := qc1.QC().GlobalRange(committee) + qc1, blocks1 := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC](), registry.FirstBlock(), time.Time{}) + gr1 := qc1.QC().GlobalRange() - dw1 := utils.OrPanic1(NewDataWAL(utils.Some(dir), committee)) + dw1 := utils.OrPanic1(NewDataWAL(utils.Some(dir), registry.FirstBlock())) require.NoError(t, dw1.CommitQCs.PersistQC(qc1)) for i, n := 0, gr1.First; n < gr1.Next; n++ { require.NoError(t, dw1.Blocks.PersistBlock(n, blocks1[i])) @@ -430,7 +440,7 @@ func TestReconcileCase2Corrupted(t *testing.T) { require.NoError(t, os.RemoveAll(filepath.Join(dir, "fullcommitqcs"))) // Reopen should fail — blocks exist but QCs are gone. - _, err := NewDataWAL(utils.Some(dir), committee) + _, err := NewDataWAL(utils.Some(dir), registry.FirstBlock()) require.Error(t, err) require.Contains(t, err.Error(), "corrupted") } @@ -444,15 +454,16 @@ func TestReconcileCase3BlocksLost(t *testing.T) { t.Log("Reconcile case 3: Blocks lost (crash), QCs survive") ctx := t.Context() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) + committee := registry.LatestCommittee() dir := t.TempDir() // First run: populate both WALs. - qc1, blocks1 := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC]()) - gr1 := qc1.QC().GlobalRange(committee) + qc1, blocks1 := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC](), registry.FirstBlock(), time.Time{}) + gr1 := qc1.QC().GlobalRange() - dw1 := utils.OrPanic1(NewDataWAL(utils.Some(dir), committee)) - state1 := utils.OrPanic1(NewState(&Config{Committee: committee}, dw1)) + dw1 := utils.OrPanic1(NewDataWAL(utils.Some(dir), registry.FirstBlock())) + state1 := utils.OrPanic1(NewState(&Config{Registry: registry}, dw1)) require.NoError(t, state1.PushQC(ctx, qc1, blocks1)) require.NoError(t, dw1.CommitQCs.PersistQC(qc1)) for i, n := 0, gr1.First; n < gr1.Next; n++ { @@ -465,8 +476,8 @@ func TestReconcileCase3BlocksLost(t *testing.T) { require.NoError(t, os.RemoveAll(filepath.Join(dir, "globalblocks"))) // Second run: only QCs WAL survives. - dw2 := utils.OrPanic1(NewDataWAL(utils.Some(dir), committee)) - state2 := utils.OrPanic1(NewState(&Config{Committee: committee}, dw2)) + dw2 := utils.OrPanic1(NewDataWAL(utils.Some(dir), registry.FirstBlock())) + state2 := utils.OrPanic1(NewState(&Config{Registry: registry}, dw2)) // QCs loaded, blocks empty. The state needs blocks re-pushed. // Without the cursor sync fix, PushBlock here would fail with @@ -484,9 +495,9 @@ func TestReconcileCase3BlocksLost(t *testing.T) { } // State should accept the next QC normally. - qc2, blocks2 := TestCommitQC(rng, committee, keys, utils.Some(qc1.QC())) + qc2, blocks2 := TestCommitQC(rng, committee, keys, utils.Some(qc1.QC()), registry.FirstBlock(), time.Time{}) require.NoError(t, state2.PushQC(ctx, qc2, blocks2)) - require.Equal(t, qc2.QC().GlobalRange(committee).Next, state2.NextBlock()) + require.Equal(t, qc2.QC().GlobalRange().Next, state2.NextBlock()) require.NoError(t, dw2.Close()) } @@ -494,18 +505,19 @@ func TestReconcileCase4Normal(t *testing.T) { t.Log("Reconcile case 4: Normal (a=X, bX)") rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) + committee := registry.LatestCommittee() dir := t.TempDir() - qc1, blocks1 := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC]()) - qc2, blocks2 := TestCommitQC(rng, committee, keys, utils.Some(qc1.QC())) - gr1 := qc1.QC().GlobalRange(committee) - gr2 := qc2.QC().GlobalRange(committee) + qc1, blocks1 := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC](), registry.FirstBlock(), time.Time{}) + qc2, blocks2 := TestCommitQC(rng, committee, keys, utils.Some(qc1.QC()), registry.FirstBlock(), time.Time{}) + gr1 := qc1.QC().GlobalRange() + gr2 := qc2.QC().GlobalRange() // Persist both QCs and all blocks. - dw := utils.OrPanic1(NewDataWAL(utils.Some(dir), committee)) + dw := utils.OrPanic1(NewDataWAL(utils.Some(dir), registry.FirstBlock())) require.NoError(t, dw.CommitQCs.PersistQC(qc1)) require.NoError(t, dw.CommitQCs.PersistQC(qc2)) allBlocks := append(blocks1, blocks2...) @@ -642,8 +656,8 @@ func TestReconcileCase5BlocksAhead(t *testing.T) { // Reopen: blocks start at gr2.First, QCs start at gr1.First. // Reconcile should truncate QCs to match blocks. - dw2 := utils.OrPanic1(NewDataWAL(utils.Some(dir), committee)) - state := utils.OrPanic1(NewState(&Config{Committee: committee}, dw2)) + dw2 := utils.OrPanic1(NewDataWAL(utils.Some(dir), registry.FirstBlock())) + state := utils.OrPanic1(NewState(&Config{Registry: registry}, dw2)) for inner := range state.inner.Lock() { require.Equal(t, gr2.First, inner.first) @@ -665,18 +679,19 @@ func TestReconcileCase6QCsAhead(t *testing.T) { t.Log("Reconcile case 6: Prune crash, QCs ahead (a=Y)") rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) + committee := registry.LatestCommittee() dir := t.TempDir() // Build 2 sequential QCs. - qc1, blocks1 := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC]()) - qc2, blocks2 := TestCommitQC(rng, committee, keys, utils.Some(qc1.QC())) - gr1 := qc1.QC().GlobalRange(committee) - gr2 := qc2.QC().GlobalRange(committee) + qc1, blocks1 := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC](), registry.FirstBlock(), time.Time{}) + qc2, blocks2 := TestCommitQC(rng, committee, keys, utils.Some(qc1.QC()), registry.FirstBlock(), time.Time{}) + gr1 := qc1.QC().GlobalRange() + gr2 := qc2.QC().GlobalRange() // Persist only qc1 to QCs WAL but persist ALL blocks (qc1 + qc2) to blocks WAL. // This simulates blocks being persisted ahead of QCs. - dw1 := utils.OrPanic1(NewDataWAL(utils.Some(dir), committee)) + dw1 := utils.OrPanic1(NewDataWAL(utils.Some(dir), registry.FirstBlock())) require.NoError(t, dw1.CommitQCs.PersistQC(qc1)) allBlocks := append(blocks1, blocks2...) for i, n := 0, gr1.First; n < gr2.Next; n++ { @@ -739,8 +755,8 @@ func TestReconcileCase7BlocksPastQCs(t *testing.T) { // On recovery, only blocks within qc1's range should be loaded. // Blocks in qc2's range have no QC and should be ignored. - dw2 := utils.OrPanic1(NewDataWAL(utils.Some(dir), committee)) - state2 := utils.OrPanic1(NewState(&Config{Committee: committee}, dw2)) + dw2 := utils.OrPanic1(NewDataWAL(utils.Some(dir), registry.FirstBlock())) + state2 := utils.OrPanic1(NewState(&Config{Registry: registry}, dw2)) // Blocks in qc1's range should be available. for n := gr1.First; n < gr1.Next; n++ { @@ -768,18 +784,19 @@ func TestReconcileCase7BlocksTail(t *testing.T) { t.Log("Reconcile case 7: Persist crash, tail truncation with re-push") ctx := t.Context() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) + committee := registry.LatestCommittee() dir := t.TempDir() // Build 2 sequential QCs. - qc1, blocks1 := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC]()) - qc2, blocks2 := TestCommitQC(rng, committee, keys, utils.Some(qc1.QC())) - gr1 := qc1.QC().GlobalRange(committee) - gr2 := qc2.QC().GlobalRange(committee) + qc1, blocks1 := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC](), registry.FirstBlock(), time.Time{}) + qc2, blocks2 := TestCommitQC(rng, committee, keys, utils.Some(qc1.QC()), registry.FirstBlock(), time.Time{}) + gr1 := qc1.QC().GlobalRange() + gr2 := qc2.QC().GlobalRange() // Persist qc1 to both WALs, but only blocks (not QC) for qc2. // This simulates a crash during parallel persistence in runPersist. - dw1 := utils.OrPanic1(NewDataWAL(utils.Some(dir), committee)) + dw1 := utils.OrPanic1(NewDataWAL(utils.Some(dir), registry.FirstBlock())) require.NoError(t, dw1.CommitQCs.PersistQC(qc1)) allBlocks := append(blocks1, blocks2...) for i, n := 0, gr1.First; n < gr2.Next; n++ { @@ -789,12 +806,12 @@ func TestReconcileCase7BlocksTail(t *testing.T) { require.NoError(t, dw1.Close()) // Reopen: reconcile should truncate the blocks tail (qc2's blocks). - dw2 := utils.OrPanic1(NewDataWAL(utils.Some(dir), committee)) + dw2 := utils.OrPanic1(NewDataWAL(utils.Some(dir), registry.FirstBlock())) // Blocks persister cursor should now match QCs range. require.Equal(t, dw2.CommitQCs.Next(), dw2.Blocks.Next()) - state := utils.OrPanic1(NewState(&Config{Committee: committee}, dw2)) + state := utils.OrPanic1(NewState(&Config{Registry: registry}, dw2)) // qc1's blocks should be available. for n := gr1.First; n < gr1.Next; n++ { @@ -817,17 +834,18 @@ func TestReconcileCase8BlocksBehind(t *testing.T) { t.Log("Reconcile case 8: QCs ahead normal (b 0 { parent := bs[len(bs)-1] - return types.NewBlock( - producer, - parent.Header().Next(), - parent.Header().Hash(), - types.GenPayload(rng), - ) + return types.NewBlock(producer, parent.Header().Next(), parent.Header().Hash(), types.GenPayload(rng)) } - return types.NewBlock( - producer, - types.LaneRangeOpt(prev, producer).Next(), - types.GenBlockHeaderHash(rng), - types.GenPayload(rng), - ) + return types.NewBlock(producer, types.LaneRangeOpt(prev, producer).Next(), types.GenBlockHeaderHash(rng), types.GenPayload(rng)) } - // Make some blocks for range 10 { producer := committee.Lanes().At(rng.Intn(committee.Lanes().Len())) blocks[producer] = append(blocks[producer], makeBlock(producer)) } - // Construct a proposal. laneQCs := map[types.LaneID]*types.LaneQC{} var headers []*types.BlockHeader var blockList []*types.Block @@ -72,37 +62,16 @@ func TestCommitQC( } } } - viewSpec := types.ViewSpec{CommitQC: prev} - leader := committee.Leader(viewSpec.View()) - var leaderKey types.SecretKey - for _, k := range keys { - if k.Public() == leader { - leaderKey = k - break - } - } - proposal := utils.OrPanic1(types.NewProposal( - leaderKey, - committee, - viewSpec, - time.Now(), - laneQCs, - func() utils.Option[*types.AppQC] { - if n := types.GlobalRangeOpt(prev, committee).Next; n > 0 { - p := types.NewAppProposal(n-1, viewSpec.View().Index, types.GenAppHash(rng)) - return utils.Some(TestAppQC(keys, p)) - } - return utils.None[*types.AppQC]() - }(), - )) - votes := make([]*types.Signed[*types.CommitVote], 0, len(keys)) - for _, k := range keys { - votes = append(votes, types.Sign(k, types.NewCommitVote(proposal.Proposal().Msg()))) + var appQC utils.Option[*types.AppQC] + if cqc, ok := prev.Get(); ok { + vs := types.ViewSpec{CommitQC: prev, FirstBlock: firstBlock} + p := types.NewAppProposal(cqc.GlobalRange().Next-1, vs.View().Index, types.GenAppHash(rng)) + appQC = utils.Some(TestAppQC(keys, p)) + } else { + appQC = utils.None[*types.AppQC]() } - return types.NewFullCommitQC( - types.NewCommitQC(votes), - headers, - ), blockList + cqc := types.BuildCommitQC(committee, keys, prev, firstBlock, genesisTimestamp, laneQCs, appQC) + return types.NewFullCommitQC(cqc, headers), blockList } var _ StateAPI = (*MockState)(nil) diff --git a/sei-tendermint/internal/autobahn/epoch/registry.go b/sei-tendermint/internal/autobahn/epoch/registry.go new file mode 100644 index 0000000000..c0bb097d88 --- /dev/null +++ b/sei-tendermint/internal/autobahn/epoch/registry.go @@ -0,0 +1,92 @@ +package epoch + +import ( + "fmt" + "time" + + "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" +) + +// Index is the epoch number. +type Index uint64 + +// Registry is the authoritative source of committee and stake information. +// All layers (consensus, data, avail) read from it. +// +// Currently the committee is fixed at genesis. Dynamic committee support +// will be wired up when the execution layer is ready. +type Registry struct { + genesis *types.Committee + firstBlock types.GlobalBlockNumber + genesisTimestamp time.Time +} + +// NewRegistry creates a Registry with the genesis committee. +func NewRegistry( + weights map[types.PublicKey]uint64, + firstBlock types.GlobalBlockNumber, + genesisTimestamp time.Time, +) (*Registry, error) { + genesis, err := types.NewCommittee(weights) + if err != nil { + return nil, fmt.Errorf("genesis committee: %w", err) + } + return &Registry{ + genesis: genesis, + firstBlock: firstBlock, + genesisTimestamp: genesisTimestamp, + }, nil +} + +// FirstBlock returns the first global block number of the chain. +func (r *Registry) FirstBlock() types.GlobalBlockNumber { + return r.firstBlock +} + +// GenesisTimestamp returns the genesis timestamp of the chain. +func (r *Registry) GenesisTimestamp() time.Time { + return r.genesisTimestamp +} + +// EpochFor returns the epoch index for the given RoadIndex. +// Currently always returns 0 (genesis epoch); dynamic lookup will be added +// with the execution layer. +func (r *Registry) EpochFor(_ types.RoadIndex) Index { + return 0 +} + +// CommitteeFor returns the committee active at the given RoadIndex. +// Currently always returns the genesis committee; dynamic lookup will be +// added with the execution layer. +func (r *Registry) CommitteeFor(_ types.RoadIndex) *types.Committee { + return r.genesis +} + +// LatestCommittee returns the genesis committee. +func (r *Registry) LatestCommittee() *types.Committee { + return r.genesis +} + +// EpochWindow returns the epoch→committee map for message acceptance across +// epoch transitions. With a fixed genesis committee it always returns a +// single entry. +func (r *Registry) EpochWindow() map[Index]*types.Committee { + return map[Index]*types.Committee{0: r.genesis} +} + +// VerifyInWindow calls fn with each committee in the epoch window and returns +// nil as soon as any call succeeds. Returns the last error if all fail. +func (r *Registry) VerifyInWindow(fn func(*types.Committee) error) error { + var lastErr error + for _, c := range r.EpochWindow() { + if err := fn(c); err == nil { + return nil + } else { //nolint:revive + lastErr = err + } + } + if lastErr != nil { + return lastErr + } + return fmt.Errorf("empty epoch window") +} diff --git a/sei-tendermint/internal/autobahn/epoch/registry_test.go b/sei-tendermint/internal/autobahn/epoch/registry_test.go new file mode 100644 index 0000000000..fe3e0e3df7 --- /dev/null +++ b/sei-tendermint/internal/autobahn/epoch/registry_test.go @@ -0,0 +1,42 @@ +package epoch + +import ( + "testing" + "time" + + "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" +) + +func makeRegistry(t *testing.T) (*Registry, types.SecretKey) { + t.Helper() + rng := utils.TestRng() + key := types.GenSecretKey(rng) + weights := map[types.PublicKey]uint64{key.Public(): 10} + reg, err := NewRegistry(weights, 0, time.Now()) + if err != nil { + t.Fatalf("NewRegistry(): %v", err) + } + return reg, key +} + +func TestRegistry_CommitteeForAlwaysReturnsGenesis(t *testing.T) { + reg, key := makeRegistry(t) + + for _, r := range []types.RoadIndex{0, 50, 99, 100, 199} { + c := reg.CommitteeFor(r) + if c == nil { + t.Fatalf("CommitteeFor(%d) = nil", r) + } + if !c.HasReplica(key.Public()) { + t.Errorf("CommitteeFor(%d): genesis key not in committee", r) + } + } +} + +func TestNewRegistry_RejectsEmptyWeights(t *testing.T) { + _, err := NewRegistry(map[types.PublicKey]uint64{}, 0, time.Now()) + if err == nil { + t.Fatal("NewRegistry() succeeded with empty weights, want error") + } +} diff --git a/sei-tendermint/internal/autobahn/epoch/testonly.go b/sei-tendermint/internal/autobahn/epoch/testonly.go new file mode 100644 index 0000000000..cfcda1544e --- /dev/null +++ b/sei-tendermint/internal/autobahn/epoch/testonly.go @@ -0,0 +1,22 @@ +package epoch + +import ( + "time" + + "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" +) + +// GenRegistry generates a random Registry of the given committee size. +// Returns the generated secret keys as well. +// Intended for use in tests only. +func GenRegistry(rng utils.Rng, size int) (*Registry, []types.SecretKey) { + sks := utils.GenSliceN(rng, size, types.GenSecretKey) + weights := map[types.PublicKey]uint64{} + for _, sk := range sks { + weights[sk.Public()] = 1000 + uint64(rng.Intn(1000)) //nolint:gosec + } + firstBlock := types.GenGlobalBlockNumber(rng) % 1000000 + registry := utils.OrPanic1(NewRegistry(weights, firstBlock, time.Now())) + return registry, sks +} diff --git a/sei-tendermint/internal/autobahn/pb/autobahn.pb.go b/sei-tendermint/internal/autobahn/pb/autobahn.pb.go index 18192fbb6b..ba39c86c98 100644 --- a/sei-tendermint/internal/autobahn/pb/autobahn.pb.go +++ b/sei-tendermint/internal/autobahn/pb/autobahn.pb.go @@ -835,10 +835,11 @@ func (x *View) GetNumber() uint64 { type Proposal struct { state protoimpl.MessageState `protogen:"open.v1"` - View *View `protobuf:"bytes,1,opt,name=view,proto3,oneof" json:"view,omitempty"` // required. - Timestamp *Timestamp `protobuf:"bytes,5,opt,name=timestamp,proto3,oneof" json:"timestamp,omitempty"` // required - LaneRanges []*LaneRange `protobuf:"bytes,3,rep,name=lane_ranges,json=laneRanges,proto3" json:"lane_ranges,omitempty"` // Sorted by lane. - App *AppProposal `protobuf:"bytes,4,opt,name=app,proto3,oneof" json:"app,omitempty"` // optional + View *View `protobuf:"bytes,1,opt,name=view,proto3,oneof" json:"view,omitempty"` // required. + Timestamp *Timestamp `protobuf:"bytes,5,opt,name=timestamp,proto3,oneof" json:"timestamp,omitempty"` // required + LaneRanges []*LaneRange `protobuf:"bytes,3,rep,name=lane_ranges,json=laneRanges,proto3" json:"lane_ranges,omitempty"` // Sorted by lane. + App *AppProposal `protobuf:"bytes,4,opt,name=app,proto3,oneof" json:"app,omitempty"` // optional + GlobalFirst *uint64 `protobuf:"varint,6,opt,name=global_first,json=globalFirst,proto3,oneof" json:"global_first,omitempty"` // genesis InitialHeight; added to lane block numbers to produce absolute global block numbers unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -901,6 +902,13 @@ func (x *Proposal) GetApp() *AppProposal { return nil } +func (x *Proposal) GetGlobalFirst() uint64 { + if x != nil && x.GlobalFirst != nil { + return *x.GlobalFirst + } + return 0 +} + type FullProposal struct { state protoimpl.MessageState `protogen:"open.v1"` ProposalV2 *SignedProposal `protobuf:"bytes,5,opt,name=proposal_v2,json=proposalV2,proto3" json:"proposal_v2,omitempty"` @@ -2242,17 +2250,19 @@ const file_autobahn_autobahn_proto_rawDesc = "" + "\x05index\x18\x01 \x01(\x04H\x00R\x05index\x88\x01\x01\x12\x1b\n" + "\x06number\x18\x02 \x01(\x04H\x01R\x06number\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\b\n" + "\x06_indexB\t\n" + - "\a_number\"\x96\x02\n" + + "\a_number\"\xcf\x02\n" + "\bProposal\x12'\n" + "\x04view\x18\x01 \x01(\v2\x0e.autobahn.ViewH\x00R\x04view\x88\x01\x01\x126\n" + "\ttimestamp\x18\x05 \x01(\v2\x13.autobahn.TimestampH\x01R\ttimestamp\x88\x01\x01\x12<\n" + "\vlane_ranges\x18\x03 \x03(\v2\x13.autobahn.LaneRangeB\x06Ј\xe2\xab\fdR\n" + "laneRanges\x12,\n" + - "\x03app\x18\x04 \x01(\v2\x15.autobahn.AppProposalH\x02R\x03app\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\a\n" + + "\x03app\x18\x04 \x01(\v2\x15.autobahn.AppProposalH\x02R\x03app\x88\x01\x01\x12&\n" + + "\fglobal_first\x18\x06 \x01(\x04H\x03R\vglobalFirst\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\a\n" + "\x05_viewB\f\n" + "\n" + "_timestampB\x06\n" + - "\x04_appJ\x04\b\x02\x10\x03R\n" + + "\x04_appB\x0f\n" + + "\r_global_firstJ\x04\b\x02\x10\x03R\n" + "created_at\"\x96\x02\n" + "\fFullProposal\x129\n" + "\vproposal_v2\x18\x05 \x01(\v2\x18.autobahn.SignedProposalR\n" + diff --git a/sei-tendermint/internal/autobahn/pb/autobahn.wireguard.go b/sei-tendermint/internal/autobahn/pb/autobahn.wireguard.go index 19a860a431..8753191b80 100644 --- a/sei-tendermint/internal/autobahn/pb/autobahn.wireguard.go +++ b/sei-tendermint/internal/autobahn/pb/autobahn.wireguard.go @@ -44,23 +44,23 @@ func (*View) MaxSize() int { } func (*Proposal) MaxSize() int { - return 9506 + return 9517 } func (*FullProposal) MaxSize() int { - return 1106394 + return 1106416 } func (*PrepareQC) MaxSize() int { - return 19909 + return 19920 } func (*CommitQC) MaxSize() int { - return 19909 + return 19920 } func (*FullCommitQC) MaxSize() int { - return 31613 + return 31624 } func (*TimeoutVote) MaxSize() int { @@ -68,11 +68,11 @@ func (*TimeoutVote) MaxSize() int { } func (*TimeoutQC) MaxSize() int { - return 34313 + return 34324 } func (*FullTimeoutVote) MaxSize() int { - return 20057 + return 20068 } func (*AppQC) MaxSize() int { @@ -88,7 +88,7 @@ func (*Msg) MaxSize() int { } func (*SignedProposal) MaxSize() int { - return 9613 + return 9624 } func (*SignedTimeoutVote) MaxSize() int { @@ -112,7 +112,7 @@ func (*SignedAppProposal) MaxSize() int { } func (*ConsensusReq) MaxSize() int { - return 1106398 + return 1106420 } func init() { @@ -215,6 +215,7 @@ func init() { 5: {MaxCount: 1, Nested: utils.Some(reflect.TypeFor[*Timestamp]())}, 3: {MaxCount: 100, Nested: utils.Some(reflect.TypeFor[*LaneRange]())}, 4: {MaxCount: 1, Nested: utils.Some(reflect.TypeFor[*AppProposal]())}, + 6: {MaxCount: 1}, }) // Register the wireguard.Schema generated for autobahn.FullProposal. diff --git a/sei-tendermint/internal/autobahn/producer/mempool_test.go b/sei-tendermint/internal/autobahn/producer/mempool_test.go index 3b83fc77f7..3366ce9ddb 100644 --- a/sei-tendermint/internal/autobahn/producer/mempool_test.go +++ b/sei-tendermint/internal/autobahn/producer/mempool_test.go @@ -16,6 +16,7 @@ import ( "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/autobahn/epoch" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/proxy" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" @@ -181,7 +182,8 @@ func (env *testEnv) Run(ctx context.Context) error { s.Spawn(func() error { return env.state.Run(ctx) }) // Process blocks. stats := blockStats{} - for i := env.data.Committee().FirstBlock(); ; i += 1 { + firstBlock := env.data.Registry().FirstBlock() + for i := firstBlock; ; i += 1 { // Wait for the next block to be finalized. b, err := env.data.GlobalBlock(ctx, i) if err != nil { @@ -189,7 +191,7 @@ func (env *testEnv) Run(ctx context.Context) error { } // Check that adding first transaction to the previous block would exceed the limit. - if i > env.data.Committee().FirstBlock() { + if i > firstBlock { tx, err := decodeTxSpec(b.Payload.Txs()[0]) if err != nil { return fmt.Errorf("decodeTxSpec(): %w", err) @@ -228,10 +230,10 @@ func (env *testEnv) Run(ctx context.Context) error { } func newTestEnv(rng utils.Rng, cfg *Config, app *proxy.Proxy) *testEnv { - committee, keys := types.GenCommittee(rng, 1) + registry, keys := epoch.GenRegistry(rng, 1) dataState := utils.OrPanic1(data.NewState( - &data.Config{Committee: committee}, - utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)), + &data.Config{Registry: registry}, + utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())), )) consensusState := utils.OrPanic1(consensus.NewState(&consensus.Config{ Key: keys[0], diff --git a/sei-tendermint/internal/p2p/giga/avail_test.go b/sei-tendermint/internal/p2p/giga/avail_test.go index 2406af6350..b534b31af7 100644 --- a/sei-tendermint/internal/p2p/giga/avail_test.go +++ b/sei-tendermint/internal/p2p/giga/avail_test.go @@ -8,6 +8,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/avail" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/consensus" + "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" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/scope" @@ -16,8 +17,9 @@ import ( func TestAvailClientServer(t *testing.T) { ctx := t.Context() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) - env := newTestEnv(committee) + registry, keys := epoch.GenRegistry(rng, 4) + committee := registry.LatestCommittee() + env := newTestEnv(registry) var nodes []*testNode activeKeys := keys[:3] // keys are sorted by weight, so that's ok. totalWeight := uint64(0) @@ -31,7 +33,7 @@ func TestAvailClientServer(t *testing.T) { } totalBlocks := 3 * avail.BlocksPerLane - firstBlock := committee.FirstBlock() + firstBlock := nodes[0].data.Registry().FirstBlock() if err := scope.Run(ctx, func(ctx context.Context, s scope.Scope) error { t.Log("Spawn network.") s.SpawnBg(func() error { return env.Run(ctx) }) diff --git a/sei-tendermint/internal/p2p/giga/consensus_test.go b/sei-tendermint/internal/p2p/giga/consensus_test.go index 08eb75f77d..f2da12547f 100644 --- a/sei-tendermint/internal/p2p/giga/consensus_test.go +++ b/sei-tendermint/internal/p2p/giga/consensus_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" + "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/scope" ) @@ -13,14 +14,15 @@ import ( func TestConsensusClientServer(t *testing.T) { ctx := t.Context() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 7) - firstBlock := committee.FirstBlock() - env := newTestEnv(committee) + registry, keys := epoch.GenRegistry(rng, 7) + committee := registry.LatestCommittee() + env := newTestEnv(registry) // Run only a subset of replicas, to enforce timeouts. var nodes []*testNode for _, key := range types.TestKeysWithWeight(committee, keys, committee.CommitQuorum()) { nodes = append(nodes, env.AddNode(key)) } + firstBlock := nodes[0].data.Registry().FirstBlock() if err := scope.Run(ctx, func(ctx context.Context, s scope.Scope) error { s.SpawnBg(func() error { return env.Run(ctx) }) var wantAppProposal utils.Option[*types.AppProposal] @@ -55,7 +57,7 @@ func TestConsensusClientServer(t *testing.T) { if err != nil { return fmt.Errorf("ds.QC(): %w", err) } - want.Timestamp = qc.QC().Proposal().BlockTimestamp(committee, idx).OrPanic("global block not in QC") + want.Timestamp = qc.QC().Proposal().BlockTimestamp(idx).OrPanic("global block not in QC") if err := utils.TestDiff(want, got); err != nil { return err } diff --git a/sei-tendermint/internal/p2p/giga/data_test.go b/sei-tendermint/internal/p2p/giga/data_test.go index f5249517b9..dead3a5ea0 100644 --- a/sei-tendermint/internal/p2p/giga/data_test.go +++ b/sei-tendermint/internal/p2p/giga/data_test.go @@ -9,6 +9,7 @@ import ( "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/autobahn/epoch" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/conn" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/rpc" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" @@ -23,8 +24,8 @@ type testNode struct { func defaultViewTimeout(view types.View) time.Duration { return time.Hour } -func newTestNode(committee *types.Committee, cfg *consensus.Config) *testNode { - dataState := utils.OrPanic1(data.NewState(&data.Config{Committee: committee}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), committee)))) +func newTestNode(registry *epoch.Registry, cfg *consensus.Config) *testNode { + dataState := utils.OrPanic1(data.NewState(&data.Config{Registry: registry}, utils.OrPanic1(data.NewDataWAL(utils.None[string](), registry.FirstBlock())))) consensusState, err := consensus.NewState(cfg, dataState) if err != nil { panic(fmt.Sprintf("consensus.NewState(): %v", err)) @@ -46,17 +47,18 @@ func (n *testNode) Run(ctx context.Context) error { } type testEnv struct { + registry *epoch.Registry committee *types.Committee nodes map[types.PublicKey]*testNode } -func newTestEnv(committee *types.Committee) *testEnv { - return &testEnv{committee, map[types.PublicKey]*testNode{}} +func newTestEnv(registry *epoch.Registry) *testEnv { + return &testEnv{registry, registry.LatestCommittee(), map[types.PublicKey]*testNode{}} } // Call AddNode BEFORE Run. func (e *testEnv) AddNode(key types.SecretKey) *testNode { - n := newTestNode(e.committee, &consensus.Config{ + n := newTestNode(e.registry, &consensus.Config{ Key: key, ViewTimeout: func(view types.View) time.Duration { if _, ok := e.nodes[e.committee.Leader(view)]; ok { @@ -90,11 +92,12 @@ func (e *testEnv) Run(ctx context.Context) error { func TestDataClientServer(t *testing.T) { ctx := t.Context() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 2) - firstBlock := committee.FirstBlock() - env := newTestEnv(committee) + registry, keys := epoch.GenRegistry(rng, 2) + env := newTestEnv(registry) + committee := env.committee server := env.AddNode(keys[0]) client := env.AddNode(keys[1]) + firstBlock := server.data.Registry().FirstBlock() if err := scope.Run(ctx, func(ctx context.Context, s scope.Scope) error { s.SpawnBg(func() error { return env.Run(ctx) }) @@ -102,7 +105,7 @@ func TestDataClientServer(t *testing.T) { prev := utils.None[*types.CommitQC]() for i := range 3 { t.Logf("iteration %v", i) - qc, blocks := data.TestCommitQC(rng, committee, keys, prev) + qc, blocks := data.TestCommitQC(rng, committee, keys, prev, server.data.Registry().FirstBlock(), time.Time{}) if err := server.data.PushQC(ctx, qc, blocks); err != nil { return fmt.Errorf("serverState.PushQC(): %w", err) } diff --git a/sei-tendermint/internal/p2p/giga/pb/api.wireguard.go b/sei-tendermint/internal/p2p/giga/pb/api.wireguard.go index 3ef12ec7ac..41ef7b3dac 100644 --- a/sei-tendermint/internal/p2p/giga/pb/api.wireguard.go +++ b/sei-tendermint/internal/p2p/giga/pb/api.wireguard.go @@ -41,7 +41,7 @@ func (*StreamAppQCsReq) MaxSize() int { } func (*StreamAppQCsResp) MaxSize() int { - return 30374 + return 30385 } func (*StreamCommitQCsReq) MaxSize() int { diff --git a/sei-tendermint/internal/p2p/giga_router_common.go b/sei-tendermint/internal/p2p/giga_router_common.go index 9ef33db906..70a3e18be6 100644 --- a/sei-tendermint/internal/p2p/giga_router_common.go +++ b/sei-tendermint/internal/p2p/giga_router_common.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "maps" "net/url" "slices" "sync/atomic" @@ -14,6 +13,7 @@ import ( atypes "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" "github.com/sei-protocol/sei-chain/sei-tendermint/crypto" "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/internal/p2p/giga" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/rpc" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/proxy" @@ -63,19 +63,20 @@ func buildDataState(cfg *GigaRouterCommonConfig) (*data.State, error) { if cfg.MaxInboundFullnodePeers < 0 || cfg.MaxInboundFullnodePeers > maxInboundFullnodePeers { return nil, fmt.Errorf("GigaRouterCommonConfig.MaxInboundFullnodePeers = %v, want 0..%v", cfg.MaxInboundFullnodePeers, maxInboundFullnodePeers) } - committee, err := atypes.NewRoundRobinElection( - slices.Collect(maps.Keys(cfg.ValidatorAddrs)), - atypes.GlobalBlockNumber(cfg.GenDoc.InitialHeight), // nolint:gosec // verified to be positive. - cfg.GenDoc.GenesisTime, - ) + firstBlock := atypes.GlobalBlockNumber(cfg.GenDoc.InitialHeight) // nolint:gosec // verified to be positive. + genesisWeights := map[atypes.PublicKey]uint64{} + for k := range cfg.ValidatorAddrs { + genesisWeights[k] = 1 + } + registry, err := epoch.NewRegistry(genesisWeights, firstBlock, cfg.GenDoc.GenesisTime) if err != nil { - return nil, fmt.Errorf("atypes.NewRoundRobinElection(): %w", err) + return nil, fmt.Errorf("epoch.NewRegistry(): %w", err) } - dataWAL, err := data.NewDataWAL(cfg.PersistentStateDir, committee) + dataWAL, err := data.NewDataWAL(cfg.PersistentStateDir, registry.FirstBlock()) if err != nil { return nil, fmt.Errorf("data.NewDataWAL(): %w", err) } - dataState, err := data.NewState(&data.Config{Committee: committee}, dataWAL) + dataState, err := data.NewState(&data.Config{Registry: registry}, dataWAL) if err != nil { return nil, fmt.Errorf("data.NewState(): %w", err) } @@ -393,6 +394,6 @@ func (r *gigaRouterCommon) RunInboundConn(ctx context.Context, hConn *handshaked // None if the caller should handle it locally. Overridden on // *gigaValidatorRouter to short-circuit self-shard sends. func (r *gigaRouterCommon) EvmProxy(sender common.Address) utils.Option[*url.URL] { - shardValidator := r.data.Committee().EvmShard(sender) + shardValidator := r.data.Registry().LatestCommittee().EvmShard(sender) return utils.Some(r.cfg.ValidatorAddrs[shardValidator].EVMRPC) } diff --git a/sei-tendermint/internal/p2p/giga_router_fullnode_test.go b/sei-tendermint/internal/p2p/giga_router_fullnode_test.go index ce5a64d778..f0262a5d01 100644 --- a/sei-tendermint/internal/p2p/giga_router_fullnode_test.go +++ b/sei-tendermint/internal/p2p/giga_router_fullnode_test.go @@ -82,7 +82,7 @@ func TestGigaRouter_Fullnode(t *testing.T) { returnedRemoteURLs := map[string]struct{}{} for range 200 { sender := common.BytesToAddress(utils.GenBytes(rng, common.AddressLength)) - shardValidator := router.data.Committee().EvmShard(sender) + shardValidator := router.data.Registry().LatestCommittee().EvmShard(sender) expectedURL := urlByValidator[shardValidator] proxyURL, ok := router.EvmProxy(sender).Get() require.True(t, ok) diff --git a/sei-tendermint/internal/p2p/giga_router_validator.go b/sei-tendermint/internal/p2p/giga_router_validator.go index 4c65cd1538..d6107e3b38 100644 --- a/sei-tendermint/internal/p2p/giga_router_validator.go +++ b/sei-tendermint/internal/p2p/giga_router_validator.go @@ -89,7 +89,7 @@ func (r *gigaValidatorRouter) Run(ctx context.Context) error { // EvmProxy on the validator returns None when the sender's shard owner is // us (handle locally via mempool, no HTTP round-trip to self). func (r *gigaValidatorRouter) EvmProxy(sender common.Address) utils.Option[*url.URL] { - shardValidator := r.data.Committee().EvmShard(sender) + shardValidator := r.data.Registry().LatestCommittee().EvmShard(sender) if r.validatorKey == shardValidator { return utils.None[*url.URL]() } diff --git a/sei-tendermint/internal/p2p/giga_router_validator_test.go b/sei-tendermint/internal/p2p/giga_router_validator_test.go index 8f971356b4..790c98cd2b 100644 --- a/sei-tendermint/internal/p2p/giga_router_validator_test.go +++ b/sei-tendermint/internal/p2p/giga_router_validator_test.go @@ -256,7 +256,7 @@ func TestGigaRouter_EvmProxy(t *testing.T) { for range 200 { sender := common.BytesToAddress(utils.GenBytes(rng, common.AddressLength)) - shardValidator := router.data.Committee().EvmShard(sender) + shardValidator := router.data.Registry().LatestCommittee().EvmShard(sender) proxyURL, ok := router.EvmProxy(sender).Get() expectedURL := urlByValidator[shardValidator]