diff --git a/.gitignore b/.gitignore index 488a96ce20..0f78674fd9 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,4 @@ sei-db/state_db/bench/cryptosim/data/** sei-db/state_db/bench/cryptosim/bin/ sei-db/state_db/bench/cryptosim/logs/ sei-db/ledger_db/block/blocksim/bin/ -sei-db/db_engine/litt/bin/ \ No newline at end of file +sei-db/db_engine/litt/bin/*.test diff --git a/sei-db/ledger_db/block/block_db_test.go b/sei-db/ledger_db/block/block_db_test.go index 3b905cd4f5..33aaeb4bfd 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), cqc.Proposal().EpochIndex()) + 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..712db2fa0c 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} } @@ -132,7 +132,7 @@ func (g *BlockGenerator) buildFullCommitQC() (*types.FullCommitQC, []*types.Bloc } } - viewSpec := types.ViewSpec{CommitQC: prev} + viewSpec := types.ViewSpec{CommitQC: prev, Epoch: types.NewEpoch(0, types.OpenRoadRange(), genesisTime, committee, 0)} leader := committee.Leader(viewSpec.View()) var leaderKey types.SecretKey for _, k := range keys { @@ -143,13 +143,13 @@ func (g *BlockGenerator) buildFullCommitQC() (*types.FullCommitQC, []*types.Bloc } 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)) + if cqc, ok := prev.Get(); ok { + n := cqc.GlobalRange().Next + p := types.NewAppProposal(n-1, viewSpec.View().Index, types.GenAppHash(rng), viewSpec.Epoch.EpochIndex()) return utils.Some(testAppQC(keys, p)) } return utils.None[*types.AppQC]() 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..98a280ea58 100644 --- a/sei-tendermint/autobahn/types/app_proposal.go +++ b/sei-tendermint/autobahn/types/app_proposal.go @@ -18,11 +18,12 @@ type AppProposal struct { globalNumber GlobalBlockNumber roadIndex RoadIndex appHash AppHash + epochIndex uint64 } // NewAppProposal creates a new AppProposal. -func NewAppProposal(globalNumber GlobalBlockNumber, roadIndex RoadIndex, appHash AppHash) *AppProposal { - return &AppProposal{globalNumber: globalNumber, roadIndex: roadIndex, appHash: appHash} +func NewAppProposal(globalNumber GlobalBlockNumber, roadIndex RoadIndex, appHash AppHash, epochIndex uint64) *AppProposal { + return &AppProposal{globalNumber: globalNumber, roadIndex: roadIndex, appHash: appHash, epochIndex: epochIndex} } // GlobalNumber . @@ -34,6 +35,9 @@ func (m *AppProposal) RoadIndex() RoadIndex { return m.roadIndex } // AppHash . func (m *AppProposal) AppHash() AppHash { return m.appHash } +// EpochIndex returns the epoch this proposal belongs to. +func (m *AppProposal) EpochIndex() uint64 { return m.epochIndex } + // Next is the next global block number to compute AppHash for. func (m *AppProposal) Next() RoadIndex { return m.RoadIndex() + 1 @@ -44,9 +48,12 @@ 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) } + if got, want := m.EpochIndex(), qc.Proposal().EpochIndex(); got != want { + return fmt.Errorf("epoch_index = %d, want %d", got, want) + } return nil } @@ -57,6 +64,7 @@ var AppProposalConv = protoutils.Conv[*AppProposal, *pb.AppProposal]{ GlobalNumber: utils.Alloc(uint64(m.globalNumber)), RoadIndex: utils.Alloc(uint64(m.roadIndex)), AppHash: m.appHash, + EpochIndex: utils.Alloc(m.epochIndex), } }, Decode: func(m *pb.AppProposal) (*AppProposal, error) { @@ -66,10 +74,14 @@ var AppProposalConv = protoutils.Conv[*AppProposal, *pb.AppProposal]{ if m.RoadIndex == nil { return nil, fmt.Errorf("RoadIndex: missing") } + if m.EpochIndex == nil { + return nil, fmt.Errorf("EpochIndex: missing") + } return &AppProposal{ globalNumber: GlobalBlockNumber(*m.GlobalNumber), roadIndex: RoadIndex(*m.RoadIndex), appHash: AppHash(m.AppHash), + epochIndex: *m.EpochIndex, }, nil }, } diff --git a/sei-tendermint/autobahn/types/commit_qc.go b/sei-tendermint/autobahn/types/commit_qc.go index 3f2562cc78..7bf841f727 100644 --- a/sei-tendermint/autobahn/types/commit_qc.go +++ b/sei-tendermint/autobahn/types/commit_qc.go @@ -41,13 +41,21 @@ 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. -// Currently it doesn't require the previous CommitQC. -func (m *CommitQC) Verify(c *Committee) error { +// Verify verifies the CommitQC against the epoch. +func (m *CommitQC) Verify(ep *Epoch) error { + p := m.Proposal() + if err := p.Verify(ep); err != nil { + return err + } + roads := ep.Roads() + if p.Index() < roads.First || p.Index() > roads.Last { + return fmt.Errorf("road_index %v not in epoch roads [%v, %v]", p.Index(), roads.First, roads.Last) + } + c := ep.Committee() return m.vote.verifyQC(c, c.CommitQuorum(), m.sigs) } @@ -60,7 +68,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} @@ -77,16 +85,16 @@ func (m *FullCommitQC) Index() RoadIndex { return m.qc.Index() } -// Verify verifies the FullCommitQC against the committee. -func (m *FullCommitQC) Verify(c *Committee) error { - if err := m.qc.Verify(c); err != nil { +// Verify verifies the FullCommitQC against the epoch. +func (m *FullCommitQC) Verify(ep *Epoch) error { + if err := m.qc.Verify(ep); err != nil { 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() { + for lane := range ep.Committee().Lanes().All() { lr := m.qc.LaneRange(lane) if lr.Len() == 0 { continue 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..f7fbed8472 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,114 +61,121 @@ 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") } } -func makeCommittee() (*Committee, []SecretKey) { +func makeEpoch(rng utils.Rng) (*Epoch, []SecretKey) { keys := []SecretKey{ TestSecretKey("heavy"), TestSecretKey("light1"), TestSecretKey("light2"), } - return utils.OrPanic1(NewCommittee(map[PublicKey]uint64{ + committee := utils.OrPanic1(NewCommittee(map[PublicKey]uint64{ keys[0].Public(): 5, keys[1].Public(): 1, keys[2].Public(): 1, - }, 0, time.Now())), keys + })) + return GenEpochWithCommittee(rng, committee), keys } func TestLaneQCVerifyChecksWeight(t *testing.T) { rng := utils.TestRng() - committee, keys := makeCommittee() + ep, keys := makeEpoch(rng) vote := NewLaneVote(NewBlock(keys[0].Public(), 0, GenBlockHeaderHash(rng), GenPayload(rng)).Header()) heavyOnly := NewLaneQC([]*Signed[*LaneVote]{ Sign(keys[0], vote), }) - require.NoError(t, heavyOnly.Verify(committee)) + require.NoError(t, heavyOnly.Verify(ep.Committee())) lightMajority := NewLaneQC([]*Signed[*LaneVote]{ Sign(keys[1], vote), Sign(keys[2], vote), }) - require.Error(t, lightMajority.Verify(committee)) + require.Error(t, lightMajority.Verify(ep.Committee())) } func TestPrepareQCVerifyChecksWeight(t *testing.T) { rng := utils.TestRng() - committee, keys := makeCommittee() - vote := NewPrepareVote(GenProposalAt(rng, View{})) + ep, keys := makeEpoch(rng) + vote := NewPrepareVote(ProposalAt(ep, View{})) heavyOnly := NewPrepareQC([]*Signed[*PrepareVote]{ Sign(keys[0], vote), }) - require.NoError(t, heavyOnly.Verify(committee)) + require.NoError(t, heavyOnly.Verify(ep)) lightMajority := NewPrepareQC([]*Signed[*PrepareVote]{ Sign(keys[1], vote), Sign(keys[2], vote), }) - require.Error(t, lightMajority.Verify(committee)) + require.Error(t, lightMajority.Verify(ep)) } func TestCommitQCVerifyChecksWeight(t *testing.T) { rng := utils.TestRng() - committee, keys := makeCommittee() - vote := NewCommitVote(GenProposalAt(rng, View{})) + ep, keys := makeEpoch(rng) + vote := NewCommitVote(ProposalAt(ep, View{})) heavyOnly := NewCommitQC([]*Signed[*CommitVote]{ Sign(keys[0], vote), }) - require.NoError(t, heavyOnly.Verify(committee)) + require.NoError(t, heavyOnly.Verify(ep)) lightMajority := NewCommitQC([]*Signed[*CommitVote]{ Sign(keys[1], vote), Sign(keys[2], vote), }) - require.Error(t, lightMajority.Verify(committee)) + require.Error(t, lightMajority.Verify(ep)) } func TestAppQCVerifyChecksWeight(t *testing.T) { rng := utils.TestRng() - committee, keys := makeCommittee() - vote := NewAppVote(NewAppProposal(0, 0, GenAppHash(rng))) + ep, keys := makeEpoch(rng) + vote := NewAppVote(NewAppProposal(0, 0, GenAppHash(rng), 0)) heavyOnly := NewAppQC([]*Signed[*AppVote]{ Sign(keys[0], vote), }) - require.NoError(t, heavyOnly.Verify(committee)) + require.NoError(t, heavyOnly.Verify(ep.Committee())) lightMajority := NewAppQC([]*Signed[*AppVote]{ Sign(keys[1], vote), Sign(keys[2], vote), }) - require.Error(t, lightMajority.Verify(committee)) + require.Error(t, lightMajority.Verify(ep.Committee())) } func TestTimeoutQCVerifyChecksWeight(t *testing.T) { - committee, keys := makeCommittee() + rng := utils.TestRng() + ep, keys := makeEpoch(rng) view := View{} heavyOnly := NewTimeoutQC([]*FullTimeoutVote{ - NewFullTimeoutVote(keys[0], view, utils.None[*PrepareQC]()), + NewFullTimeoutVote(keys[0], view, utils.None[*PrepareQC](), ep.EpochIndex()), }) - if err := heavyOnly.Verify(committee, utils.None[*CommitQC]()); err != nil { + if err := heavyOnly.Verify(ep, utils.None[*CommitQC]()); err != nil { t.Fatalf("heavyOnly.Verify(): %v", err) } lightMajority := NewTimeoutQC([]*FullTimeoutVote{ - NewFullTimeoutVote(keys[1], view, utils.None[*PrepareQC]()), - NewFullTimeoutVote(keys[2], view, utils.None[*PrepareQC]()), + NewFullTimeoutVote(keys[1], view, utils.None[*PrepareQC](), ep.EpochIndex()), + NewFullTimeoutVote(keys[2], view, utils.None[*PrepareQC](), ep.EpochIndex()), }) - if err := lightMajority.Verify(committee, utils.None[*CommitQC]()); err == nil { + if err := lightMajority.Verify(ep, utils.None[*CommitQC]()); err == nil { t.Fatal("lightMajority.Verify() succeeded, want error") } } + +func TestNewCommittee_RejectsEmptyWeights(t *testing.T) { + _, err := NewCommittee(map[PublicKey]uint64{}) + if err == nil { + t.Fatal("NewCommittee() succeeded with empty weights, want error") + } +} diff --git a/sei-tendermint/autobahn/types/epoch.go b/sei-tendermint/autobahn/types/epoch.go new file mode 100644 index 0000000000..5e783cecf3 --- /dev/null +++ b/sei-tendermint/autobahn/types/epoch.go @@ -0,0 +1,46 @@ +package types + +import ( + "math" + "time" + + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" +) + +// RoadRange is an inclusive range of RoadIndex values [First, Last]. +type RoadRange struct { + First RoadIndex + Last RoadIndex +} + +// OpenRoadRange returns a RoadRange covering all road indices from 0. +// Use in tests and genesis epochs where no upper bound is known yet. +func OpenRoadRange() RoadRange { return RoadRange{First: 0, Last: math.MaxUint64} } + +// Epoch holds the complete context for a single epoch. +// Retrieved from the local Registry; never transmitted on the wire. +type Epoch struct { + utils.ReadOnly + epochIndex uint64 + roads RoadRange + firstTimestamp time.Time + committee *Committee + firstBlock GlobalBlockNumber +} + +// NewEpoch constructs an Epoch. +func NewEpoch(index uint64, roads RoadRange, firstTimestamp time.Time, committee *Committee, firstBlock GlobalBlockNumber) *Epoch { + return &Epoch{ + epochIndex: index, + roads: roads, + firstTimestamp: firstTimestamp, + committee: committee, + firstBlock: firstBlock, + } +} + +func (e *Epoch) EpochIndex() uint64 { return e.epochIndex } +func (e *Epoch) Roads() RoadRange { return e.roads } +func (e *Epoch) FirstTimestamp() time.Time { return e.firstTimestamp } +func (e *Epoch) Committee() *Committee { return e.committee } +func (e *Epoch) FirstBlock() GlobalBlockNumber { return e.firstBlock } 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/prepare_qc.go b/sei-tendermint/autobahn/types/prepare_qc.go index 7df064988b..0b172ccf74 100644 --- a/sei-tendermint/autobahn/types/prepare_qc.go +++ b/sei-tendermint/autobahn/types/prepare_qc.go @@ -36,9 +36,17 @@ func (m *PrepareQC) View() View { return m.vote.Msg().Proposal().View() } -// Verify verifies the PrepareQC against the committee. -// Currently it doesn't require the previous CommitQC. -func (m *PrepareQC) Verify(c *Committee) error { +// Verify verifies the PrepareQC against the epoch. +func (m *PrepareQC) Verify(ep *Epoch) error { + p := m.Proposal() + if err := p.Verify(ep); err != nil { + return err + } + roads := ep.Roads() + if p.Index() < roads.First || p.Index() > roads.Last { + return fmt.Errorf("road_index %v not in epoch roads [%v, %v]", p.Index(), roads.First, roads.Last) + } + c := ep.Committee() return m.vote.verifyQC(c, c.PrepareQuorum(), m.sigs) } diff --git a/sei-tendermint/autobahn/types/proposal.go b/sei-tendermint/autobahn/types/proposal.go index 5630839a21..025b998a99 100644 --- a/sei-tendermint/autobahn/types/proposal.go +++ b/sei-tendermint/autobahn/types/proposal.go @@ -106,13 +106,24 @@ func (v View) Next() View { return v } -// ViewSpec is a justification to start a given view. +// ViewSpec is the full local context for starting a view: justification QCs plus +// the epoch active at that view. 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] + Epoch *Epoch +} + +// NextGlobalBlock returns the first global block number expected in the next proposal. +// When CommitQC is present it equals CommitQC.GlobalRange().Next; otherwise it equals Epoch.FirstBlock. +func (vs *ViewSpec) NextGlobalBlock() GlobalBlockNumber { + if cQC, ok := vs.CommitQC.Get(); ok { + return cQC.GlobalRange().Next + } + return vs.Epoch.FirstBlock() } // View is the view justified by vs. @@ -124,11 +135,11 @@ func (vs *ViewSpec) View() View { return View{Index: idx, Number: 0} } -func (vs *ViewSpec) NextTimestamp(c *Committee) time.Time { +func (vs *ViewSpec) NextTimestamp() time.Time { if cQC, ok := vs.CommitQC.Get(); ok { return cQC.Proposal().NextTimestamp() } - return c.GenesisTimestamp() + return vs.Epoch.FirstTimestamp() } // Proposal is the road tipcut proposal. @@ -136,33 +147,35 @@ 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 + epochIndex uint64 + 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], epochIndex uint64, 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, + epochIndex: epochIndex, + firstBlock: firstBlock, + app: app, } } @@ -178,24 +191,24 @@ func (m *Proposal) Timestamp() time.Time { return m.timestamp } // App . func (m *Proposal) App() utils.Option[*AppProposal] { return m.app } +// EpochIndex returns the epoch index encoded in the proposal. +func (m *Proposal) EpochIndex() uint64 { return m.epochIndex } + +// FirstBlock returns the first global block number of the epoch encoded in the proposal. +func (m *Proposal) FirstBlock() GlobalBlockNumber { return m.firstBlock } + // 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]() } @@ -206,19 +219,29 @@ 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 -// and is internally valid. Lanes may be omitted — omitted lanes are -// treated as implicit empty ranges by FullProposal.Verify. -func (m *Proposal) Verify(c *Committee) error { +// Verify checks epoch binding and structural lane-range validity (range bounds and +// max-length). Lane membership is not checked here — it requires committee context +// and is enforced by FullProposal.Verify. QC-chain continuity is likewise only +// enforced there. +func (m *Proposal) Verify(ep *Epoch) error { + if got, want := m.EpochIndex(), ep.EpochIndex(); got != want { + return fmt.Errorf("epoch_index = %d, want %d", got, want) + } + if got, want := m.FirstBlock(), ep.FirstBlock(); got != want { + return fmt.Errorf("first_block = %v, want %v", got, want) + } for _, r := range m.laneRanges { - if err := r.Verify(c); err != nil { - return fmt.Errorf("laneRange[%v]: %w", r.Lane(), err) + if r.first > r.next { + return fmt.Errorf("laneRange[%v]: invalid range [%v,%v)", r.Lane(), r.first, r.next) } - if got, wantMax := r.Len(), uint64(MaxLaneRangeInProposal); got > wantMax { - return fmt.Errorf("laneRanges[%v]: len = %d, want <= %d", r.Lane(), got, wantMax) + if r.first == r.next && r.lastHash != (BlockHeaderHash{}) { + return fmt.Errorf("laneRange[%v]: non-zero hash for empty range", r.Lane()) + } + if got := r.Len(); got > MaxLaneRangeInProposal { + return fmt.Errorf("laneRange[%v].Len() = %d, want <= %d", r.Lane(), got, MaxLaneRangeInProposal) } } return nil @@ -265,12 +288,12 @@ func NewReproposal( // timestamp might get replaced to ensure that timestamps are monotone. func NewProposal( key SecretKey, - committee *Committee, viewSpec ViewSpec, timestamp time.Time, laneQCs map[LaneID]*LaneQC, appQC utils.Option[*AppQC], ) (*FullProposal, error) { + committee := viewSpec.Epoch.Committee() if got, want := key.Public(), committee.Leader(viewSpec.View()); got != want { return nil, fmt.Errorf("key %q is not the leader %q for view %v", got, want, viewSpec.View()) } @@ -301,15 +324,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(); timestamp.Before(wantMin) { timestamp = wantMin } - proposal := newProposal(viewSpec.View(), timestamp, laneRanges, app) + proposal := newProposal(viewSpec.View(), timestamp, laneRanges, app, viewSpec.Epoch.EpochIndex(), viewSpec.Epoch.FirstBlock()) return &FullProposal{ proposal: Sign(key, proposal), @@ -339,17 +362,18 @@ 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(vs ViewSpec) error { + c := vs.Epoch.Committee() 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(); got.Before(wantMin) { return fmt.Errorf("proposal.Timestamp() = %v, want >= %v", got, wantMin) } // Is proposer valid? @@ -367,7 +391,7 @@ func (m *FullProposal) Verify(c *Committee, vs ViewSpec) error { // Verify timeoutQC. if tQC, ok := m.timeoutQC.Get(); ok { s.Spawn(func() error { - if err := tQC.Verify(c, vs.CommitQC); err != nil { + if err := tQC.Verify(vs.Epoch, vs.CommitQC); err != nil { return fmt.Errorf("timeoutQC: %w", err) } return nil @@ -384,11 +408,22 @@ func (m *FullProposal) Verify(c *Committee, vs ViewSpec) error { return nil } } - // Verify the proposal's lane structure against the committee. + // Verify the proposal's epoch binding and lane structure. proposal := m.proposal.Msg() - if err := proposal.Verify(c); err != nil { + if err := proposal.Verify(vs.Epoch); err != nil { return fmt.Errorf("proposal: %w", err) } + if roads := vs.Epoch.Roads(); proposal.Index() < roads.First || proposal.Index() > roads.Last { + return fmt.Errorf("proposal road_index %d not in epoch roads [%d, %d]", proposal.Index(), roads.First, roads.Last) + } + for _, r := range proposal.laneRanges { + if err := r.Verify(c); err != nil { + return fmt.Errorf("proposal: laneRange[%v]: %w", r.Lane(), err) + } + if got := r.Len(); got > MaxLaneRangeInProposal { + return fmt.Errorf("proposal: laneRange[%v].Len() = %d, want <= %d", r.Lane(), got, MaxLaneRangeInProposal) + } + } // Verify each lane range against the previous commitQC and its laneQC justification. for lane := range c.Lanes().All() { r := proposal.LaneRange(lane) @@ -425,6 +460,9 @@ func (m *FullProposal) Verify(c *Committee, vs ViewSpec) error { } } else { app, _ := m.proposal.Msg().App().Get() + if got, want := app.EpochIndex(), m.proposal.Msg().EpochIndex(); got != want { + return fmt.Errorf("app epoch_index %d != proposal epoch_index %d", got, want) + } appQC, ok := m.appQC.Get() if !ok { return errors.New("appQC missing") @@ -438,7 +476,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) } } @@ -515,6 +553,8 @@ var ProposalConv = protoutils.Conv[*Proposal, *pb.Proposal]{ Timestamp: TimeConv.Encode(m.timestamp), LaneRanges: LaneRangeConv.EncodeSlice(laneRanges), App: AppProposalConv.EncodeOpt(m.app), + FirstBlock: utils.Alloc(uint64(m.firstBlock)), + EpochIndex: utils.Alloc(m.epochIndex), } }, Decode: func(m *pb.Proposal) (*Proposal, error) { @@ -534,12 +574,16 @@ var ProposalConv = protoutils.Conv[*Proposal, *pb.Proposal]{ if err != nil { return nil, fmt.Errorf("appQC: %w", err) } - proposal := newProposal( - view, - timestamp, - laneRanges, - app, - ) + // Hard-reject messages with absent first_block/epoch_index. + // Autobahn is pre-production; there is no rolling-upgrade path from + // messages encoded before these fields were added. + if m.FirstBlock == nil { + return nil, fmt.Errorf("first_block: missing") + } + if m.EpochIndex == nil { + return nil, fmt.Errorf("epoch_index: missing") + } + proposal := newProposal(view, timestamp, laneRanges, app, m.GetEpochIndex(), GlobalBlockNumber(m.GetFirstBlock())) 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 c6aab48ecc..c785d4ebe4 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" @@ -50,7 +49,7 @@ func makeCommitQCFromProposal(keys []SecretKey, fp *FullProposal) *CommitQC { // makeAppQCFor creates an AppQC for the given parameters, signed by all keys. func makeAppQCFor(keys []SecretKey, globalNum GlobalBlockNumber, roadIdx RoadIndex, appHash AppHash) *AppQC { - appProposal := NewAppProposal(globalNum, roadIdx, appHash) + appProposal := NewAppProposal(globalNum, roadIdx, appHash, 0) vote := NewAppVote(appProposal) var votes []*Signed[*AppVote] for _, k := range keys { @@ -62,26 +61,26 @@ func makeAppQCFor(keys []SecretKey, globalNum GlobalBlockNumber, roadIdx RoadInd func TestProposalVerifyFreshEmptyRanges(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} 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, vs, time.Now(), nil, utils.None[*AppQC]())) + require.NoError(t, fp.Verify(vs)) } func TestProposalVerifyFreshWithBlocks(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} proposerKey := leaderKey(committee, keys, vs.View()) // Produce a LaneQC for the proposer's lane. 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, vs, time.Now(), map[LaneID]*LaneQC{lane: laneQC}, utils.None[*AppQC]())) - require.NoError(t, fp.Verify(committee, vs)) + require.NoError(t, fp.Verify(vs)) } func TestNewProposalRejectsLaneRangeLongerThanMaxLaneRangeInProposal(t *testing.T) { @@ -92,9 +91,9 @@ func TestNewProposalRejectsLaneRangeLongerThanMaxLaneRangeInProposal(t *testing. lane := proposerKey.Public() laneQC := makeLaneQC(rng, committee, keys, lane, MaxLaneRangeInProposal, GenBlockHeaderHash(rng)) + vs.Epoch = NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0) _, err := NewProposal( proposerKey, - committee, vs, time.Now(), map[LaneID]*LaneQC{lane: laneQC}, @@ -106,49 +105,46 @@ func TestNewProposalRejectsLaneRangeLongerThanMaxLaneRangeInProposal(t *testing. func TestProposalBlockTimestampStrictlyMonotone(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs0 := ViewSpec{} + firstBlock := GlobalBlockNumber(0) + vs0 := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} proposer0 := leaderKey(committee, keys, vs0.View()) lane := proposer0.Public() firstProposal := utils.OrPanic1(NewProposal( proposer0, - committee, - vs0, - time.Now(), + vs0, time.Now(), map[LaneID]*LaneQC{ lane: makeLaneQC(rng, committee, keys, lane, 2, GenBlockHeaderHash(rng)), }, 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") commitQC0 := makeCommitQCFromProposal(keys, firstProposal) - vs1 := ViewSpec{CommitQC: utils.Some(commitQC0)} + vs1 := ViewSpec{CommitQC: utils.Some(commitQC0), Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} proposer1 := leaderKey(committee, keys, vs1.View()) secondProposal := utils.OrPanic1(NewProposal( proposer1, - committee, - vs1, - time.Now(), + vs1, time.Now(), map[LaneID]*LaneQC{ lane: makeLaneQC(rng, committee, keys, lane, 3, GenBlockHeaderHash(rng)), }, 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") } @@ -156,59 +152,51 @@ func TestProposalVerifyRejectsNonMonotoneTimestamp(t *testing.T) { t.Run("wrt genesis timestamp", func(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} + genesisTimestamp := time.Now() + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), genesisTimestamp, committee, 0)} 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, vs, genesisTimestamp, nil, utils.None[*AppQC]())) + require.NoError(t, fp.Verify(vs)) + + vsLater := vs + vsLater.Epoch = NewEpoch(0, OpenRoadRange(), fp.Proposal().Msg().Timestamp().Add(time.Nanosecond), committee, 0) + require.Error(t, fp.Verify(vsLater)) }) t.Run("wrt previous proposal", func(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs0 := ViewSpec{} + vs0 := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} proposer0 := leaderKey(committee, keys, vs0.View()) lane := proposer0.Public() lQC := makeLaneQC(rng, committee, keys, lane, 0, GenBlockHeaderHash(rng)) fp0a := utils.OrPanic1(NewProposal( proposer0, - committee, - vs0, - time.Now(), + vs0, time.Now(), map[LaneID]*LaneQC{lane: lQC}, utils.None[*AppQC](), )) fp0b := utils.OrPanic1(NewProposal( proposer0, - committee, - vs0, - fp0a.Proposal().Msg().NextTimestamp().Add(time.Hour), + vs0, fp0a.Proposal().Msg().NextTimestamp().Add(time.Hour), map[LaneID]*LaneQC{lane: lQC}, utils.None[*AppQC](), )) - vs1a := ViewSpec{CommitQC: utils.Some(makeCommitQCFromProposal(keys, fp0a))} - vs1b := ViewSpec{CommitQC: utils.Some(makeCommitQCFromProposal(keys, fp0b))} + vs1a := ViewSpec{CommitQC: utils.Some(makeCommitQCFromProposal(keys, fp0a)), Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} + vs1b := ViewSpec{CommitQC: utils.Some(makeCommitQCFromProposal(keys, fp0b)), Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} proposer1 := leaderKey(committee, keys, vs1a.View()) fp1a := utils.OrPanic1(NewProposal( proposer1, - committee, - vs1a, - fp0a.Proposal().Msg().NextTimestamp(), + vs1a, 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(vs1a)) + require.Error(t, fp1a.Verify(vs1b)) }) } @@ -217,40 +205,40 @@ func TestProposalVerifyRejectsViewMismatch(t *testing.T) { committee, keys := GenCommittee(rng, 4) // Build a valid proposal at genesis view (0, 0). - vs0 := ViewSpec{} + vs0 := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} leader0 := leaderKey(committee, keys, vs0.View()) - fp := utils.OrPanic1(NewProposal(leader0, committee, vs0, time.Now(), nil, utils.None[*AppQC]())) + fp := utils.OrPanic1(NewProposal(leader0, vs0, 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) + vs1 := ViewSpec{CommitQC: utils.Some(commitQC), Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} + err := fp.Verify(vs1) require.Error(t, err) } func TestProposalVerifyRejectsForgedSignature(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} 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, vs, time.Now(), nil, utils.None[*AppQC]())) + fp2 := utils.OrPanic1(NewProposal(proposerKey, vs, 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(vs) require.Error(t, err) } func TestProposalVerifyRejectsWrongProposer(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} correctLeader := leaderKey(committee, keys, vs.View()) - fp := utils.OrPanic1(NewProposal(correctLeader, committee, vs, time.Now(), nil, utils.None[*AppQC]())) + fp := utils.OrPanic1(NewProposal(correctLeader, vs, time.Now(), nil, utils.None[*AppQC]())) // Re-sign the same proposal with a different (non-leader) key. var wrongKey SecretKey @@ -266,22 +254,22 @@ func TestProposalVerifyRejectsWrongProposer(t *testing.T) { appQC: fp.appQC, timeoutQC: fp.timeoutQC, } - err := tamperedFP.Verify(committee, vs) + err := tamperedFP.Verify(vs) require.Error(t, err) } func TestProposalVerifyRejectsInconsistentTimeoutQC(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} // no timeoutQC + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} // 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, vs, time.Now(), nil, utils.None[*AppQC]())) // Attach a timeoutQC that the ViewSpec doesn't expect. var timeoutVotes []*FullTimeoutVote for _, k := range keys { - timeoutVotes = append(timeoutVotes, NewFullTimeoutVote(k, View{Index: 0, Number: 0}, utils.None[*PrepareQC]())) + timeoutVotes = append(timeoutVotes, NewFullTimeoutVote(k, View{Index: 0, Number: 0}, utils.None[*PrepareQC](), 0)) } tQC := NewTimeoutQC(timeoutVotes) @@ -291,17 +279,17 @@ func TestProposalVerifyRejectsInconsistentTimeoutQC(t *testing.T) { appQC: fp.appQC, timeoutQC: utils.Some(tQC), } - err := tamperedFP.Verify(committee, vs) + err := tamperedFP.Verify(vs) require.Error(t, err) } func TestProposalVerifyRejectsNonCommitteeLane(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} proposerKey := leaderKey(committee, keys, vs.View()) - fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now(), nil, utils.None[*AppQC]())) + fp := utils.OrPanic1(NewProposal(proposerKey, vs, 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}. @@ -324,24 +312,24 @@ func TestProposalVerifyRejectsNonCommitteeLane(t *testing.T) { } } - tamperedProposal := newProposal(origProposal.view, origProposal.timestamp, tamperedRanges, origProposal.app) + tamperedProposal := newProposal(origProposal.view, origProposal.timestamp, tamperedRanges, origProposal.app, origProposal.epochIndex, origProposal.firstBlock) maliciousFP := &FullProposal{ proposal: Sign(proposerKey, tamperedProposal), laneQCs: fp.laneQCs, appQC: fp.appQC, timeoutQC: fp.timeoutQC, } - err := maliciousFP.Verify(committee, vs) + err := maliciousFP.Verify(vs) require.Error(t, err) } func TestProposalVerifyAcceptsImplicitLaneRange(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} proposerKey := leaderKey(committee, keys, vs.View()) - fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now(), nil, utils.None[*AppQC]())) + fp := utils.OrPanic1(NewProposal(proposerKey, vs, 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. @@ -356,20 +344,20 @@ 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.epochIndex, origP.firstBlock) shortFP := &FullProposal{ proposal: Sign(proposerKey, shortProposal), } - require.NoError(t, shortFP.Verify(committee, vs)) + require.NoError(t, shortFP.Verify(vs)) } func TestProposalVerifyAcceptsNonContiguousImplicitRanges(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} proposerKey := leaderKey(committee, keys, vs.View()) - fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now(), nil, utils.None[*AppQC]())) + fp := utils.OrPanic1(NewProposal(proposerKey, vs, 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() @@ -382,20 +370,20 @@ 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.epochIndex, origP.firstBlock) shortFP := &FullProposal{ proposal: Sign(proposerKey, shortProposal), } - require.NoError(t, shortFP.Verify(committee, vs)) + require.NoError(t, shortFP.Verify(vs)) } func TestProposalVerifyRejectsLaneRangeFirstMismatch(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} proposerKey := leaderKey(committee, keys, vs.View()) - fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now(), nil, utils.None[*AppQC]())) + fp := utils.OrPanic1(NewProposal(proposerKey, vs, time.Now(), nil, utils.None[*AppQC]())) // Tamper: change one lane's first to 5 (genesis expects 0). origP := fp.Proposal().Msg() @@ -408,46 +396,46 @@ 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.epochIndex, origP.firstBlock) tamperedFP := &FullProposal{ proposal: Sign(proposerKey, tamperedProposal), } - err := tamperedFP.Verify(committee, vs) + err := tamperedFP.Verify(vs) require.Error(t, err) } func TestProposalVerifyRejectsMissingLaneQC(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} proposerKey := leaderKey(committee, keys, vs.View()) lane := keys[0].Public() 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, vs, 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(vs) require.Error(t, err) } func TestProposalVerifyRejectsLaneQCBlockNumberMismatch(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} proposerKey := leaderKey(committee, keys, vs.View()) lane := keys[0].Public() // 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, vs, time.Now(), map[LaneID]*LaneQC{lane: goodQC}, utils.None[*AppQC]())) // Swap in a QC certifying block 0 — range expects block 1. @@ -456,14 +444,14 @@ func TestProposalVerifyRejectsLaneQCBlockNumberMismatch(t *testing.T) { proposal: fp.proposal, laneQCs: map[LaneID]*LaneQC{lane: wrongQC}, } - err := tamperedFP.Verify(committee, vs) + err := tamperedFP.Verify(vs) require.Error(t, err) } func TestProposalVerifyRejectsInvalidLaneQCSignature(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} proposerKey := leaderKey(committee, keys, vs.View()) lane := keys[0].Public() @@ -481,10 +469,10 @@ func TestProposalVerifyRejectsInvalidLaneQCSignature(t *testing.T) { } badLaneQC := NewLaneQC(badVotes) - fp := utils.OrPanic1(NewProposal(proposerKey, committee, vs, time.Now(), + fp := utils.OrPanic1(NewProposal(proposerKey, vs, time.Now(), map[LaneID]*LaneQC{lane: badLaneQC}, utils.None[*AppQC]())) - err := fp.Verify(committee, vs) + err := fp.Verify(vs) require.Error(t, err) } @@ -500,26 +488,6 @@ func TestProposalConvDecode_RejectsDuplicateLaneRanges(t *testing.T) { require.Error(t, err) } -func TestProposalVerifyRejectsLaneRangeLongerThanMaxLaneRangeInProposal(t *testing.T) { - rng := utils.TestRng() - committee, _ := GenCommittee(rng, 4) - lane := slices.Collect(committee.Lanes().All())[0] - parentHash := GenBlockHeaderHash(rng) - var lastHeader *BlockHeader - for blockNumber := range BlockNumber(MaxLaneRangeInProposal + 1) { - lastHeader = NewBlock(lane, blockNumber, parentHash, GenPayload(rng)).Header() - parentHash = lastHeader.Hash() - } - - proposal := newProposal( - View{}, - time.Unix(1, 2), - []*LaneRange{NewLaneRange(lane, 0, utils.Some(lastHeader))}, - utils.None[*AppProposal](), - ) - require.Error(t, proposal.Verify(committee)) -} - func makeFullProposal( committee *Committee, keys []SecretKey, @@ -527,12 +495,10 @@ func makeFullProposal( laneQCs map[LaneID]*LaneQC, appQC utils.Option[*AppQC], ) *FullProposal { - vs := ViewSpec{CommitQC: prev} + vs := ViewSpec{CommitQC: prev, Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} return utils.OrPanic1(NewProposal( leaderKey(committee, keys, vs.View()), - committee, - vs, - time.Now(), + vs, time.Now(), laneQCs, appQC, )) @@ -557,113 +523,113 @@ func TestProposalVerifyRejectsAppProposalLowerThanPrevious(t *testing.T) { 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)) + appQC0 := makeAppQCFor(keys, commitQC0.GlobalRange().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]()) // 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)) + vs := ViewSpec{CommitQC: utils.Some(commitQC1a), Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} + require.NoError(t, fp2a.Verify(vs)) + require.Error(t, fp2b.Verify(vs)) } 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) + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 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, vs, 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(vs) 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 + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 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, vs, time.Now(), nil, utils.Some(goodAppQC))) tamperedFP := &FullProposal{ proposal: fp.proposal, } - err := tamperedFP.Verify(committee, vs) + err := tamperedFP.Verify(vs) require.Error(t, err) } func TestProposalVerifyRejectsAppQCMismatch(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} + firstBlock := GlobalBlockNumber(0) + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} 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, vs, 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(vs) require.Error(t, err) } func TestProposalVerifyRejectsInvalidAppQCSignature(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} + firstBlock := GlobalBlockNumber(0) + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} 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, vs, 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(vs) require.Error(t, err) } func TestProposalVerifyRejectsLaneQCHeaderHashMismatch(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - vs := ViewSpec{} + vs := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} proposerKey := leaderKey(committee, keys, vs.View()) lane := proposerKey.Public() // 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, vs, time.Now(), map[LaneID]*LaneQC{lane: realQC}, utils.None[*AppQC]())) // Swap in a different QC for block 0 (different payload → different hash). @@ -674,18 +640,23 @@ func TestProposalVerifyRejectsLaneQCHeaderHashMismatch(t *testing.T) { proposal: fp.proposal, laneQCs: map[LaneID]*LaneQC{lane: differentQC}, } - err := tamperedFP.Verify(committee, vs) + err := tamperedFP.Verify(vs) require.Error(t, err) } func TestProposalVerifyValidReproposal(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) - - // First, create a valid proposal at view (0, 0) with a PrepareQC. - vs0 := ViewSpec{} + firstBlock := GlobalBlockNumber(100) + // 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{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 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, vs0, time.Now(), + map[LaneID]*LaneQC{lane: laneQC0}, utils.None[*AppQC]())) // Build a PrepareQC for the proposal at (0, 0). var prepareVotes []*Signed[*PrepareVote] @@ -697,17 +668,19 @@ func TestProposalVerifyValidReproposal(t *testing.T) { // Timeout at view (0, 0) with the PrepareQC → forces reproposal at (0, 1). var timeoutVotes []*FullTimeoutVote for _, k := range keys { - timeoutVotes = append(timeoutVotes, NewFullTimeoutVote(k, View{Index: 0, Number: 0}, utils.Some(prepareQC))) + timeoutVotes = append(timeoutVotes, NewFullTimeoutVote(k, View{Index: 0, Number: 0}, utils.Some(prepareQC), 0)) } timeoutQC := NewTimeoutQC(timeoutVotes) - vs1 := ViewSpec{TimeoutQC: utils.Some(timeoutQC)} + vs1 := ViewSpec{TimeoutQC: utils.Some(timeoutQC), Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 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, vs1, 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(vs1)) } func TestProposalVerifyRejectsReproposalWithUnnecessaryData(t *testing.T) { @@ -715,9 +688,9 @@ func TestProposalVerifyRejectsReproposalWithUnnecessaryData(t *testing.T) { committee, keys := GenCommittee(rng, 4) // Build a PrepareQC at (0, 0). - vs0 := ViewSpec{} + vs0 := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} leader0 := leaderKey(committee, keys, vs0.View()) - fp0 := utils.OrPanic1(NewProposal(leader0, committee, vs0, time.Now(), nil, utils.None[*AppQC]())) + fp0 := utils.OrPanic1(NewProposal(leader0, vs0, time.Now(), nil, utils.None[*AppQC]())) var prepareVotes []*Signed[*PrepareVote] for _, k := range keys { @@ -727,15 +700,15 @@ func TestProposalVerifyRejectsReproposalWithUnnecessaryData(t *testing.T) { var timeoutVotes []*FullTimeoutVote for _, k := range keys { - timeoutVotes = append(timeoutVotes, NewFullTimeoutVote(k, View{Index: 0, Number: 0}, utils.Some(prepareQC))) + timeoutVotes = append(timeoutVotes, NewFullTimeoutVote(k, View{Index: 0, Number: 0}, utils.Some(prepareQC), 0)) } timeoutQC := NewTimeoutQC(timeoutVotes) - vs1 := ViewSpec{TimeoutQC: utils.Some(timeoutQC)} + vs1 := ViewSpec{TimeoutQC: utils.Some(timeoutQC), Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} 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, vs1, time.Now(), nil, utils.None[*AppQC]())) lane := keys[0].Public() laneQC := makeLaneQC(rng, committee, keys, lane, 0, GenBlockHeaderHash(rng)) @@ -744,7 +717,7 @@ func TestProposalVerifyRejectsReproposalWithUnnecessaryData(t *testing.T) { laneQCs: map[LaneID]*LaneQC{lane: laneQC}, timeoutQC: reproposal.timeoutQC, } - err := tamperedFP.Verify(committee, vs1) + err := tamperedFP.Verify(vs1) require.Error(t, err) } @@ -753,9 +726,9 @@ func TestProposalVerifyRejectsReproposalHashMismatch(t *testing.T) { committee, keys := GenCommittee(rng, 4) // Build a PrepareQC at (0, 0). - vs0 := ViewSpec{} + vs0 := ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} leader0 := leaderKey(committee, keys, vs0.View()) - fp0 := utils.OrPanic1(NewProposal(leader0, committee, vs0, time.Now(), nil, utils.None[*AppQC]())) + fp0 := utils.OrPanic1(NewProposal(leader0, vs0, time.Now(), nil, utils.None[*AppQC]())) var prepareVotes []*Signed[*PrepareVote] for _, k := range keys { @@ -765,27 +738,27 @@ func TestProposalVerifyRejectsReproposalHashMismatch(t *testing.T) { var timeoutVotes []*FullTimeoutVote for _, k := range keys { - timeoutVotes = append(timeoutVotes, NewFullTimeoutVote(k, View{Index: 0, Number: 0}, utils.Some(prepareQC))) + timeoutVotes = append(timeoutVotes, NewFullTimeoutVote(k, View{Index: 0, Number: 0}, utils.Some(prepareQC), 0)) } timeoutQC := NewTimeoutQC(timeoutVotes) - vs1 := ViewSpec{TimeoutQC: utils.Some(timeoutQC)} + vs1 := ViewSpec{TimeoutQC: utils.Some(timeoutQC), Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} 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, vs1, 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.epochIndex, origP.firstBlock) wrongFP := &FullProposal{ proposal: Sign(leader1, wrongP), timeoutQC: reproposal.timeoutQC, } - err := wrongFP.Verify(committee, vs1) + err := wrongFP.Verify(vs1) require.Error(t, err) } @@ -800,15 +773,15 @@ func TestProposalVerifyRejectsInvalidTimeoutQCSignature(t *testing.T) { } var timeoutVotes []*FullTimeoutVote for _, k := range otherKeys { - timeoutVotes = append(timeoutVotes, NewFullTimeoutVote(k, View{Index: 0, Number: 0}, utils.None[*PrepareQC]())) + timeoutVotes = append(timeoutVotes, NewFullTimeoutVote(k, View{Index: 0, Number: 0}, utils.None[*PrepareQC](), 0)) } badTimeoutQC := NewTimeoutQC(timeoutVotes) - vs := ViewSpec{TimeoutQC: utils.Some(badTimeoutQC)} + vs := ViewSpec{TimeoutQC: utils.Some(badTimeoutQC), Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)} leader := leaderKey(committee, keys, vs.View()) - fp := utils.OrPanic1(NewProposal(leader, committee, vs, time.Now(), nil, utils.None[*AppQC]())) + fp := utils.OrPanic1(NewProposal(leader, vs, time.Now(), nil, utils.None[*AppQC]())) - err := fp.Verify(committee, vs) + err := fp.Verify(vs) require.Error(t, err) } diff --git a/sei-tendermint/autobahn/types/testonly.go b/sei-tendermint/autobahn/types/testonly.go index 3dd9698d3d..0c4951f88c 100644 --- a/sei-tendermint/autobahn/types/testonly.go +++ b/sei-tendermint/autobahn/types/testonly.go @@ -11,6 +11,37 @@ 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, + Epoch: NewEpoch(0, OpenRoadRange(), genesisTimestamp, committee, 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, vs, 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 +68,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. @@ -178,14 +209,42 @@ func GenView(rng utils.Rng) View { } } +// GenEpochWithCommittee returns a random Epoch wrapping committee. +// epochIndex, firstBlock, and timestamp are randomized so that tests exercise +// epoch-binding checks rather than silently passing on zero values. +// Roads always starts at 0 so tests using low view indices don't need special handling. +func GenEpochWithCommittee(rng utils.Rng, committee *Committee) *Epoch { + return NewEpoch( + rng.Uint64()%100, + RoadRange{First: 0, Last: RoadIndex(rng.Uint64()%10000) + 1}, + utils.GenTimestamp(rng), + committee, + GlobalBlockNumber(rng.Uint64()%1000000)+1, + ) +} + // 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)), 0, 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)), 0, GlobalBlockNumber(rng.Uint64())) +} + +// ProposalAt returns a minimal Proposal at view, consistent with ep. +// No lane ranges and no app proposal — only for tests that care about +// signature weight or epoch binding, not lane/app data. +func ProposalAt(ep *Epoch, view View) *Proposal { + return newProposal(view, time.Time{}, nil, utils.None[*AppProposal](), ep.EpochIndex(), ep.FirstBlock()) +} + +// GenProposalForEpoch generates a Proposal at a specific view whose epochIndex +// and firstBlock are taken from ep. Use in tests that verify QCs against a +// known Epoch — the proposal fields must match ep for Verify to accept. +func GenProposalForEpoch(rng utils.Rng, ep *Epoch, view View) *Proposal { + return newProposal(view, time.Now(), utils.GenSlice(rng, GenLaneRange), utils.Some(GenAppProposal(rng)), ep.EpochIndex(), ep.FirstBlock()) } // GenAppHash generates a random AppHash. @@ -195,7 +254,7 @@ func GenAppHash(rng utils.Rng) AppHash { // GenAppProposal generates a random AppProposal. func GenAppProposal(rng utils.Rng) *AppProposal { - return NewAppProposal(GenGlobalBlockNumber(rng), GenRoadIndex(rng), GenAppHash(rng)) + return NewAppProposal(GenGlobalBlockNumber(rng), GenRoadIndex(rng), GenAppHash(rng), rng.Uint64()) } // GenAppVote generates a random AppVote. @@ -278,12 +337,12 @@ func GenFullCommitQC(rng utils.Rng) *FullCommitQC { // GenTimeoutVote generates a random TimeoutVote. func GenTimeoutVote(rng utils.Rng) *TimeoutVote { - return NewTimeoutVote(GenView(rng), utils.Some(GenViewNumber(rng))) + return NewTimeoutVote(GenView(rng), utils.Some(GenViewNumber(rng)), rng.Uint64()) } // GenFullTimeoutVote generates a random FullTimeoutVote. func GenFullTimeoutVote(rng utils.Rng) *FullTimeoutVote { - return NewFullTimeoutVote(GenSecretKey(rng), GenView(rng), utils.Some(GenPrepareQC(rng))) + return NewFullTimeoutVote(GenSecretKey(rng), GenView(rng), utils.Some(GenPrepareQC(rng)), rng.Uint64()) } // GenTimeoutQC generates a random TimeoutQC. diff --git a/sei-tendermint/autobahn/types/timeout.go b/sei-tendermint/autobahn/types/timeout.go index 003ff64b26..0e9609b7a6 100644 --- a/sei-tendermint/autobahn/types/timeout.go +++ b/sei-tendermint/autobahn/types/timeout.go @@ -14,13 +14,15 @@ type TimeoutVote struct { utils.ReadOnly view View latestPrepareQC utils.Option[ViewNumber] + epochIndex uint64 } // NewTimeoutVote creates a new TimeoutVote. -func NewTimeoutVote(view View, latestPrepareQC utils.Option[ViewNumber]) *TimeoutVote { +func NewTimeoutVote(view View, latestPrepareQC utils.Option[ViewNumber], epochIndex uint64) *TimeoutVote { return &TimeoutVote{ view: view, latestPrepareQC: latestPrepareQC, + epochIndex: epochIndex, } } @@ -29,6 +31,9 @@ func (m *TimeoutVote) View() View { return m.view } +// EpochIndex returns the epoch this vote belongs to. +func (m *TimeoutVote) EpochIndex() uint64 { return m.epochIndex } + // latestPrepareQCView is the highest view number for which a PrepareQC was observed by the node. func (m *TimeoutVote) latestPrepareQCView() utils.Option[View] { return utils.MapOpt(m.latestPrepareQC, func(n ViewNumber) View { @@ -44,10 +49,11 @@ type FullTimeoutVote struct { } // NewFullTimeoutVote creates a new FullTimeoutVote. -func NewFullTimeoutVote(key SecretKey, view View, latestPrepareQC utils.Option[*PrepareQC]) *FullTimeoutVote { +func NewFullTimeoutVote(key SecretKey, view View, latestPrepareQC utils.Option[*PrepareQC], epochIndex uint64) *FullTimeoutVote { vote := &TimeoutVote{ view: view, latestPrepareQC: utils.MapOpt(latestPrepareQC, func(qc *PrepareQC) ViewNumber { return qc.Proposal().View().Number }), + epochIndex: epochIndex, } return &FullTimeoutVote{ vote: Sign(key, vote), @@ -65,8 +71,12 @@ func (m *FullTimeoutVote) View() View { return m.vote.Msg().View() } -// Verify verifies the FullTimeoutVote against the committee. -func (m *FullTimeoutVote) Verify(c *Committee) error { +// Verify verifies the FullTimeoutVote against the epoch. +func (m *FullTimeoutVote) Verify(ep *Epoch) error { + if got, want := m.vote.Msg().epochIndex, ep.EpochIndex(); got != want { + return fmt.Errorf("epoch_index = %d, want %d", got, want) + } + c := ep.Committee() if err := m.vote.VerifySig(c); err != nil { return err } @@ -77,7 +87,7 @@ func (m *FullTimeoutVote) Verify(c *Committee) error { } // TODO: verifying PrepareQC in all Timeout votes might be too inefficient. // If it is, we can skip duplicated verification. - if err := pQC.Verify(c); err != nil { + if err := pQC.Verify(ep); err != nil { return fmt.Errorf("latestPrepareQC: %w", err) } if got := pQC.Proposal().View(); got != want { @@ -132,14 +142,18 @@ func (m *TimeoutQC) LatestPrepareQC() utils.Option[*PrepareQC] { return m.latestPrepareQC } -// Verify verifies the TimeoutQC against the committee and the previous CommitQC. +// Verify verifies the TimeoutQC against the epoch and the previous CommitQC. // Verifying TimeoutQC should NOT require previous TimeoutQC, // since observing prior TimeoutQCs is not required in the pb. -func (m *TimeoutQC) Verify(c *Committee, prev utils.Option[*CommitQC]) error { +func (m *TimeoutQC) Verify(ep *Epoch, prev utils.Option[*CommitQC]) error { + c := ep.Committee() // Verify the signatures. weight := uint64(0) done := map[PublicKey]struct{}{} for _, v := range m.votes { + if got, want := v.Msg().epochIndex, ep.EpochIndex(); got != want { + return fmt.Errorf("vote epoch_index = %d, want %d", got, want) + } if _, ok := done[v.sig.key]; ok { return fmt.Errorf("duplicate vote from %q", v.sig.key) } @@ -153,9 +167,14 @@ func (m *TimeoutQC) Verify(c *Committee, prev utils.Option[*CommitQC]) error { if got, want := weight, c.TimeoutQuorum(); got < want { return fmt.Errorf("got %v votes weight, want >= %v", got, want) } + // Verify that the view index is within the epoch's road range. + roads := ep.Roads() + view := m.View() + if view.Index < roads.First || view.Index > roads.Last { + return fmt.Errorf("road_index %v not in epoch roads [%v, %v]", view.Index, roads.First, roads.Last) + } // Check that the TimeoutQC is from the correct consensus instance. h := utils.None[ViewNumber]() - view := m.View() if got, want := view.Index, NextIndexOpt(prev); got != want { return fmt.Errorf("timeoutQC.View().Index = %v, want %v", got, want) } @@ -177,7 +196,7 @@ func (m *TimeoutQC) Verify(c *Committee, prev utils.Option[*CommitQC]) error { if got, want := pQC.Proposal().View(), (View{Index: view.Index, Number: vn}); got != want { return fmt.Errorf("latestPrepareQC view number mismatch, got %v, want %v", got, want) } - if err := pQC.Verify(c); err != nil { + if err := pQC.Verify(ep); err != nil { return fmt.Errorf("higPrepareQC: %w", err) } } else { @@ -199,12 +218,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.epochIndex, p.firstBlock), true } // TimeoutVoteConv is the protobuf converter for TimeoutVote. @@ -218,6 +232,7 @@ var TimeoutVoteConv = protoutils.Conv[*TimeoutVote, *pb.TimeoutVote]{ } return nil }(), + EpochIndex: utils.Alloc(m.epochIndex), } }, Decode: func(m *pb.TimeoutVote) (*TimeoutVote, error) { @@ -225,6 +240,9 @@ var TimeoutVoteConv = protoutils.Conv[*TimeoutVote, *pb.TimeoutVote]{ if err != nil { return nil, fmt.Errorf("view: %w", err) } + if m.EpochIndex == nil { + return nil, fmt.Errorf("EpochIndex: missing") + } return &TimeoutVote{ view: view, latestPrepareQC: func() utils.Option[ViewNumber] { @@ -233,6 +251,7 @@ var TimeoutVoteConv = protoutils.Conv[*TimeoutVote, *pb.TimeoutVote]{ } return utils.None[ViewNumber]() }(), + epochIndex: *m.EpochIndex, }, nil }, } diff --git a/sei-tendermint/autobahn/types/types_test.go b/sei-tendermint/autobahn/types/types_test.go index 79f48a7773..d4014a3fee 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,16 +115,17 @@ 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)), 0, GlobalBlockNumber(rng.Uint64())) if wantView.Less(pView) { wantView = pView } - votes = append(votes, NewFullTimeoutVote(k, view, utils.Some(makePrepareQC(keys, NewPrepareVote(p))))) + votes = append(votes, NewFullTimeoutVote(k, view, utils.Some(makePrepareQC(keys, NewPrepareVote(p))), 0)) } tQC := NewTimeoutQC(votes) 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) @@ -162,17 +144,16 @@ func TestTimeoutQCConvDecode_EmptyVotesReturnsError(t *testing.T) { func TestNewTimeoutQC_MixedPrepareQCs(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + ep := GenEpochWithCommittee(rng, committee) view := View{Index: 0, Number: 0} - pqc := makePrepareQC(keys, NewPrepareVote( - newProposal(view, time.Now(), utils.GenSlice(rng, GenLaneRange), utils.Some(GenAppProposal(rng))), - )) + pqc := makePrepareQC(keys, NewPrepareVote(ProposalAt(ep, view))) // Only keys[0] carries the PrepareQC; the rest carry None. votes := make([]*FullTimeoutVote, len(keys)) - votes[0] = NewFullTimeoutVote(keys[0], view, utils.Some(pqc)) + votes[0] = NewFullTimeoutVote(keys[0], view, utils.Some(pqc), ep.EpochIndex()) for i := 1; i < len(keys); i++ { - votes[i] = NewFullTimeoutVote(keys[i], view, utils.None[*PrepareQC]()) + votes[i] = NewFullTimeoutVote(keys[i], view, utils.None[*PrepareQC](), ep.EpochIndex()) } tqc := NewTimeoutQC(votes) @@ -183,7 +164,7 @@ func TestNewTimeoutQC_MixedPrepareQCs(t *testing.T) { if got.View() != view { t.Fatalf("LatestPrepareQC.View() = %v, want %v", got.View(), view) } - if err := tqc.Verify(committee, utils.None[*CommitQC]()); err != nil { + if err := tqc.Verify(ep, utils.None[*CommitQC]()); err != nil { t.Fatalf("Verify: %v", err) } } @@ -193,18 +174,19 @@ func TestNewTimeoutQC_MixedPrepareQCs(t *testing.T) { func TestNewTimeoutQC_AllNone(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + ep := GenEpochWithCommittee(rng, committee) view := View{Index: 0, Number: 0} votes := make([]*FullTimeoutVote, len(keys)) for i, k := range keys { - votes[i] = NewFullTimeoutVote(k, view, utils.None[*PrepareQC]()) + votes[i] = NewFullTimeoutVote(k, view, utils.None[*PrepareQC](), ep.EpochIndex()) } tqc := NewTimeoutQC(votes) if tqc.LatestPrepareQC().IsPresent() { t.Fatal("LatestPrepareQC should be None when no vote carries one") } - if err := tqc.Verify(committee, utils.None[*CommitQC]()); err != nil { + if err := tqc.Verify(ep, utils.None[*CommitQC]()); err != nil { t.Fatalf("Verify: %v", err) } } @@ -214,21 +196,20 @@ func TestNewTimeoutQC_AllNone(t *testing.T) { func TestTimeoutQCVerify_HighestPrepareQCSelected(t *testing.T) { rng := utils.TestRng() committee, keys := GenCommittee(rng, 4) + ep := GenEpochWithCommittee(rng, committee) view := View{Index: 0, Number: 5} 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))), - )) + return makePrepareQC(keys, NewPrepareVote(ProposalAt(ep, pView))) } // keys[0] has PrepareQC at view number 2, keys[1] at 4, rest None. votes := make([]*FullTimeoutVote, len(keys)) - votes[0] = NewFullTimeoutVote(keys[0], view, utils.Some(makePQCAt(2))) - votes[1] = NewFullTimeoutVote(keys[1], view, utils.Some(makePQCAt(4))) - votes[2] = NewFullTimeoutVote(keys[2], view, utils.None[*PrepareQC]()) - votes[3] = NewFullTimeoutVote(keys[3], view, utils.None[*PrepareQC]()) + votes[0] = NewFullTimeoutVote(keys[0], view, utils.Some(makePQCAt(2)), ep.EpochIndex()) + votes[1] = NewFullTimeoutVote(keys[1], view, utils.Some(makePQCAt(4)), ep.EpochIndex()) + votes[2] = NewFullTimeoutVote(keys[2], view, utils.None[*PrepareQC](), ep.EpochIndex()) + votes[3] = NewFullTimeoutVote(keys[3], view, utils.None[*PrepareQC](), ep.EpochIndex()) tqc := NewTimeoutQC(votes) got, ok := tqc.LatestPrepareQC().Get() @@ -239,7 +220,7 @@ func TestTimeoutQCVerify_HighestPrepareQCSelected(t *testing.T) { if got.View() != wantView { t.Fatalf("LatestPrepareQC.View() = %v, want %v", got.View(), wantView) } - if err := tqc.Verify(committee, utils.None[*CommitQC]()); err != nil { + if err := tqc.Verify(ep, utils.None[*CommitQC]()); err != nil { t.Fatalf("Verify: %v", err) } } diff --git a/sei-tendermint/autobahn/types/wireguard_test.go b/sei-tendermint/autobahn/types/wireguard_test.go index cfe57647c3..b883c11bd8 100644 --- a/sei-tendermint/autobahn/types/wireguard_test.go +++ b/sei-tendermint/autobahn/types/wireguard_test.go @@ -143,7 +143,7 @@ func TestFullCommitQCWireguardAcceptsMaxValidatorsAndHeaders(t *testing.T) { } laneRanges = append(laneRanges, NewLaneRange(lane, 0, utils.Some(lastHeader))) } - proposal := newProposal(View{}, time.Unix(1, 2), laneRanges, utils.None[*AppProposal]()) + proposal := newProposal(View{}, time.Unix(1, 2), laneRanges, utils.None[*AppProposal](), 0, 0) vote := NewCommitVote(proposal) votes := make([]*Signed[*CommitVote], len(keys)) for i, key := range keys { @@ -176,7 +176,7 @@ func TestTimeoutQCWireguardAcceptsMaxValidators(t *testing.T) { _, keys := maxValidatorCommittee(t) votes := make([]*FullTimeoutVote, len(keys)) for i, key := range keys { - votes[i] = NewFullTimeoutVote(key, View{}, utils.None[*PrepareQC]()) + votes[i] = NewFullTimeoutVote(key, View{}, utils.None[*PrepareQC](), 0) } qc := NewTimeoutQC(votes) @@ -197,8 +197,7 @@ func TestFullProposalWireguardAcceptsMaxValidators(t *testing.T) { } proposal, err := NewProposal( secretKeyFor(keys, committee.Leader(View{})), - committee, - ViewSpec{}, + ViewSpec{Epoch: NewEpoch(0, OpenRoadRange(), time.Time{}, committee, 0)}, 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 6dc7f9a0e8..b3d321c71c 100644 --- a/sei-tendermint/internal/autobahn/autobahn.proto +++ b/sei-tendermint/internal/autobahn/autobahn.proto @@ -130,6 +130,8 @@ 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 first_block = 6; // genesis InitialHeight; added to lane block numbers to produce absolute global block numbers + optional uint64 epoch_index = 7; // epoch this proposal belongs to } message FullProposal { @@ -165,6 +167,7 @@ message TimeoutVote { option (wireguard.sized) = true; optional View view = 1; // required optional uint64 latest_prepare_qc_view_number = 2; // optional + optional uint64 epoch_index = 3; // epoch this vote belongs to } message TimeoutQC { @@ -221,6 +224,8 @@ message AppProposal { optional uint64 road_index = 2; // required // App hash at that block. optional bytes app_hash = 3 [(wireguard.max_size) = 32]; // required + // Epoch this proposal belongs to. + optional uint64 epoch_index = 4; } // This is the signable message. diff --git a/sei-tendermint/internal/autobahn/avail/block_votes.go b/sei-tendermint/internal/autobahn/avail/block_votes.go index ae64772803..ba84945e1e 100644 --- a/sei-tendermint/internal/autobahn/avail/block_votes.go +++ b/sei-tendermint/internal/autobahn/avail/block_votes.go @@ -17,7 +17,9 @@ func newBlockVotes() blockVotes { } // Returns true iff a new QC has been constructed. -func (bv blockVotes) pushVote(c *types.Committee, vote *types.Signed[*types.LaneVote]) (*types.LaneQC, bool) { +// TODO: handle epoch transitions — weight must be counted per-epoch committee once multi-epoch is wired up. +func (bv blockVotes) pushVote(ep *types.Epoch, vote *types.Signed[*types.LaneVote]) (*types.LaneQC, bool) { + c := ep.Committee() k := vote.Key() h := vote.Msg().Header().Hash() if _, ok := bv.byKey[k]; ok { diff --git a/sei-tendermint/internal/autobahn/avail/conv_test.go b/sei-tendermint/internal/autobahn/avail/conv_test.go index 2bec6a4042..77b354ada9 100644 --- a/sei-tendermint/internal/autobahn/avail/conv_test.go +++ b/sei-tendermint/internal/autobahn/avail/conv_test.go @@ -4,25 +4,28 @@ 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" + "google.golang.org/protobuf/proto" ) 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), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) - require.NoError(t, PruneAnchorConv.Test(&PruneAnchor{ - AppQC: appQC, - CommitQC: commitQC, - })) + anchor := &PruneAnchor{AppQC: appQC, CommitQC: commitQC} + pb1 := PruneAnchorConv.Encode(anchor) + decoded, err := PruneAnchorConv.Decode(pb1) + require.NoError(t, err) + require.True(t, proto.Equal(pb1, PruneAnchorConv.Encode(decoded))) } diff --git a/sei-tendermint/internal/autobahn/avail/inner.go b/sei-tendermint/internal/autobahn/avail/inner.go index 601ca4611c..bb67d4d18d 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/libs/utils" ) @@ -15,6 +16,7 @@ import ( // BlockPersister creates lane WALs lazily inside MaybePruneAndPersistLane, but the new // member must also appear in inner.blocks before the next persist cycle. type inner struct { + ep *types.Epoch latestAppQC utils.Option[*types.AppQC] latestCommitQC utils.AtomicSend[utils.Option[*types.CommitQC]] appVotes *queue[types.GlobalBlockNumber, appVotes] @@ -55,25 +57,28 @@ type loadedAvailState struct { blocks map[types.LaneID][]persist.LoadedBlock } -func newInner(c *types.Committee, loaded utils.Option[*loadedAvailState]) (*inner, error) { +func newInner(ep *types.Epoch, loaded utils.Option[*loadedAvailState]) (*inner, error) { + pruneCommittee := ep.Committee() + 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() { + for lane := range ep.Committee().Lanes().All() { votes[lane] = newQueue[types.BlockNumber, blockVotes]() blocks[lane] = newQueue[types.BlockNumber, *types.Signed[*types.LaneProposal]]() } i := &inner{ + ep: ep, latestAppQC: utils.None[*types.AppQC](), latestCommitQC: utils.NewAtomicSend(utils.None[*types.CommitQC]()), appVotes: newQueue[types.GlobalBlockNumber, appVotes](), 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(ep.FirstBlock()) l, ok := loaded.Get() if !ok { @@ -88,7 +93,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, anchor.AppQC, anchor.CommitQC); err != nil { return nil, fmt.Errorf("prune: %w", err) } for lane := range i.blocks { @@ -143,7 +148,9 @@ 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) { +// TODO: filter votes per-epoch committee once epoch transitions are wired up. +func (i *inner) laneQC(ep *types.Epoch, lane types.LaneID, n types.BlockNumber) (*types.LaneQC, bool) { + c := ep.Committee() for _, byHash := range i.votes[lane].q[n].byHash { if byHash.weight >= c.LaneQuorum() { return types.NewLaneQC(byHash.votes[:]), true @@ -167,7 +174,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..7701317e2d 100644 --- a/sei-tendermint/internal/autobahn/avail/inner_test.go +++ b/sei-tendermint/internal/autobahn/avail/inner_test.go @@ -1,19 +1,20 @@ package avail import ( - pb "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/pb" "testing" "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/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,12 +23,12 @@ 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)) + ap := types.NewAppProposal(gr.First, qcForIndex.Index(), types.GenAppHash(rng), 0) 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.LatestEpoch().Committee(), 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.LatestEpoch().Committee(), 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.LatestEpoch(), 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.LatestEpoch().Committee().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,9 +86,9 @@ 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)) + appProposal := types.NewAppProposal(42, 5, types.GenAppHash(rng), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) _, err := PruneAnchorConv.Decode(&pb.PersistedAvailPruneAnchor{ @@ -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.LatestEpoch(), 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.LatestEpoch(), 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.LatestEpoch().Committee().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.LatestEpoch(), 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.LatestEpoch(), utils.Some(loaded)) require.NoError(t, err) - for lane := range committee.Lanes().All() { + for lane := range registry.LatestEpoch().Committee().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.LatestEpoch(), 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.LatestEpoch(), utils.Some(loaded)) require.NoError(t, err) // Without anchor, commitQCs.first = 0. All 3 should be restored. @@ -271,19 +272,19 @@ 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) globalNum := types.GlobalBlockNumber(10) - appProposal := types.NewAppProposal(globalNum, roadIdx, types.GenAppHash(rng)) + appProposal := types.NewAppProposal(globalNum, roadIdx, types.GenAppHash(rng), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) // Create 5 sequential CommitQCs (indices 0-4). 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.LatestEpoch(), utils.Some(loaded)) require.NoError(t, err) // latestAppQC should be set by prune. @@ -323,19 +324,19 @@ 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. roadIdx := types.RoadIndex(2) - appProposal := types.NewAppProposal(10, roadIdx, types.GenAppHash(rng)) + appProposal := types.NewAppProposal(10, roadIdx, types.GenAppHash(rng), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) // CommitQCs 0-4. 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.LatestEpoch(), 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.LatestEpoch(), 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.LatestEpoch().Committee(), keys, registry.LatestEpoch().Committee().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]) } @@ -426,10 +427,10 @@ func TestPruneAdvancesNextBlockToPersist(t *testing.T) { "CommitQC lane range should reference blocks for this test to be meaningful") // AppQC at index 2 → prune will fast-forward blocks past the cursor. - appProposal := types.NewAppProposal(10, 2, types.GenAppHash(rng)) + appProposal := types.NewAppProposal(10, 2, types.GenAppHash(rng), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) - updated, err := i.prune(committee, appQC, qcs[2]) + updated, err := i.prune(registry.LatestEpoch().Committee(), 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,18 +454,18 @@ 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]) } - appProposal := types.NewAppProposal(20, 5, types.GenAppHash(rng)) + appProposal := types.NewAppProposal(20, 5, types.GenAppHash(rng), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) loaded := &loadedAvailState{ pruneAnchor: utils.Some(&PruneAnchor{AppQC: appQC, CommitQC: qcs[5]}), } - inner, err := newInner(committee, utils.Some(loaded)) + inner, err := newInner(registry.LatestEpoch(), utils.Some(loaded)) require.NoError(t, err) // prune() pushes the anchor's CommitQC into the queue. @@ -475,25 +476,25 @@ 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]) } - appProposal := types.NewAppProposal(20, 3, types.GenAppHash(rng)) + appProposal := types.NewAppProposal(20, 3, types.GenAppHash(rng), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) loaded := &loadedAvailState{ pruneAnchor: utils.Some(&PruneAnchor{AppQC: appQC, CommitQC: qcs[3]}), } - inner, err := newInner(committee, utils.Some(loaded)) + inner, err := newInner(registry.LatestEpoch(), 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.LatestEpoch().Committee().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.LatestEpoch(), 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.LatestEpoch(), 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,11 +569,11 @@ 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]) } - appProposal := types.NewAppProposal(50, 10, types.GenAppHash(rng)) + appProposal := types.NewAppProposal(50, 10, types.GenAppHash(rng), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) loadedQCs := []persist.LoadedCommitQC{ @@ -584,7 +585,7 @@ func TestNewInnerLoadedCommitQCsGapWithAppQCAnchor(t *testing.T) { commitQCs: loadedQCs, } - inner, err := newInner(committee, utils.Some(loaded)) + inner, err := newInner(registry.LatestEpoch(), 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,11 +614,11 @@ 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]) } - appProposal := types.NewAppProposal(20, 3, types.GenAppHash(rng)) + appProposal := types.NewAppProposal(20, 3, types.GenAppHash(rng), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) loadedQCs := []persist.LoadedCommitQC{ @@ -633,7 +634,7 @@ func TestNewInnerLoadedCommitQCsBelowAnchorSkipped(t *testing.T) { commitQCs: loadedQCs, } - inner, err := newInner(committee, utils.Some(loaded)) + inner, err := newInner(registry.LatestEpoch(), 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,11 +655,11 @@ 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]) } - appProposal := types.NewAppProposal(10, 2, types.GenAppHash(rng)) + appProposal := types.NewAppProposal(10, 2, types.GenAppHash(rng), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) loadedQCs := []persist.LoadedCommitQC{ @@ -672,14 +673,14 @@ func TestNewInnerLoadedCommitQCsGapAfterAnchorReturnsError(t *testing.T) { commitQCs: loadedQCs, } - _, err := newInner(committee, utils.Some(loaded)) + _, err := newInner(registry.LatestEpoch(), 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.LatestEpoch(), 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.LatestEpoch(), 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,26 +751,26 @@ func TestNewInnerLoadedBlocksOverCapacityReturnsError(t *testing.T) { blocks: map[types.LaneID][]persist.LoadedBlock{lane: bs}, } - _, err := newInner(committee, utils.Some(loaded)) + _, err := newInner(registry.LatestEpoch(), 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]) } // AppQC at road index 2, prune anchor is CommitQC[2]. - appProposal := types.NewAppProposal(initialBlock, 2, types.GenAppHash(rng)) + appProposal := types.NewAppProposal(initialBlock, 2, types.GenAppHash(rng), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) pruneQC := qcs[2] @@ -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.LatestEpoch(), 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.LatestEpoch().Committee().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,19 +807,19 @@ 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]) } // AppQC at road index 1, prune anchor is CommitQC[1]. - appProposal := types.NewAppProposal(initialBlock, 1, types.GenAppHash(rng)) + appProposal := types.NewAppProposal(initialBlock, 1, types.GenAppHash(rng), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) loaded := &loadedAvailState{ @@ -829,7 +830,7 @@ func TestNewInnerPruneAnchorCommitQCUsedForPrune(t *testing.T) { }, } - i, err := newInner(committee, utils.Some(loaded)) + i, err := newInner(registry.LatestEpoch(), 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 cf8e1948bd..f707385bf5 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" @@ -155,7 +156,8 @@ func NewState(key types.SecretKey, data *data.State, stateDir utils.Option[strin return nil, err } - inner, err := newInner(data.Committee(), loaded) + ep := data.Registry().LatestEpoch() + inner, err := newInner(ep, loaded) if err != nil { return nil, err } @@ -164,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 ep.Committee().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) } @@ -260,7 +262,11 @@ func (s *State) PushCommitQC(ctx context.Context, qc *types.CommitQC) error { return err } } - if err := qc.Verify(s.data.Committee()); err != nil { + ep, err := s.data.Registry().EpochForProposal(qc.Proposal()) + if err != nil { + return fmt.Errorf("qc.Verify(): %w", err) + } + if err := qc.Verify(ep); err != nil { return fmt.Errorf("qc.Verify(): %w", err) } for inner, ctrl := range s.inner.Lock() { @@ -268,6 +274,12 @@ func (s *State) PushCommitQC(ctx context.Context, qc *types.CommitQC) error { return nil } inner.commitQCs.pushBack(qc) + // TODO: rotate inner.ep when dynamic committees are wired up. + // if idx >= inner.ep.Roads().Last { + // next, ok := s.data.Registry().EpochByIndex(inner.ep.EpochIndex() + 1) + // if !ok { panic("epoch not registered") } + // inner.ep = next + // } // The persist goroutine publishes latestCommitQC after writing to disk // (or immediately for no-op persisters), so consensus won't advance // until the CommitQC is durable. @@ -279,10 +291,15 @@ 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 { - return fmt.Errorf("v.VerifySig(): %w", err) + ep, ok := s.data.Registry().EpochByIndex(epoch.Index(v.Msg().Proposal().EpochIndex())) + if !ok { + return fmt.Errorf("unknown epoch_index %d", v.Msg().Proposal().EpochIndex()) } + committee := ep.Committee() idx := v.Msg().Proposal().RoadIndex() + if err := v.VerifySig(committee); err != nil { + return fmt.Errorf("v.VerifySig(): %w", err) + } // Wait for the corresponding commitQC. if err := s.waitForCommitQC(ctx, idx); err != nil { return err @@ -294,7 +311,10 @@ 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 got, want := v.Msg().Proposal().EpochIndex(), qc.Proposal().EpochIndex(); got != want { + return fmt.Errorf("app_proposal epoch_index %d != commit_qc epoch_index %d", got, want) + } + if err := v.Msg().Proposal().Verify(committee, qc); err != nil { return fmt.Errorf("invalid vote: %w", err) } // Push the vote. @@ -303,11 +323,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, appQC, qc) if err != nil { return err } @@ -327,23 +347,29 @@ func (s *State) PushAppQC(appQC *types.AppQC, commitQC *types.CommitQC) error { return nil } } - c := s.data.Committee() - if err := appQC.Verify(c); err != nil { + ep, err := s.data.Registry().EpochForProposal(commitQC.Proposal()) + if err != nil { + return fmt.Errorf("unknown epoch: %w", err) + } + if err := appQC.Verify(ep.Committee()); err != nil { return fmt.Errorf("appQC.Verify(): %w", err) } - if err := commitQC.Verify(c); err != nil { + if err := commitQC.Verify(ep); err != nil { return fmt.Errorf("commitQC.Verify(): %w", err) } if appQC.Proposal().RoadIndex() != commitQC.Proposal().Index() { return fmt.Errorf("mismatched QCs: appQC index %v, commitQC index %v", appQC.Proposal().RoadIndex(), commitQC.Proposal().Index()) } + if got, want := appQC.Proposal().EpochIndex(), commitQC.Proposal().EpochIndex(); got != want { + return fmt.Errorf("appQC epoch_index %d != commitQC epoch_index %d", got, want) + } // 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(ep.Committee(), appQC, commitQC) if err != nil { return err } @@ -391,12 +417,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 { @@ -441,11 +469,13 @@ 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) } h := vote.Msg().Header() for inner, ctrl := range s.inner.Lock() { @@ -464,7 +494,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 _, ok := q.q[h.BlockNumber()].pushVote(inner.ep, vote); ok { ctrl.Updated() } } @@ -489,8 +519,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 @@ -513,7 +543,11 @@ 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() { + ep, err := s.data.Registry().EpochForProposal(qc.Proposal()) + if err != nil { + return nil, fmt.Errorf("unknown epoch: %w", err) + } + for lane := range ep.Committee().Lanes().All() { headers, err := s.headers(ctx, qc.LaneRange(lane)) if err != nil { return nil, err @@ -536,18 +570,18 @@ 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], ep *types.Epoch, ) (map[types.LaneID]*types.LaneQC, error) { - c := s.data.Committee() 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(types.MaxLaneRangeInProposal) { - if qc, ok := inner.laneQC(c, lane, first+i); ok { + if qc, ok := inner.laneQC(ep, lane, first+i); ok { laneQCs[lane] = qc } else { break @@ -611,7 +645,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 { @@ -622,6 +655,11 @@ func (s *State) Run(ctx context.Context) error { } // Collect the blocks we have locally. + ep, err := s.data.Registry().EpochForProposal(qc.QC().Proposal()) + if err != nil { + return fmt.Errorf("unknown epoch: %w", err) + } + c := ep.Committee() var blocks []*types.Block for inner := range s.inner.Lock() { for lane := range c.Lanes().All() { @@ -684,7 +722,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) @@ -698,7 +736,7 @@ func (s *State) runPersist(ctx context.Context, pers persisters) error { s.markCommitQCsPersisted(qc) })) }) - for lane := range s.data.Committee().Lanes().All() { + for lane := range s.data.Registry().LatestEpoch().Committee().Lanes().All() { proposals := blocksByLane[lane] ps.Spawn(func() error { return pers.blocks.MaybePruneAndPersistLane(lane, anchorQC, proposals, utils.Some(markBlock)) diff --git a/sei-tendermint/internal/autobahn/avail/state_test.go b/sei-tendermint/internal/autobahn/avail/state_test.go index 4bb2d3e2fa..22b3827451 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,14 @@ 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, Epoch: registry.LatestEpoch()} + return types.BuildCommitQC(vs.Epoch.Committee(), keys, prev, registry.FirstBlock(), time.Time{}, laneQCs, appQC) } func qcPayloadHashes(qc *types.FullCommitQC) byLane[types.PayloadHash] { @@ -102,12 +90,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.LatestEpoch().Committee() 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 +143,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, registry.LatestEpoch()) 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), 0) for _, vote := range makeAppVotes(keys, appProposal) { if err := state.PushAppVote(ctx, vote); err != nil { return fmt.Errorf("state.PushAppVote(): %w", err) @@ -200,7 +189,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 +217,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.LatestEpoch().Committee() dir := t.TempDir() // Phase 1: Run state with persistence through 2 iterations. @@ -236,7 +226,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 +264,16 @@ func TestStateRestartFromPersisted(t *testing.T) { } } - laneQCs, err := state.WaitForLaneQCs(ctx, prev) + laneQCs, err := state.WaitForLaneQCs(ctx, prev, registry.LatestEpoch()) 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), 0) for _, vote := range makeAppVotes(keys, appProposal) { if err := state.PushAppVote(ctx, vote); err != nil { return fmt.Errorf("PushAppVote: %w", err) @@ -311,7 +301,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,21 +322,21 @@ 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.LatestEpoch().Committee() + 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, Epoch: types.NewEpoch(0, types.OpenRoadRange(), time.Time{}, committee, initialBlock)} fullProposal := utils.OrPanic1(types.NewProposal( leaderKey(committee, keys, vs.View()), - committee, vs, time.Now(), laneQCs, @@ -374,13 +364,13 @@ 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) // AppQC for index 1, but paired with CommitQC for index 0 - appProposal1 := types.NewAppProposal(initialBlock, 1, types.GenAppHash(rng)) + appProposal1 := types.NewAppProposal(initialBlock, 1, types.GenAppHash(rng), 0) appQC1 := types.NewAppQC(makeAppVotes(keys, appProposal1)) err := state.PushAppQC(appQC1, qc0) @@ -391,11 +381,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 +406,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 +424,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,11 +442,11 @@ 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) - appProposal := types.NewAppProposal(globalNum, roadIdx, types.GenAppHash(rng)) + appProposal := types.NewAppProposal(globalNum, roadIdx, types.GenAppHash(rng), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) // Persist commitQCs 0-7 so the matching one at roadIdx exists. @@ -465,7 +455,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 +483,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,12 +507,12 @@ 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) globalNum := types.GlobalBlockNumber(5) - appProposal := types.NewAppProposal(globalNum, roadIdx, types.GenAppHash(rng)) + appProposal := types.NewAppProposal(globalNum, roadIdx, types.GenAppHash(rng), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) // Persist commitQCs 0-2 so the matching one at roadIdx exists. @@ -531,7 +521,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 +560,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 +569,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)) } @@ -590,17 +580,19 @@ func TestNewStateWithPersistence(t *testing.T) { // All 3 commitQCs should be loaded (no AppQC to skip past). require.Equal(t, types.RoadIndex(0), state.FirstCommitQC()) // LastCommitQC should be set to the last loaded one. - require.NoError(t, utils.TestDiff(utils.Some(qcs[2]), state.LastCommitQC().Load())) + got2, ok2 := state.LastCommitQC().Load().Get() + require.True(t, ok2) + requireCommitQCEqual(t, qcs[2], got2) }) 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) globalNum := types.GlobalBlockNumber(5) - appProposal := types.NewAppProposal(globalNum, roadIdx, types.GenAppHash(rng)) + appProposal := types.NewAppProposal(globalNum, roadIdx, types.GenAppHash(rng), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) // Persist CommitQCs 0-4. @@ -610,7 +602,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)) } @@ -628,7 +620,9 @@ func TestNewStateWithPersistence(t *testing.T) { // inner.prune(appQC@1, commitQC@1) sets commitQCs.first = 1. require.Equal(t, types.RoadIndex(1), state.FirstCommitQC()) - require.NoError(t, utils.TestDiff(utils.Some(qcs[4]), state.LastCommitQC().Load())) + got4, ok4 := state.LastCommitQC().Load().Get() + require.True(t, ok4) + requireCommitQCEqual(t, qcs[4], got4) }) t.Run("non-contiguous commitQC files return error", func(t *testing.T) { @@ -638,12 +632,12 @@ 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]) } // Persist prune anchor (AppQC + CommitQC pair at road index 0). - appProposal := types.NewAppProposal(initialBlock, 0, types.GenAppHash(rng)) + appProposal := types.NewAppProposal(initialBlock, 0, types.GenAppHash(rng), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) prunePers, _, err := persist.NewPersister[*pb.PersistedAvailPruneAnchor](utils.Some(dir), innerFile) require.NoError(t, err) @@ -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]) } @@ -687,7 +681,7 @@ func TestNewStateWithPersistence(t *testing.T) { require.NoError(t, cp.Close()) // Persist a prune anchor at index 9 — well past the persisted range. - appProposal := types.NewAppProposal(50, 9, types.GenAppHash(rng)) + appProposal := types.NewAppProposal(50, 9, types.GenAppHash(rng), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) prunePers, _, err := persist.NewPersister[*pb.PersistedAvailPruneAnchor](utils.Some(dir), innerFile) require.NoError(t, err) @@ -702,7 +696,9 @@ func TestNewStateWithPersistence(t *testing.T) { require.NoError(t, err) require.Equal(t, types.RoadIndex(9), state.FirstCommitQC()) - require.NoError(t, utils.TestDiff(utils.Some(qcs[9]), state.LastCommitQC().Load())) + got9, ok9 := state.LastCommitQC().Load().Get() + require.True(t, ok9) + requireCommitQCEqual(t, qcs[9], got9) got, ok := state.LastAppQC().Get() require.True(t, ok) @@ -711,7 +707,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 +716,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)) } @@ -738,7 +734,7 @@ func TestNewStateWithPersistence(t *testing.T) { // Persist a prune anchor at index 9 with a laneRange that starts past // all persisted blocks — MaybePruneAndPersistLane will TruncateAll the block WAL. - appProposal := types.NewAppProposal(50, 9, types.GenAppHash(rng)) + appProposal := types.NewAppProposal(50, 9, types.GenAppHash(rng), 0) appQC := types.NewAppQC(makeAppVotes(keys, appProposal)) prunePers, _, err := persist.NewPersister[*pb.PersistedAvailPruneAnchor](utils.Some(dir), innerFile) require.NoError(t, err) @@ -759,7 +755,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/avail/testhelpers_test.go b/sei-tendermint/internal/autobahn/avail/testhelpers_test.go new file mode 100644 index 0000000000..01a15ca2cc --- /dev/null +++ b/sei-tendermint/internal/autobahn/avail/testhelpers_test.go @@ -0,0 +1,14 @@ +package avail + +import ( + "testing" + + "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" + "google.golang.org/protobuf/proto" +) + +func requireCommitQCEqual(t *testing.T, want, got *types.CommitQC) { + t.Helper() + require.True(t, proto.Equal(types.CommitQCConv.Encode(want), types.CommitQCConv.Encode(got))) +} diff --git a/sei-tendermint/internal/autobahn/consensus/inner.go b/sei-tendermint/internal/autobahn/consensus/inner.go index 6cc197cbbd..39791bdf38 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" @@ -93,12 +94,13 @@ const innerFile = "inner" type inner struct { persistedInner + ep *types.Epoch } // 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,20 +111,22 @@ func newInner(data utils.Option[*pb.PersistedInner], committee *types.Committee) persisted = *decoded } - if err := persisted.validate(committee); err != nil { + ep := registry.LatestEpoch() + if err := persisted.validate(ep); err != nil { return inner{}, err } logger.Info("restored consensus state", "state", innerProtoConv.Encode(&persisted)) - return inner{persistedInner: persisted}, nil + return inner{persistedInner: persisted, ep: ep}, nil } func (s *State) pushCommitQC(qc *types.CommitQC) error { - if i := s.innerRecv.Load(); qc.Proposal().Index() < i.View().Index { + i := s.innerRecv.Load() + if qc.Proposal().Index() < i.View().Index { return nil } - if err := qc.Verify(s.Data().Committee()); err != nil { + if err := qc.Verify(i.ep); err != nil { return fmt.Errorf("qc.Verify(): %w", err) } for iSend := range s.inner.Lock() { @@ -130,10 +134,9 @@ func (s *State) pushCommitQC(qc *types.CommitQC) error { if qc.Proposal().Index() < i.View().Index { return nil } - // CommitQC advances to new index; clear all state for new view - iSend.Store(inner{persistedInner{ - CommitQC: utils.Some(qc), - }}) + // CommitQC advances to new index; clear all state for new view. + // TODO: rotate ep when epoch transitions are wired up. + iSend.Store(inner{persistedInner: persistedInner{CommitQC: utils.Some(qc)}, ep: i.ep}) } return nil } @@ -151,7 +154,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(i.ep, i.CommitQC); err != nil { return fmt.Errorf("qc.Verify(): %w", err) } for isend := range s.inner.Lock() { @@ -160,10 +163,7 @@ func (s *State) pushTimeoutQC(ctx context.Context, qc *types.TimeoutQC) error { return nil } // TimeoutQC advances view number; clear votes and prepareQC (stale view). - isend.Store(inner{persistedInner{ - CommitQC: i.CommitQC, - TimeoutQC: utils.Some(qc), - }}) + isend.Store(inner{persistedInner: persistedInner{CommitQC: i.CommitQC, TimeoutQC: utils.Some(qc)}, ep: i.ep}) } return nil } @@ -179,7 +179,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(vs); err != nil { return fmt.Errorf("proposal.Verify(): %w", err) } // Update. @@ -205,7 +205,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(vs.Epoch); err != nil { return fmt.Errorf("qc.Verify(): %w", err) } // Update. @@ -240,7 +240,7 @@ func (s *State) voteTimeout(ctx context.Context, view types.View) error { if tqc, ok := i.TimeoutQC.Get(); ok && !pqc.IsPresent() { pqc = tqc.LatestPrepareQC() } - v := types.NewFullTimeoutVote(s.cfg.Key, view, pqc) + v := types.NewFullTimeoutVote(s.cfg.Key, view, pqc, i.ep.EpochIndex()) i.TimeoutVote = utils.Some(v) isend.Store(i) } diff --git a/sei-tendermint/internal/autobahn/consensus/inner_test.go b/sei-tendermint/internal/autobahn/consensus/inner_test.go index 761e528d36..dfca0191c0 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,9 +62,9 @@ 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}) + genesisProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 0, Number: 0}) vote := types.Sign(key, types.NewPrepareVote(genesisProposal)) seedPersistedInner(dir, &persistedInner{ @@ -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,9 +84,9 @@ 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}) + genesisProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 0, Number: 0}) prepareQC := makePrepareQC([]types.SecretKey{key}, genesisProposal) vote := types.Sign(key, types.NewCommitVote(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,16 +108,16 @@ 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]()) + vote := types.NewFullTimeoutVote(key, types.View{Index: 0, Number: 0}, utils.None[*types.PrepareQC](), 0) seedPersistedInner(dir, &persistedInner{ TimeoutVote: utils.Some(vote), }) // 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,13 +129,13 @@ 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}) + genesisProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 0, Number: 0}) prepareQC := makePrepareQC([]types.SecretKey{key}, genesisProposal) prepareVote := types.Sign(key, types.NewPrepareVote(genesisProposal)) commitVote := types.Sign(key, types.NewCommitVote(genesisProposal)) - timeoutVote := types.NewFullTimeoutVote(key, types.View{Index: 0, Number: 0}, utils.None[*types.PrepareQC]()) + timeoutVote := types.NewFullTimeoutVote(key, types.View{Index: 0, Number: 0}, utils.None[*types.PrepareQC](), 0) seedPersistedInner(dir, &persistedInner{ PrepareQC: utils.Some(prepareQC), @@ -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,9 +157,9 @@ 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}) + genesisProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 0, Number: 0}) prepareVote := types.Sign(key, types.NewPrepareVote(genesisProposal)) seedPersistedInner(dir, &persistedInner{ @@ -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,10 +177,10 @@ 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}) + proposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) vote := types.NewCommitVote(proposal) var votes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -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,10 +206,10 @@ 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}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -219,7 +220,7 @@ func TestNewInnerTimeoutQC(t *testing.T) { // Create TimeoutQC at (6, 2) - this advances view to (6, 3) var timeoutVotes []*types.FullTimeoutVote for _, k := range keys { - timeoutVotes = append(timeoutVotes, types.NewFullTimeoutVote(k, types.View{Index: 6, Number: 2}, utils.None[*types.PrepareQC]())) + timeoutVotes = append(timeoutVotes, types.NewFullTimeoutVote(k, types.View{Index: 6, Number: 2}, utils.None[*types.PrepareQC](), 0)) } timeoutQC := types.NewTimeoutQC(timeoutVotes) @@ -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,12 +240,12 @@ 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 for _, k := range keys { - timeoutVotes = append(timeoutVotes, types.NewFullTimeoutVote(k, types.View{Index: 0, Number: 2}, utils.None[*types.PrepareQC]())) + timeoutVotes = append(timeoutVotes, types.NewFullTimeoutVote(k, types.View{Index: 0, Number: 2}, utils.None[*types.PrepareQC](), 0)) } timeoutQC := types.NewTimeoutQC(timeoutVotes) @@ -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,12 +263,12 @@ 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 for _, k := range keys { - timeoutVotes = append(timeoutVotes, types.NewFullTimeoutVote(k, types.View{Index: 6, Number: 0}, utils.None[*types.PrepareQC]())) + timeoutVotes = append(timeoutVotes, types.NewFullTimeoutVote(k, types.View{Index: 6, Number: 0}, utils.None[*types.PrepareQC](), 0)) } timeoutQC := types.NewTimeoutQC(timeoutVotes) @@ -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,10 +285,10 @@ 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}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -298,7 +299,7 @@ func TestNewInnerTimeoutQCAheadOfCommitQCError(t *testing.T) { // Create TimeoutQC at index 10 (way ahead of CommitQC) var timeoutVotes []*types.FullTimeoutVote for _, k := range keys { - timeoutVotes = append(timeoutVotes, types.NewFullTimeoutVote(k, types.View{Index: 10, Number: 0}, utils.None[*types.PrepareQC]())) + timeoutVotes = append(timeoutVotes, types.NewFullTimeoutVote(k, types.View{Index: 10, Number: 0}, utils.None[*types.PrepareQC](), 0)) } timeoutQC := types.NewTimeoutQC(timeoutVotes) @@ -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,10 +317,10 @@ 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}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 10, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -331,7 +332,7 @@ func TestNewInnerViewSpecStaleTimeoutQC(t *testing.T) { // Since inner is persisted atomically, a mismatched index is always corrupt. var timeoutVotes []*types.FullTimeoutVote for _, k := range keys { - timeoutVotes = append(timeoutVotes, types.NewFullTimeoutVote(k, types.View{Index: 5, Number: 2}, utils.None[*types.PrepareQC]())) + timeoutVotes = append(timeoutVotes, types.NewFullTimeoutVote(k, types.View{Index: 5, Number: 2}, utils.None[*types.PrepareQC](), 0)) } timeoutQC := types.NewTimeoutQC(timeoutVotes) @@ -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,10 +350,10 @@ 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}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -363,7 +364,7 @@ func TestNewInnerViewSpecValidBothQCs(t *testing.T) { // Create TimeoutQC at index 6, number 2 (valid - exactly CommitQC.Index + 1) var timeoutVotes []*types.FullTimeoutVote for _, k := range keys { - timeoutVotes = append(timeoutVotes, types.NewFullTimeoutVote(k, types.View{Index: 6, Number: 2}, utils.None[*types.PrepareQC]())) + timeoutVotes = append(timeoutVotes, types.NewFullTimeoutVote(k, types.View{Index: 6, Number: 2}, utils.None[*types.PrepareQC](), 0)) } timeoutQC := types.NewTimeoutQC(timeoutVotes) @@ -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,10 +385,10 @@ 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}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -397,7 +398,7 @@ func TestNewInnerStaleVoteError(t *testing.T) { // Create stale vote at view (3, 0) - before current view (6, 0). // Since inner is persisted atomically, a mismatched view is corrupt. - staleProposal := types.GenProposalAt(rng, types.View{Index: 3, Number: 0}) + staleProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 3, Number: 0}) staleVote := types.Sign(keys[0], types.NewPrepareVote(staleProposal)) seedPersistedInner(dir, &persistedInner{ @@ -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,10 +414,10 @@ 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}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -425,7 +426,7 @@ func TestNewInnerFuturePrepareVoteError(t *testing.T) { commitQC := types.NewCommitQC(qcVotes) // Create future vote at view (10, 0) - ahead of current view (6, 0) - futureProposal := types.GenProposalAt(rng, types.View{Index: 10, Number: 0}) + futureProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 10, Number: 0}) futureVote := types.Sign(keys[0], types.NewPrepareVote(futureProposal)) seedPersistedInner(dir, &persistedInner{ @@ -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,10 +443,10 @@ 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}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -454,7 +455,7 @@ func TestNewInnerFutureCommitVoteError(t *testing.T) { commitQC := types.NewCommitQC(qcVotes) // Create future commit vote at view (10, 0) - futureProposal := types.GenProposalAt(rng, types.View{Index: 10, Number: 0}) + futureProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 10, Number: 0}) futureVote := types.Sign(keys[0], types.NewCommitVote(futureProposal)) seedPersistedInner(dir, &persistedInner{ @@ -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,10 +472,10 @@ 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}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -483,7 +484,7 @@ func TestNewInnerFutureTimeoutVoteError(t *testing.T) { commitQC := types.NewCommitQC(qcVotes) // Create future timeout vote at view (10, 0) - futureVote := types.NewFullTimeoutVote(keys[0], types.View{Index: 10, Number: 0}, utils.None[*types.PrepareQC]()) + futureVote := types.NewFullTimeoutVote(keys[0], types.View{Index: 10, Number: 0}, utils.None[*types.PrepareQC](), 0) seedPersistedInner(dir, &persistedInner{ CommitQC: utils.Some(commitQC), @@ -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,10 +500,10 @@ 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}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -511,7 +512,7 @@ func TestNewInnerCurrentViewVoteOk(t *testing.T) { commitQC := types.NewCommitQC(qcVotes) // Create vote at exactly current view (6, 0) - currentProposal := types.GenProposalAt(rng, types.View{Index: 6, Number: 0}) + currentProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 6, Number: 0}) currentVote := types.Sign(keys[0], types.NewPrepareVote(currentProposal)) seedPersistedInner(dir, &persistedInner{ @@ -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,14 +529,14 @@ 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) for i := range otherKeys { otherKeys[i] = types.GenSecretKey(rng) } - proposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) + proposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) vote := types.NewCommitVote(proposal) var votes []*types.Signed[*types.CommitVote] for _, k := range otherKeys { @@ -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,10 +557,10 @@ 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}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -574,7 +575,7 @@ func TestNewInnerTimeoutQCInvalidSignatureError(t *testing.T) { } var timeoutVotes []*types.FullTimeoutVote for _, k := range otherKeys { - timeoutVotes = append(timeoutVotes, types.NewFullTimeoutVote(k, types.View{Index: 6, Number: 0}, utils.None[*types.PrepareQC]())) + timeoutVotes = append(timeoutVotes, types.NewFullTimeoutVote(k, types.View{Index: 6, Number: 0}, utils.None[*types.PrepareQC](), 0)) } timeoutQC := types.NewTimeoutQC(timeoutVotes) @@ -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,10 +593,10 @@ 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}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -605,7 +606,7 @@ func TestNewInnerCurrentViewVoteInvalidSignatureError(t *testing.T) { // Create vote at current view (6, 0) but signed by key NOT in committee otherKey := types.GenSecretKey(rng) - currentProposal := types.GenProposalAt(rng, types.View{Index: 6, Number: 0}) + currentProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 6, Number: 0}) badVote := types.Sign(otherKey, types.NewPrepareVote(currentProposal)) seedPersistedInner(dir, &persistedInner{ @@ -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,10 +623,10 @@ 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}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -636,7 +637,7 @@ func TestNewInnerStaleVoteInvalidSignatureError(t *testing.T) { // Create stale vote at (3, 0) signed by key NOT in committee. // Since inner is persisted atomically, a mismatched view is corrupt. otherKey := types.GenSecretKey(rng) - staleProposal := types.GenProposalAt(rng, types.View{Index: 3, Number: 0}) + staleProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 3, Number: 0}) badVote := types.Sign(otherKey, types.NewPrepareVote(staleProposal)) seedPersistedInner(dir, &persistedInner{ @@ -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,10 +653,10 @@ 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}) + proposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 0, Number: 0}) prepareQC := makePrepareQC(keys, proposal) seedPersistedInner(dir, &persistedInner{ @@ -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,10 +672,10 @@ 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}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -684,7 +685,7 @@ func TestNewInnerStalePrepareQCError(t *testing.T) { // Create stale prepareQC at view (3, 0) - before current view (6, 0). // Since inner is persisted atomically, a mismatched view is corrupt. - staleProposal := types.GenProposalAt(rng, types.View{Index: 3, Number: 0}) + staleProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 3, Number: 0}) stalePrepareQC := makePrepareQC(keys, staleProposal) seedPersistedInner(dir, &persistedInner{ @@ -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,18 +701,18 @@ 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. - proposal := types.GenProposalAt(rng, types.View{Index: 0, Number: 0}) + proposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 0, Number: 0}) commitVote := types.Sign(keys[0], types.NewCommitVote(proposal)) seedPersistedInner(dir, &persistedInner{ 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,10 +720,10 @@ 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}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -731,7 +732,7 @@ func TestNewInnerFuturePrepareQCError(t *testing.T) { commitQC := types.NewCommitQC(qcVotes) // Create future prepareQC at index 10 (> current 6) - futureProposal := types.GenProposalAt(rng, types.View{Index: 10, Number: 0}) + futureProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 10, Number: 0}) prepareQC := makePrepareQC(keys, futureProposal) seedPersistedInner(dir, &persistedInner{ @@ -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,10 +749,10 @@ 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}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -760,7 +761,7 @@ func TestNewInnerCurrentViewPrepareQCOk(t *testing.T) { commitQC := types.NewCommitQC(qcVotes) // Create prepareQC at current view (6, 0) - currentProposal := types.GenProposalAt(rng, types.View{Index: 6, Number: 0}) + currentProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 6, Number: 0}) prepareQC := makePrepareQC(keys, currentProposal) seedPersistedInner(dir, &persistedInner{ @@ -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,10 +778,10 @@ 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}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -793,7 +794,7 @@ func TestNewInnerCurrentViewPrepareQCInvalidSignatureError(t *testing.T) { for i := range otherKeys { otherKeys[i] = types.GenSecretKey(rng) } - currentProposal := types.GenProposalAt(rng, types.View{Index: 6, Number: 0}) + currentProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 6, Number: 0}) prepareQC := makePrepareQC(otherKeys, currentProposal) seedPersistedInner(dir, &persistedInner{ @@ -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,11 +811,11 @@ 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) voteKey := keys[0] // Create CommitQC at index 5 -> current view is (6, 0) - qcProposal := types.GenProposalAt(rng, types.View{Index: 5, Number: 0}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -823,7 +824,7 @@ func TestNewInnerPrepareQCIncludedInTimeoutVote(t *testing.T) { commitQC := types.NewCommitQC(qcVotes) // Create prepareQC at current view (6, 0) - currentProposal := types.GenProposalAt(rng, types.View{Index: 6, Number: 0}) + currentProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 6, Number: 0}) prepareQC := makePrepareQC(keys, currentProposal) seedPersistedInner(dir, &persistedInner{ @@ -832,16 +833,16 @@ 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") // Simulate what voteTimeout does: create a FullTimeoutVote using i.PrepareQC currentView := i.View() - timeoutVote := types.NewFullTimeoutVote(voteKey, currentView, i.PrepareQC) + timeoutVote := types.NewFullTimeoutVote(voteKey, currentView, i.PrepareQC, registry.LatestEpoch().EpochIndex()) // The timeoutVote should pass verification (which checks prepareQC is correctly included) - err = timeoutVote.Verify(committee) + err = timeoutVote.Verify(registry.LatestEpoch()) require.NoError(t, err, "timeoutVote with loaded prepareQC should verify") // Verify the loaded prepareQC matches what we persisted @@ -855,10 +856,10 @@ 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}) + qcProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 5, Number: 0}) qcVote := types.NewCommitVote(qcProposal) var qcVotes []*types.Signed[*types.CommitVote] for _, k := range keys { @@ -867,13 +868,13 @@ func TestPushTimeoutQCClearsStaleState(t *testing.T) { commitQC := types.NewCommitQC(qcVotes) // Setup: Create prepareQC at current view (6, 0) - currentProposal := types.GenProposalAt(rng, types.View{Index: 6, Number: 0}) + currentProposal := types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 6, Number: 0}) prepareQC := makePrepareQC(keys, currentProposal) // Setup: Create votes at current view (6, 0) prepareVote := types.Sign(keys[0], types.NewPrepareVote(currentProposal)) commitVote := types.Sign(keys[0], types.NewCommitVote(currentProposal)) - timeoutVote := types.NewFullTimeoutVote(keys[0], types.View{Index: 6, Number: 0}, utils.Some(prepareQC)) + timeoutVote := types.NewFullTimeoutVote(keys[0], types.View{Index: 6, Number: 0}, utils.Some(prepareQC), 0) seedPersistedInner(dir, &persistedInner{ CommitQC: utils.Some(commitQC), @@ -884,7 +885,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") @@ -895,15 +896,12 @@ func TestPushTimeoutQCClearsStaleState(t *testing.T) { // Create a TimeoutQC for current view (6, 0) that advances to (6, 1) var timeoutVotes []*types.FullTimeoutVote for _, k := range keys { - timeoutVotes = append(timeoutVotes, types.NewFullTimeoutVote(k, types.View{Index: 6, Number: 0}, utils.Some(prepareQC))) + timeoutVotes = append(timeoutVotes, types.NewFullTimeoutVote(k, types.View{Index: 6, Number: 0}, utils.Some(prepareQC), 0)) } timeoutQC := types.NewTimeoutQC(timeoutVotes) // Simulate pushTimeoutQC's Update callback - newInner := inner{persistedInner{ - CommitQC: i.CommitQC, - TimeoutQC: utils.Some(timeoutQC), - }} + newInner := inner{persistedInner: persistedInner{CommitQC: i.CommitQC, TimeoutQC: utils.Some(timeoutQC)}, ep: i.ep} // Verify: view advanced to (6, 1) require.Equal(t, types.View{Index: 6, Number: 1}, newInner.View(), "view should advance to (6, 1)") @@ -924,8 +922,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..f41a538917 100644 --- a/sei-tendermint/internal/autobahn/consensus/persist/commitqcs_test.go +++ b/sei-tendermint/internal/autobahn/consensus/persist/commitqcs_test.go @@ -7,10 +7,17 @@ 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" + "google.golang.org/protobuf/proto" ) +func requireCommitQCEqual(t *testing.T, want, got *types.CommitQC) { + t.Helper() + require.True(t, proto.Equal(types.CommitQCConv.Encode(want), types.CommitQCConv.Encode(got))) +} + var noQC = utils.None[*types.CommitQC]() var noCommitQCCB = utils.None[func(*types.CommitQC)]() @@ -21,7 +28,7 @@ func testCommitQC( laneQCs map[types.LaneID]*types.LaneQC, appQC utils.Option[*types.AppQC], ) *types.CommitQC { - vs := types.ViewSpec{CommitQC: prev} + vs := types.ViewSpec{CommitQC: prev, Epoch: types.NewEpoch(0, types.OpenRoadRange(), time.Time{}, committee, 0)} leader := committee.Leader(vs.View()) var leaderKey types.SecretKey for _, k := range keys { @@ -32,7 +39,6 @@ func testCommitQC( } fullProposal := utils.OrPanic1(types.NewProposal( leaderKey, - committee, vs, time.Now(), laneQCs, @@ -106,7 +112,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.LatestEpoch().Committee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 3) @@ -126,7 +133,7 @@ func TestPersistCommitQCAndLoad(t *testing.T) { require.Equal(t, 3, len(loaded)) for i, lqc := range loaded { require.Equal(t, types.RoadIndex(i), lqc.Index) - require.NoError(t, utils.TestDiff(qcs[i], lqc.QC)) + requireCommitQCEqual(t, qcs[i], lqc.QC) } require.Equal(t, types.RoadIndex(3), cp2.LoadNext()) require.NoError(t, cp2.Close()) @@ -134,7 +141,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.LatestEpoch().Committee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 5) @@ -156,7 +164,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.LatestEpoch().Committee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 3) @@ -183,7 +192,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.LatestEpoch().Committee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 3) @@ -200,7 +210,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.LatestEpoch().Committee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 5) @@ -218,7 +229,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.LatestEpoch().Committee() dir := t.TempDir() // Build 3 sequential CommitQCs (indices 0, 1, 2). @@ -241,7 +253,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.LatestEpoch().Committee() qcs := makeSequentialCommitQCs(committee, keys, 11) // Fresh no-op persister: prune with anchor at index 0 (idx==0, @@ -274,7 +287,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.LatestEpoch().Committee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 12) @@ -305,7 +319,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.LatestEpoch().Committee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 12) @@ -348,7 +363,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.LatestEpoch().Committee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 5) @@ -381,12 +397,13 @@ func TestCommitQCDeleteBeforeWithAnchorRecovers(t *testing.T) { require.NoError(t, err) require.Equal(t, 1, len(loaded)) require.Equal(t, types.RoadIndex(4), loaded[0].Index) - require.NoError(t, utils.TestDiff(qcs[4], loaded[0].QC)) + requireCommitQCEqual(t, qcs[4], loaded[0].QC) } func TestCommitQCDeleteBeforeThenPersistMore(t *testing.T) { rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 4) + registry, keys := epoch.GenRegistry(rng, 4) + committee := registry.LatestEpoch().Committee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 6) @@ -411,7 +428,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.LatestEpoch().Committee() dir := t.TempDir() qcs := makeSequentialCommitQCs(committee, keys, 5) @@ -439,7 +457,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.LatestEpoch().Committee() 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..1daee55f52 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, Epoch: registry.LatestEpoch()} + committee := vs.Epoch.Committee() + 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..482708af4f 100644 --- a/sei-tendermint/internal/autobahn/consensus/persisted_inner.go +++ b/sei-tendermint/internal/autobahn/consensus/persisted_inner.go @@ -81,9 +81,9 @@ 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(ep *types.Epoch) error { if cqc, ok := p.CommitQC.Get(); ok { - if err := cqc.Verify(committee); err != nil { + if err := cqc.Verify(ep); err != nil { return fmt.Errorf("corrupt persisted state: CommitQC failed verification: %w", err) } } @@ -96,12 +96,13 @@ 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(ep, p.CommitQC); err != nil { return fmt.Errorf("corrupt persisted state: TimeoutQC failed verification: %w", err) } } currentView := p.View() + committee := ep.Committee() // checkViewAndSig validates that a persisted field has the current view and a valid signature. // Since inner is persisted atomically, any view mismatch indicates corrupt state. @@ -117,7 +118,7 @@ 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(ep)); err != nil { return err } } else if p.CommitVote.IsPresent() { @@ -134,7 +135,7 @@ func (p *persistedInner) validate(committee *types.Committee) error { } } 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(ep)); err != nil { return err } } diff --git a/sei-tendermint/internal/autobahn/consensus/state.go b/sei-tendermint/internal/autobahn/consensus/state.go index 75374a5ba4..dc41813341 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{Epoch: initialInner.ep}), 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.myView.Load().Epoch.Committee() + 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.myView.Load().Epoch.Committee() + 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 { + ep := s.myView.Load().Epoch + if err := vote.Verify(ep); err != nil { return fmt.Errorf("vote.Verify(): %w", err) } for tv := range s.timeoutVotes.Lock() { - tv.pushVote(s.Data().Committee(), vote) + tv.pushVote(ep.Committee(), vote) } return nil } @@ -203,9 +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 { - if committee.Leader(vs.View()) != s.cfg.Key.Public() { + if vs.Epoch.Committee().Leader(vs.View()) != s.cfg.Key.Public() { return nil // not the leader. } // Try repropose. @@ -214,14 +216,13 @@ 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, vs.Epoch) if err != nil { return fmt.Errorf("s.avail.WaitForLaneQCs(): %w", err) } // Construct a full proposal. fullProposal, err := types.NewProposal( s.cfg.Key, - committee, vs, time.Now(), laneQCsMap, @@ -249,7 +250,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, Epoch: i.ep} 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..7bc071f402 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" @@ -16,18 +17,18 @@ import ( // newTestState creates a State for testing with no persistence and a long // 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) +func newTestState(rng utils.Rng) (*State, []types.SecretKey, *epoch.Registry) { + 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], ViewTimeout: func(types.View) time.Duration { return time.Hour }, PersistentStateDir: utils.None[string](), }, dataState)) - return s, keys + return s, keys, registry } // makeTimeoutQC creates a TimeoutQC at the given view where all keys @@ -35,7 +36,7 @@ func newTestState(rng utils.Rng) (*State, []types.SecretKey) { func makeTimeoutQC(keys []types.SecretKey, view types.View, pqc utils.Option[*types.PrepareQC]) *types.TimeoutQC { votes := make([]*types.FullTimeoutVote, len(keys)) for i, k := range keys { - votes[i] = types.NewFullTimeoutVote(k, view, pqc) + votes[i] = types.NewFullTimeoutVote(k, view, pqc, 0) } return types.NewTimeoutQC(votes) } @@ -58,7 +59,7 @@ func testTimeoutVotePrepareQC(tv *types.FullTimeoutVote) utils.Option[*types.Pre func TestVoteTimeoutPrepareQC_BothNone(t *testing.T) { rng := utils.TestRng() - s, _ := newTestState(rng) + s, _, _ := newTestState(rng) err := scope.Run(t.Context(), func(ctx context.Context, sc scope.Scope) error { sc.SpawnBg(func() error { return utils.IgnoreCancel(s.Run(ctx)) }) @@ -80,12 +81,12 @@ func TestVoteTimeoutPrepareQC_BothNone(t *testing.T) { func TestVoteTimeoutPrepareQC_OnlyCurrentView(t *testing.T) { rng := utils.TestRng() - s, keys := newTestState(rng) + s, keys, registry := newTestState(rng) err := scope.Run(t.Context(), func(ctx context.Context, sc scope.Scope) error { sc.SpawnBg(func() error { return utils.IgnoreCancel(s.Run(ctx)) }) - pqc := makePrepareQC(keys, types.GenProposalAt(rng, types.View{Index: 0, Number: 0})) + pqc := makePrepareQC(keys, types.GenProposalForEpoch(rng, registry.LatestEpoch(), types.View{Index: 0, Number: 0})) if err := s.pushPrepareQC(ctx, pqc); err != nil { return fmt.Errorf("pushPrepareQC: %w", err) } @@ -108,14 +109,14 @@ func TestVoteTimeoutPrepareQC_OnlyCurrentView(t *testing.T) { // consecutive timeouts with an offline leader must not lose the PrepareQC. func TestVoteTimeoutPrepareQC_InheritedFromTimeoutQC(t *testing.T) { rng := utils.TestRng() - s, keys := newTestState(rng) + s, keys, registry := newTestState(rng) err := scope.Run(t.Context(), func(ctx context.Context, sc scope.Scope) error { sc.SpawnBg(func() error { return utils.IgnoreCancel(s.Run(ctx)) }) // View (0, 0): push PrepareQC for proposal P. view0 := types.View{Index: 0, Number: 0} - pqc0 := makePrepareQC(keys, types.GenProposalAt(rng, view0)) + pqc0 := makePrepareQC(keys, types.GenProposalForEpoch(rng, registry.LatestEpoch(), view0)) if err := s.pushPrepareQC(ctx, pqc0); err != nil { return fmt.Errorf("pushPrepareQC: %w", err) } @@ -165,14 +166,14 @@ func TestVoteTimeoutPrepareQC_InheritedFromTimeoutQC(t *testing.T) { // view's PrepareQC is preferred over the older inherited one. func TestVoteTimeoutPrepareQC_CurrentViewHigherThanInherited(t *testing.T) { rng := utils.TestRng() - s, keys := newTestState(rng) + s, keys, registry := newTestState(rng) err := scope.Run(t.Context(), func(ctx context.Context, sc scope.Scope) error { sc.SpawnBg(func() error { return utils.IgnoreCancel(s.Run(ctx)) }) // View (0, 0): PrepareQC for P. view0 := types.View{Index: 0, Number: 0} - pqc0 := makePrepareQC(keys, types.GenProposalAt(rng, view0)) + pqc0 := makePrepareQC(keys, types.GenProposalForEpoch(rng, registry.LatestEpoch(), view0)) if err := s.pushPrepareQC(ctx, pqc0); err != nil { return fmt.Errorf("pushPrepareQC(pqc0): %w", err) } @@ -185,7 +186,7 @@ func TestVoteTimeoutPrepareQC_CurrentViewHigherThanInherited(t *testing.T) { // Reproposal at (0, 1) succeeds — new PrepareQC at view (0, 1). view1 := types.View{Index: 0, Number: 1} - pqc1 := makePrepareQC(keys, types.GenProposalAt(rng, view1)) + pqc1 := makePrepareQC(keys, types.GenProposalForEpoch(rng, registry.LatestEpoch(), view1)) if err := s.pushPrepareQC(ctx, pqc1); err != nil { return fmt.Errorf("pushPrepareQC(pqc1): %w", err) } @@ -215,7 +216,7 @@ func TestVoteTimeoutPrepareQC_CurrentViewHigherThanInherited(t *testing.T) { // the current view's PrepareQC is used. func TestVoteTimeoutPrepareQC_CurrentViewPresentInheritedNone(t *testing.T) { rng := utils.TestRng() - s, keys := newTestState(rng) + s, keys, registry := newTestState(rng) err := scope.Run(t.Context(), func(ctx context.Context, sc scope.Scope) error { sc.SpawnBg(func() error { return utils.IgnoreCancel(s.Run(ctx)) }) @@ -229,7 +230,7 @@ func TestVoteTimeoutPrepareQC_CurrentViewPresentInheritedNone(t *testing.T) { // Fresh PrepareQC at (0, 1). view1 := types.View{Index: 0, Number: 1} - pqc1 := makePrepareQC(keys, types.GenProposalAt(rng, view1)) + pqc1 := makePrepareQC(keys, types.GenProposalForEpoch(rng, registry.LatestEpoch(), view1)) if err := s.pushPrepareQC(ctx, pqc1); err != nil { return fmt.Errorf("pushPrepareQC: %w", err) } @@ -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,13 +271,13 @@ 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())), )) } view0 := types.View{Index: 0, Number: 0} - pqc0 := makePrepareQC(keys, types.GenProposalAt(rng, view0)) + pqc0 := makePrepareQC(keys, types.GenProposalForEpoch(rng, registry.LatestEpoch(), view0)) // Session 1: push PrepareQC + TimeoutQC, let runOutputs persist. err := scope.Run(t.Context(), func(ctx context.Context, sc scope.Scope) error { diff --git a/sei-tendermint/internal/autobahn/data/state.go b/sei-tendermint/internal/autobahn/data/state.go index 5aba4e17c1..044423d44d 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{}, @@ -203,11 +204,15 @@ func (i *inner) skipTo(n types.GlobalBlockNumber) { // insertQC verifies and inserts a FullCommitQC into the inner state. // Accepts QCs whose range starts at or before nextQC (partially pruned // prefix is silently skipped). Rejects gaps where gr.First > nextQC. -func (i *inner) insertQC(committee *types.Committee, qc *types.FullCommitQC) error { - if err := qc.Verify(committee); err != nil { +func (i *inner) insertQC(registry *epoch.Registry, qc *types.FullCommitQC) error { + e, err := registry.EpochForProposal(qc.QC().Proposal()) + if err != nil { + return fmt.Errorf("qc.Verify(): %w", err) + } + if err := qc.Verify(e); 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 +242,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 +292,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 +300,13 @@ 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 { + if err := inner.insertQC(cfg.Registry, qc); err != nil { return nil, fmt.Errorf("load QC from WAL: %w", err) } } @@ -315,10 +320,16 @@ 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] + e, err := cfg.Registry.EpochForProposal(qc.QC().Proposal()) + if err != nil { + return nil, fmt.Errorf("unknown epoch: %w", err) + } + committee := e.Committee() + 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 +353,19 @@ 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) + ep, err := s.cfg.Registry.EpochForProposal(qc.QC().Proposal()) + if err != nil { + return fmt.Errorf("unknown epoch: %w", err) + } + 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 +382,15 @@ 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(ep); err != nil { return fmt.Errorf("qc.Verify(): %w", err) } } byHash := map[types.BlockHeaderHash]*types.Block{} + committee := ep.Committee() 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 +409,14 @@ 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() + storedEp, err := s.cfg.Registry.EpochForProposal(storedQC.QC().Proposal()) + if err != nil { + return fmt.Errorf("unknown epoch: %w", err) + } + storedCommittee := storedEp.Committee() 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 +446,30 @@ 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 { + // Verify outside the lock against all window committees. A block may satisfy + // multiple committees during an epoch transition; we collect them all so the + // in-lock re-verify can be skipped when the authoritative epoch is already covered. + preEps, err := s.cfg.Registry.VerifyInWindow(block.Verify) + if 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] + if qc == nil { + // Block arrived after pruning; drop silently so the sender keeps delivering future blocks. + return nil + } + blockEp, err := s.cfg.Registry.EpochForProposal(qc.QC().Proposal()) + if err != nil { + return fmt.Errorf("unknown epoch: %w", err) + } + if !epochInSet(blockEp, preEps) { + return fmt.Errorf("block.Verify(): signed by epoch %d, not in window", blockEp.EpochIndex()) + } + if err := inner.insertBlock(blockEp.Committee(), n, block); err != nil { return err } inner.updateNextBlock(s.metrics) @@ -442,6 +478,15 @@ func (s *State) PushBlock(ctx context.Context, n types.GlobalBlockNumber, block return nil } +func epochInSet(ep *types.Epoch, set []*types.Epoch) bool { + for _, e := range set { + if e.EpochIndex() == ep.EpochIndex() { + return true + } + } + return false +} + // NextBlock returns the index of the next block to be pushed. func (s *State) NextBlock() types.GlobalBlockNumber { for inner := range s.inner.Lock() { @@ -472,7 +517,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(n)), nil } panic("unreachable") } @@ -515,12 +560,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(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 +584,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(n), nil } panic("unreachable") } @@ -560,6 +605,7 @@ func (s *State) PushAppHash(ctx context.Context, n types.GlobalBlockNumber, hash n, inner.qcs[n].QC().Proposal().Index(), hash, + inner.qcs[n].QC().Proposal().EpochIndex(), ) t := time.Now() apt := appProposalWithTimestamp{ @@ -698,7 +744,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 7f5c57b477..a25e4d61aa 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,11 @@ 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) 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 +64,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, registry.LatestEpoch(), keys, prev) 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 +97,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(), @@ -124,15 +125,16 @@ func TestState(t *testing.T) { func TestPushConflictingBadCommitQC(t *testing.T) { ctx := t.Context() rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) + registry, keys := epoch.GenRegistry(rng, 3) + committee := registry.LatestEpoch().Committee() 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, registry.LatestEpoch(), keys, utils.None[*types.CommitQC]()) require.NoError(t, state.PushQC(ctx, qc1, blocks1)) - gr1 := qc1.QC().GlobalRange(committee) + nextQC := qc1.QC().GlobalRange().Next // Construct a malicious QC signed by non-committee keys. // It starts from block 0 (stale) but extends beyond nextQC. @@ -143,7 +145,7 @@ func TestPushConflictingBadCommitQC(t *testing.T) { badKeys[i] = types.GenSecretKey(rng) } laneBlocks := map[types.LaneID][]*types.Block{} - maliciousBlocksTotal := int(gr1.Len()) + 1 + maliciousBlocksTotal := int(nextQC-registry.FirstBlock()) + 1 require.LessOrEqual(t, maliciousBlocksTotal, committee.Lanes().Len()*types.MaxLaneRangeInProposal) for i := range maliciousBlocksTotal { lane := committee.Lanes().At(i % committee.Lanes().Len()) @@ -170,7 +172,7 @@ func TestPushConflictingBadCommitQC(t *testing.T) { malBlocks = append(malBlocks, b) } } - viewSpec := types.ViewSpec{CommitQC: utils.None[*types.CommitQC]()} + viewSpec := types.ViewSpec{CommitQC: utils.None[*types.CommitQC](), Epoch: registry.LatestEpoch()} leader := committee.Leader(viewSpec.View()) var leaderKey types.SecretKey for _, k := range keys { @@ -181,15 +183,14 @@ func TestPushConflictingBadCommitQC(t *testing.T) { } proposal := utils.OrPanic1(types.NewProposal( leaderKey, - committee, viewSpec, time.Now(), laneQCs, utils.None[*types.AppQC](), )) - malGR := proposal.Proposal().Msg().GlobalRange(committee) - require.Less(t, malGR.First, gr1.Next, "test setup: malicious gr.First must be < nextQC") - require.Greater(t, malGR.Next, gr1.Next, "test setup: malicious gr.Next must be > nextQC") + 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") votes := make([]*types.Signed[*types.CommitVote], 0, len(badKeys)) for _, k := range badKeys { @@ -204,6 +205,7 @@ func TestPushConflictingBadCommitQC(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() for n := gr1.First; n < gr1.Next; n++ { got, err := state.QC(ctx, n) require.NoError(t, err) @@ -221,9 +223,9 @@ func TestPushConflictingBadCommitQC(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, registry.LatestEpoch(), keys, utils.Some(qc1.QC())) 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) @@ -234,15 +236,15 @@ func TestPushConflictingBadCommitQC(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) 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, registry.LatestEpoch(), keys, utils.None[*types.CommitQC]()) 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). @@ -279,11 +281,11 @@ 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) 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)) }) @@ -291,12 +293,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, registry.LatestEpoch(), keys, prev) 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) @@ -323,16 +325,16 @@ 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) 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, registry.LatestEpoch(), keys, utils.None[*types.CommitQC]()) 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])) @@ -356,15 +358,15 @@ 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) 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, registry.LatestEpoch(), keys, utils.None[*types.CommitQC]()) 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() @@ -401,12 +403,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) @@ -421,14 +423,14 @@ 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) 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, registry.LatestEpoch(), keys, utils.None[*types.CommitQC]()) + 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])) @@ -440,7 +442,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") } @@ -454,15 +456,15 @@ 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) 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, registry.LatestEpoch(), keys, utils.None[*types.CommitQC]()) + 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++ { @@ -475,8 +477,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 @@ -494,9 +496,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, registry.LatestEpoch(), keys, utils.Some(qc1.QC())) 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()) } @@ -504,18 +506,18 @@ 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) 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, registry.LatestEpoch(), keys, utils.None[*types.CommitQC]()) + qc2, blocks2 := TestCommitQC(rng, registry.LatestEpoch(), keys, utils.Some(qc1.QC())) + 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...) @@ -652,8 +654,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) @@ -675,18 +677,18 @@ 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) 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, registry.LatestEpoch(), keys, utils.None[*types.CommitQC]()) + qc2, blocks2 := TestCommitQC(rng, registry.LatestEpoch(), keys, utils.Some(qc1.QC())) + 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++ { @@ -749,8 +751,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++ { @@ -778,18 +780,18 @@ 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) 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, registry.LatestEpoch(), keys, utils.None[*types.CommitQC]()) + qc2, blocks2 := TestCommitQC(rng, registry.LatestEpoch(), keys, utils.Some(qc1.QC())) + 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++ { @@ -799,12 +801,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++ { @@ -827,17 +829,17 @@ 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 +60,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, Epoch: ep} + p := types.NewAppProposal(cqc.GlobalRange().Next-1, vs.View().Index, types.GenAppHash(rng), ep.EpochIndex()) + 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, ep.FirstBlock(), ep.FirstTimestamp(), 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..2830d9443a --- /dev/null +++ b/sei-tendermint/internal/autobahn/epoch/registry.go @@ -0,0 +1,134 @@ +package epoch + +import ( + "fmt" + "math" + "time" + + "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" +) + +// 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. +// +// Epochs are stored in ascending order of Roads().First. The latest epoch always has +// Roads().Last = math.MaxUint64. AddEpoch closes off the current latest epoch and +// appends the new one atomically under a write lock. +type Registry struct { + mu utils.RWMutex[struct{}] + epochs []*types.Epoch // sorted by Roads().First ascending +} + +// NewRegistry creates a Registry with the genesis committee. +func NewRegistry( + committee *types.Committee, + firstBlock types.GlobalBlockNumber, + genesisTimestamp time.Time, +) (*Registry, error) { + return &Registry{ + epochs: []*types.Epoch{types.NewEpoch( + 0, + types.OpenRoadRange(), + genesisTimestamp, + committee, + firstBlock, + )}, + }, nil +} + +// FirstBlock returns the first global block number of the chain (epoch 0's FirstBlock). +func (r *Registry) FirstBlock() types.GlobalBlockNumber { + for range r.mu.RLock() { + return r.epochs[0].FirstBlock() + } + panic("unreachable") +} + +// GenesisTimestamp returns the timestamp of the genesis epoch. +func (r *Registry) GenesisTimestamp() time.Time { + for range r.mu.RLock() { + return r.epochs[0].FirstTimestamp() + } + panic("unreachable") +} + +// EpochForProposal returns the epoch declared by p.EpochIndex(). +// Returns an error if that epoch is not yet registered — this indicates a +// proposal from a future or unknown epoch, which the caller should reject. +func (r *Registry) EpochForProposal(p *types.Proposal) (*types.Epoch, error) { + ep, ok := r.EpochByIndex(Index(p.EpochIndex())) + if !ok { + return nil, fmt.Errorf("unknown epoch_index %d", p.EpochIndex()) + } + return ep, nil +} + +// EpochByIndex returns the epoch with the given index, if it exists. +func (r *Registry) EpochByIndex(idx Index) (*types.Epoch, bool) { + for range r.mu.RLock() { + for _, e := range r.epochs { + if e.EpochIndex() == uint64(idx) { + return e, true + } + } + return nil, false + } + panic("unreachable") +} + +// LatestEpoch returns the most recently activated epoch. +func (r *Registry) LatestEpoch() *types.Epoch { + for range r.mu.RLock() { + return r.epochs[len(r.epochs)-1] + } + panic("unreachable") +} + +// AddEpoch registers a new epoch starting at startRoad with the given committee. +// The current latest epoch's Roads().Last is closed off at startRoad-1. +// firstBlock is the first global block number of the new epoch. +// Called by the execution bridge when a new committee is finalized. +func (r *Registry) AddEpoch(committee *types.Committee, startRoad types.RoadIndex, timestamp time.Time, firstBlock types.GlobalBlockNumber) error { + for range r.mu.Lock() { + latest := r.epochs[len(r.epochs)-1] + if startRoad <= latest.Roads().First { + return fmt.Errorf("new epoch start %d must be after current epoch start %d", startRoad, latest.Roads().First) + } + // TODO: also reject startRoad <= highest committed road index to prevent + // retroactively reassigning already-finalized roads to the new committee. + // Requires the caller to pass the commit watermark once the execution bridge is wired. + // Replace latest with a closed version (Roads().Last = startRoad-1). + r.epochs[len(r.epochs)-1] = types.NewEpoch( + latest.EpochIndex(), + types.RoadRange{First: latest.Roads().First, Last: startRoad - 1}, + latest.FirstTimestamp(), + latest.Committee(), + latest.FirstBlock(), + ) + r.epochs = append(r.epochs, types.NewEpoch( + latest.EpochIndex()+1, + types.RoadRange{First: startRoad, Last: math.MaxUint64}, + timestamp, + committee, + firstBlock, + )) + return nil + } + panic("unreachable") +} + +// VerifyInWindow calls fn against the latest epoch's committee and returns it if accepted. +// Returns a slice of all matching epochs so callers can skip re-verification for any +// epoch already checked here. +// TODO: expand to neighbor epochs (previous and next) once multi-epoch transitions are wired up. +func (r *Registry) VerifyInWindow(fn func(*types.Committee) error) ([]*types.Epoch, error) { + ep := r.LatestEpoch() + if err := fn(ep.Committee()); err != nil { + return nil, err + } + return []*types.Epoch{ep}, nil +} 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..a813aff245 --- /dev/null +++ b/sei-tendermint/internal/autobahn/epoch/registry_test.go @@ -0,0 +1,91 @@ +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.Committee) { + t.Helper() + rng := utils.TestRng() + committee := utils.OrPanic1(types.NewCommittee(map[types.PublicKey]uint64{ + types.GenSecretKey(rng).Public(): 1, + types.GenSecretKey(rng).Public(): 1, + types.GenSecretKey(rng).Public(): 1, + })) + r := utils.OrPanic1(NewRegistry(committee, 0, time.Time{})) + return r, committee +} + +func TestRegistry_EpochByIndex_UnknownReturnsNotFound(t *testing.T) { + r, _ := makeRegistry(t) + if _, ok := r.EpochByIndex(99); ok { + t.Fatal("EpochByIndex(99) returned ok, want not found") + } +} + +func TestRegistry_EpochByIndex_GenesisFound(t *testing.T) { + r, _ := makeRegistry(t) + ep, ok := r.EpochByIndex(0) + if !ok { + t.Fatal("EpochByIndex(0) not found") + } + if ep.EpochIndex() != 0 { + t.Fatalf("EpochIndex() = %d, want 0", ep.EpochIndex()) + } +} + +func TestRegistry_EpochForProposal_UnknownEpochReturnsError(t *testing.T) { + rng := utils.TestRng() + r, _ := makeRegistry(t) + p := types.ProposalAt(r.LatestEpoch(), types.View{Index: 0, Number: 0}) + // Build a fake proposal with epoch_index=99 by using a fresh epoch. + unknownEp := types.NewEpoch(99, types.OpenRoadRange(), time.Time{}, r.LatestEpoch().Committee(), 0) + p2 := types.ProposalAt(unknownEp, types.View{Index: 0, Number: 0}) + _ = rng + if _, err := r.EpochForProposal(p2); err == nil { + t.Fatal("EpochForProposal with unknown epoch succeeded, want error") + } + if _, err := r.EpochForProposal(p); err != nil { + t.Fatalf("EpochForProposal with genesis epoch: %v", err) + } +} + +func TestRegistry_AddEpoch_ClosesPreviousAndAppends(t *testing.T) { + rng := utils.TestRng() + r, _ := makeRegistry(t) + newCommittee := utils.OrPanic1(types.NewCommittee(map[types.PublicKey]uint64{ + types.GenSecretKey(rng).Public(): 1, + })) + if err := r.AddEpoch(newCommittee, 10, time.Time{}, 100); err != nil { + t.Fatalf("AddEpoch: %v", err) + } + ep0, ok := r.EpochByIndex(0) + if !ok { + t.Fatal("epoch 0 missing after AddEpoch") + } + if ep0.Roads().Last != 9 { + t.Fatalf("epoch 0 roads.Last = %d, want 9", ep0.Roads().Last) + } + ep1, ok := r.EpochByIndex(1) + if !ok { + t.Fatal("epoch 1 missing after AddEpoch") + } + if ep1.Roads().First != 10 { + t.Fatalf("epoch 1 roads.First = %d, want 10", ep1.Roads().First) + } +} + +func TestRegistry_AddEpoch_RejectsStartBeforeCurrentFirst(t *testing.T) { + rng := utils.TestRng() + r, _ := makeRegistry(t) + newCommittee := utils.OrPanic1(types.NewCommittee(map[types.PublicKey]uint64{ + types.GenSecretKey(rng).Public(): 1, + })) + if err := r.AddEpoch(newCommittee, 0, time.Time{}, 0); err == nil { + t.Fatal("AddEpoch with startRoad=0 succeeded, 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..c02099160f --- /dev/null +++ b/sei-tendermint/internal/autobahn/epoch/testonly.go @@ -0,0 +1,23 @@ +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 + } + committee := utils.OrPanic1(types.NewCommittee(weights)) + firstBlock := types.GenGlobalBlockNumber(rng) % 1000000 + registry := utils.OrPanic1(NewRegistry(committee, 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 0a1e281483..53d5ffb67f 100644 --- a/sei-tendermint/internal/autobahn/pb/autobahn.pb.go +++ b/sei-tendermint/internal/autobahn/pb/autobahn.pb.go @@ -835,10 +835,12 @@ 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 + FirstBlock *uint64 `protobuf:"varint,6,opt,name=first_block,json=firstBlock,proto3,oneof" json:"first_block,omitempty"` // genesis InitialHeight; added to lane block numbers to produce absolute global block numbers + EpochIndex *uint64 `protobuf:"varint,7,opt,name=epoch_index,json=epochIndex,proto3,oneof" json:"epoch_index,omitempty"` // epoch this proposal belongs to unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -901,6 +903,20 @@ func (x *Proposal) GetApp() *AppProposal { return nil } +func (x *Proposal) GetFirstBlock() uint64 { + if x != nil && x.FirstBlock != nil { + return *x.FirstBlock + } + return 0 +} + +func (x *Proposal) GetEpochIndex() uint64 { + if x != nil && x.EpochIndex != nil { + return *x.EpochIndex + } + 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"` @@ -1129,6 +1145,7 @@ type TimeoutVote struct { state protoimpl.MessageState `protogen:"open.v1"` View *View `protobuf:"bytes,1,opt,name=view,proto3,oneof" json:"view,omitempty"` // required LatestPrepareQcViewNumber *uint64 `protobuf:"varint,2,opt,name=latest_prepare_qc_view_number,json=latestPrepareQcViewNumber,proto3,oneof" json:"latest_prepare_qc_view_number,omitempty"` // optional + EpochIndex *uint64 `protobuf:"varint,3,opt,name=epoch_index,json=epochIndex,proto3,oneof" json:"epoch_index,omitempty"` // epoch this vote belongs to unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1177,6 +1194,13 @@ func (x *TimeoutVote) GetLatestPrepareQcViewNumber() uint64 { return 0 } +func (x *TimeoutVote) GetEpochIndex() uint64 { + if x != nil && x.EpochIndex != nil { + return *x.EpochIndex + } + return 0 +} + type TimeoutQC struct { state protoimpl.MessageState `protogen:"open.v1"` VotesV2 []*SignedTimeoutVote `protobuf:"bytes,3,rep,name=votes_v2,json=votesV2,proto3" json:"votes_v2,omitempty"` @@ -1482,7 +1506,9 @@ type AppProposal struct { // Index of the commit qc finalizing the block. RoadIndex *uint64 `protobuf:"varint,2,opt,name=road_index,json=roadIndex,proto3,oneof" json:"road_index,omitempty"` // required // App hash at that block. - AppHash []byte `protobuf:"bytes,3,opt,name=app_hash,json=appHash,proto3,oneof" json:"app_hash,omitempty"` // required + AppHash []byte `protobuf:"bytes,3,opt,name=app_hash,json=appHash,proto3,oneof" json:"app_hash,omitempty"` // required + // Epoch this proposal belongs to. + EpochIndex *uint64 `protobuf:"varint,4,opt,name=epoch_index,json=epochIndex,proto3,oneof" json:"epoch_index,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1538,6 +1564,13 @@ func (x *AppProposal) GetAppHash() []byte { return nil } +func (x *AppProposal) GetEpochIndex() uint64 { + if x != nil && x.EpochIndex != nil { + return *x.EpochIndex + } + return 0 +} + // This is the signable message. // To sign ConsensusMsg/BlockMsg, you need to embed it in Msg first. type Msg struct { @@ -2242,17 +2275,23 @@ 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\"\x82\x03\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" + + "\vfirst_block\x18\x06 \x01(\x04H\x03R\n" + + "firstBlock\x88\x01\x01\x12$\n" + + "\vepoch_index\x18\a \x01(\x04H\x04R\n" + + "epochIndex\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\x0e\n" + + "\f_first_blockB\x0e\n" + + "\f_epoch_indexJ\x04\b\x02\x10\x03R\n" + "created_at\"\x96\x02\n" + "\fFullProposal\x129\n" + "\vproposal_v2\x18\x05 \x01(\v2\x18.autobahn.SignedProposalR\n" + @@ -2271,12 +2310,15 @@ const file_autobahn_autobahn_proto_rawDesc = "" + "\x04sigs\x18\x03 \x03(\v2\x13.autobahn.SignatureB\x06Ј\xe2\xab\fdR\x04sigs:\x06\xe8\x88\xe2\xab\f\x01\"t\n" + "\fFullCommitQC\x12\"\n" + "\x02qc\x18\x01 \x01(\v2\x12.autobahn.CommitQCR\x02qc\x128\n" + - "\aheaders\x18\x02 \x03(\v2\x15.autobahn.BlockHeaderB\aЈ\xe2\xab\f\xe8\aR\aheaders:\x06\xe8\x88\xe2\xab\f\x01\"\xb6\x01\n" + + "\aheaders\x18\x02 \x03(\v2\x15.autobahn.BlockHeaderB\aЈ\xe2\xab\f\xe8\aR\aheaders:\x06\xe8\x88\xe2\xab\f\x01\"\xec\x01\n" + "\vTimeoutVote\x12'\n" + "\x04view\x18\x01 \x01(\v2\x0e.autobahn.ViewH\x00R\x04view\x88\x01\x01\x12E\n" + - "\x1dlatest_prepare_qc_view_number\x18\x02 \x01(\x04H\x01R\x19latestPrepareQcViewNumber\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\a\n" + + "\x1dlatest_prepare_qc_view_number\x18\x02 \x01(\x04H\x01R\x19latestPrepareQcViewNumber\x88\x01\x01\x12$\n" + + "\vepoch_index\x18\x03 \x01(\x04H\x02R\n" + + "epochIndex\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\a\n" + "\x05_viewB \n" + - "\x1e_latest_prepare_qc_view_number\"\xbc\x01\n" + + "\x1e_latest_prepare_qc_view_numberB\x0e\n" + + "\f_epoch_index\"\xbc\x01\n" + "\tTimeoutQC\x12>\n" + "\bvotes_v2\x18\x03 \x03(\v2\x1b.autobahn.SignedTimeoutVoteB\x06Ј\xe2\xab\fdR\avotesV2\x12D\n" + "\x11latest_prepare_qc\x18\x02 \x01(\v2\x13.autobahn.PrepareQCH\x00R\x0flatestPrepareQc\x88\x01\x01:\x06\xe8\x88\xe2\xab\f\x01B\x14\n" + @@ -2309,15 +2351,18 @@ const file_autobahn_autobahn_proto_rawDesc = "" + "_commit_qc\"k\n" + "\x05AppQC\x12)\n" + "\x04vote\x18\x01 \x01(\v2\x15.autobahn.AppProposalR\x04vote\x12/\n" + - "\x04sigs\x18\x02 \x03(\v2\x13.autobahn.SignatureB\x06Ј\xe2\xab\fdR\x04sigs:\x06\xe8\x88\xe2\xab\f\x01\"\xbf\x01\n" + + "\x04sigs\x18\x02 \x03(\v2\x13.autobahn.SignatureB\x06Ј\xe2\xab\fdR\x04sigs:\x06\xe8\x88\xe2\xab\f\x01\"\xf5\x01\n" + "\vAppProposal\x12(\n" + "\rglobal_number\x18\x01 \x01(\x04H\x00R\fglobalNumber\x88\x01\x01\x12\"\n" + "\n" + "road_index\x18\x02 \x01(\x04H\x01R\troadIndex\x88\x01\x01\x12&\n" + - "\bapp_hash\x18\x03 \x01(\fB\x06؈\xe2\xab\f H\x02R\aappHash\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\x10\n" + + "\bapp_hash\x18\x03 \x01(\fB\x06؈\xe2\xab\f H\x02R\aappHash\x88\x01\x01\x12$\n" + + "\vepoch_index\x18\x04 \x01(\x04H\x03R\n" + + "epochIndex\x88\x01\x01:\fȈ\xe2\xab\f\x01\xe8\x88\xe2\xab\f\x01B\x10\n" + "\x0e_global_numberB\r\n" + "\v_road_indexB\v\n" + - "\t_app_hash\"\x98\x03\n" + + "\t_app_hashB\x0e\n" + + "\f_epoch_index\"\x98\x03\n" + "\x03Msg\x126\n" + "\rlane_proposal\x18\x01 \x01(\v2\x0f.autobahn.BlockH\x00R\flaneProposal\x124\n" + "\tlane_vote\x18\x02 \x01(\v2\x15.autobahn.BlockHeaderH\x00R\blaneVote\x120\n" + diff --git a/sei-tendermint/internal/autobahn/pb/autobahn.wireguard.go b/sei-tendermint/internal/autobahn/pb/autobahn.wireguard.go index f27ae2e428..13499a0361 100644 --- a/sei-tendermint/internal/autobahn/pb/autobahn.wireguard.go +++ b/sei-tendermint/internal/autobahn/pb/autobahn.wireguard.go @@ -44,43 +44,43 @@ func (*View) MaxSize() int { } func (*Proposal) MaxSize() int { - return 9506 + return 9539 } func (*FullProposal) MaxSize() int { - return 1106394 + return 1107571 } func (*PrepareQC) MaxSize() int { - return 19909 + return 19942 } func (*CommitQC) MaxSize() int { - return 19909 + return 19942 } func (*FullCommitQC) MaxSize() int { - return 136913 + return 136946 } func (*TimeoutVote) MaxSize() int { - return 35 + return 46 } func (*TimeoutQC) MaxSize() int { - return 34313 + return 35446 } func (*FullTimeoutVote) MaxSize() int { - return 20057 + return 20101 } func (*AppQC) MaxSize() int { - return 10458 + return 10469 } func (*AppProposal) MaxSize() int { - return 56 + return 67 } func (*Msg) MaxSize() int { @@ -88,15 +88,15 @@ func (*Msg) MaxSize() int { } func (*SignedProposal) MaxSize() int { - return 9613 + return 9646 } func (*SignedTimeoutVote) MaxSize() int { - return 141 + return 152 } func (*SignedAppVote) MaxSize() int { - return 162 + return 173 } func (*SignedBlock) MaxSize() int { @@ -108,11 +108,11 @@ func (*SignedBlockHeader) MaxSize() int { } func (*SignedAppProposal) MaxSize() int { - return 162 + return 173 } func (*ConsensusReq) MaxSize() int { - return 1106398 + return 1107575 } func init() { @@ -215,6 +215,8 @@ 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}, + 7: {MaxCount: 1}, }) // Register the wireguard.Schema generated for autobahn.FullProposal. @@ -247,6 +249,7 @@ func init() { runtime.MustRegister[*TimeoutVote](runtime.Schema{ 1: {MaxCount: 1, Nested: utils.Some(reflect.TypeFor[*View]())}, 2: {MaxCount: 1}, + 3: {MaxCount: 1}, }) // Register the wireguard.Schema generated for autobahn.TimeoutQC. @@ -288,6 +291,7 @@ func init() { 1: {MaxCount: 1}, 2: {MaxCount: 1}, 3: {MaxCount: 1, MaxSize: 32}, + 4: {MaxCount: 1}, }) // Register the wireguard.Schema generated for autobahn.Msg. 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..038c0abf07 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.LatestEpoch().Committee() + 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..df6a9c6cac 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.LatestEpoch().Committee() + 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] @@ -43,6 +45,7 @@ func TestConsensusClientServer(t *testing.T) { idx, types.RoadIndex(offset), types.GenAppHash(rng), + 0, ) wantAppProposal = utils.Some(p) for _, n := range nodes { @@ -55,7 +58,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..6703123058 100644 --- a/sei-tendermint/internal/p2p/giga/data_test.go +++ b/sei-tendermint/internal/p2p/giga/data_test.go @@ -9,10 +9,12 @@ 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" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/scope" + "google.golang.org/protobuf/proto" ) type testNode struct { @@ -23,8 +25,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 +48,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.LatestEpoch().Committee(), 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 +93,11 @@ 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) 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, server.data.Registry().LatestEpoch(), keys, prev) if err := server.data.PushQC(ctx, qc, blocks); err != nil { return fmt.Errorf("serverState.PushQC(): %w", err) } @@ -130,8 +133,8 @@ func TestDataClientServer(t *testing.T) { if err != nil { return fmt.Errorf("clientState.CommitQC(): %w", err) } - if err := utils.TestDiff(wantQC, gotQC); err != nil { - return err + if !proto.Equal(types.FullCommitQCConv.Encode(wantQC), types.FullCommitQCConv.Encode(gotQC)) { + return fmt.Errorf("QC mismatch at block %d", n) } } return nil diff --git a/sei-tendermint/internal/p2p/giga/pb/api.wireguard.go b/sei-tendermint/internal/p2p/giga/pb/api.wireguard.go index 3ef12ec7ac..4e40412ad9 100644 --- a/sei-tendermint/internal/p2p/giga/pb/api.wireguard.go +++ b/sei-tendermint/internal/p2p/giga/pb/api.wireguard.go @@ -29,7 +29,7 @@ func (*LaneProposal) MaxSize() int { } func (*AppVote) MaxSize() int { - return 165 + return 176 } func (*StreamLaneProposalsReq) MaxSize() int { @@ -41,7 +41,7 @@ func (*StreamAppQCsReq) MaxSize() int { } func (*StreamAppQCsResp) MaxSize() int { - return 30374 + return 30418 } 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 bc75a7d0fb..41632afb04 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" "path/filepath" "slices" @@ -16,6 +15,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" @@ -70,19 +70,24 @@ 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 + } + genesisCommittee, err := atypes.NewCommittee(genesisWeights) + if err != nil { + return nil, fmt.Errorf("genesis committee: %w", err) + } + registry, err := epoch.NewRegistry(genesisCommittee, 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) } @@ -508,6 +513,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().LatestEpoch().Committee().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..75e49acb6d 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().LatestEpoch().Committee().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..9fe39e6dbf 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().LatestEpoch().Committee().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..2d6b5d0a87 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().LatestEpoch().Committee().EvmShard(sender) proxyURL, ok := router.EvmProxy(sender).Get() expectedURL := urlByValidator[shardValidator]