From 9720c65c1b9564800c3bbad0e0911570a33e08fd Mon Sep 17 00:00:00 2001 From: samliok Date: Wed, 2 Sep 2026 17:04:07 -0400 Subject: [PATCH 1/9] remove epoch from config --- instance.go | 18 ++++++------ simplex/epoch.go | 13 ++++++++- simplex/epoch_test.go | 46 +++++++++++++++--------------- simplex/recovery_test.go | 60 +++++++++++++++++++++++++++++++--------- 4 files changed, 91 insertions(+), 46 deletions(-) diff --git a/instance.go b/instance.go index 448c4eda..f455f7f1 100644 --- a/instance.go +++ b/instance.go @@ -118,7 +118,7 @@ func (i *Instance) Start(ctx context.Context) error { return fmt.Errorf("error determining latest epoch and validator set: %w", err) } - if err := i.startAtEpoch(nodes, epochNum); err != nil { + if err := i.startAtEpoch(nodes); err != nil { return fmt.Errorf("error starting instance at epoch %d: %w", epochNum, err) } @@ -128,8 +128,8 @@ func (i *Instance) Start(ctx context.Context) error { return nil } -func (i *Instance) startValidator(epochNum uint64, validators common.Nodes) error { - epochConfig, err := i.createEpochConfig(epochNum, validators) +func (i *Instance) startValidator(validators common.Nodes) error { + epochConfig, err := i.createEpochConfig(validators) if err != nil { return err } @@ -139,7 +139,6 @@ func (i *Instance) startValidator(epochNum uint64, validators common.Nodes) erro return fmt.Errorf("error creating simplex epoch: %w", err) } - epoch.Epoch = epochConfig.Epoch i.e = epoch i.epochOrNV = epoch epochConfig.bbw.e = epoch @@ -436,10 +435,10 @@ func (i *Instance) processEpochChange(epochChange epochChange) { case runningNonValidator: // Stop the non-validator before doing anything else, so that we don't process any more messages while we are changing epochs. i.stopNonValidator() - err = i.startAtEpoch(epochChange.validators, epochChange.epoch) + err = i.startAtEpoch(epochChange.validators) case runningValidator: i.stopValidator(true) - err = i.startAtEpoch(epochChange.validators, epochChange.epoch) + err = i.startAtEpoch(epochChange.validators) default: // This should never happen, but we log it just in case. i.lock.Unlock() i.Config.Logger.Fatal("We are not running either a validator or non-validator") @@ -453,7 +452,7 @@ func (i *Instance) processEpochChange(epochChange epochChange) { } } -func (i *Instance) createEpochConfig(epoch uint64, validators common.Nodes) (*epochConfig, error) { +func (i *Instance) createEpochConfig(validators common.Nodes) (*epochConfig, error) { wal, err := wal.NewGarbageCollectedWAL(i.Config.WALs, i.Config.WalCreator, &common.WALRetentionReader{}, i.Config.ParameterConfig.WALMaxSizeBytes) if err != nil { return nil, fmt.Errorf("error creating garbage collected wal: %w", err) @@ -523,7 +522,6 @@ func (i *Instance) createEpochConfig(epoch uint64, validators common.Nodes) (*ep }) ec := simplex.EpochConfig{ - Epoch: epoch, ReplicationEnabled: true, StartTime: time.Now(), // TODO: For simplicity, we use the same value for all timeouts. If needed we can expand the config. @@ -571,9 +569,9 @@ func (i *Instance) maybeGarbageCollectWAL() error { } // startAtEpoch starts either a validator or non-validator at `epoch“. -func (i *Instance) startAtEpoch(validators common.Nodes, epoch uint64) error { +func (i *Instance) startAtEpoch(validators common.Nodes) error { if validators.Contains(i.Config.ID) { - return i.startValidator(epoch, validators) + return i.startValidator(validators) } return i.startNonValidator() diff --git a/simplex/epoch.go b/simplex/epoch.go index 7e3c95a6..a31670c3 100644 --- a/simplex/epoch.go +++ b/simplex/epoch.go @@ -82,7 +82,6 @@ type EpochConfig struct { Storage common.Storage WAL common.WriteAheadLog BlockBuilder common.BlockBuilder - Epoch uint64 StartTime time.Time ReplicationEnabled bool RandomSource *rand.Rand @@ -90,6 +89,8 @@ type EpochConfig struct { type Epoch struct { EpochConfig + + Epoch uint64 // Runtime epochSealed atomic.Bool signatureAggregator common.SignatureAggregator @@ -671,6 +672,13 @@ func (e *Epoch) setMetadataFromStorage() error { } e.round = e.lastBlock.VerifiedBlock.BlockHeader().Round + 1 + + // The last block we indexed was a sealing block, therefore the epoch number is that blocks sequence + if e.lastBlock.VerifiedBlock.SealingBlockInfo() != nil { + e.Epoch = e.lastBlock.VerifiedBlock.BlockHeader().Seq + return nil + } + e.Epoch = e.lastBlock.VerifiedBlock.BlockHeader().Epoch return nil } @@ -711,6 +719,9 @@ func (e *Epoch) setMetadataFromRecords(records [][]byte) error { } if finalization.Finalization.Round >= highestRound { highestRound = finalization.Finalization.Round + // We do not need to check if this finalization is associated for a sealing block, because it is impossible for a finalization + // for another epoch to exist in our wal, yet the original epoch remains un-sealed. + // This is because we issue te-locks after sealing blocks, and never move onto the next epoch until the original sealing block was indexed. highestEpoch = finalization.Finalization.Epoch found = true } diff --git a/simplex/epoch_test.go b/simplex/epoch_test.go index 9a6fb0f2..159ec95c 100644 --- a/simplex/epoch_test.go +++ b/simplex/epoch_test.go @@ -933,25 +933,32 @@ func TestEpochSimpleFlow(t *testing.T) { } func TestEpochResizesBlacklistOnEpochChange(t *testing.T) { - epoch1Block := testutil.NewTestBlock(ProtocolMetadata{Epoch: 1, Round: 0, Seq: 0}, NewBlacklist(1)) nodes := []NodeID{{1}, {2}} bb := testutil.NewTestBlockBuilder() - conf, _, _ := testutil.DefaultTestNodeEpochConfig(t, NodeID{2}, testutil.NewNoopComm(nodes), bb) - conf.Epoch = 2 - require.NoError(t, conf.Storage.Index(context.Background(), epoch1Block, Finalization{})) - require.Equal(t, uint16(1), epoch1Block.Blacklist().NodeCount, + + // The epoch number is the sequence of the last indexed sealing block, so both nodes + // below start in epoch 1 while their last indexed block belongs to epoch 0. + epoch0Block := testutil.NewTestBlock(ProtocolMetadata{Epoch: 0, Round: 0, Seq: 0}, NewBlacklist(1)) + sealingBlock := testutil.NewTestBlock(ProtocolMetadata{Epoch: 0, Round: 1, Seq: 1, Prev: epoch0Block.Digest}, NewBlacklist(1)) + sealingBlock.SealingInfo = &SealingBlockInfo{ + ValidatorSet: NodeIDs(nodes).EqualWeightedNodes(), + PrevSealingBlockHash: epoch0Block.Digest, + } + require.Equal(t, uint16(1), sealingBlock.Blacklist().NodeCount, "blacklist must contain exactly one node") + conf, _, _ := testutil.DefaultTestNodeEpochConfig(t, nodes[0], testutil.NewNoopComm(nodes), bb) + require.NoError(t, conf.Storage.Index(context.Background(), epoch0Block, Finalization{})) + require.NoError(t, conf.Storage.Index(context.Background(), sealingBlock, Finalization{})) + e, err := NewEpoch(conf) require.NoError(t, err) - e.Epoch = conf.Epoch require.NoError(t, e.Start()) - require.Equal(t, uint64(2), e.Metadata().Epoch) + require.Equal(t, uint64(1), e.Metadata().Epoch) - // The node (leader) builds the next block on top of the epoch-1 block. Its + // The node (leader of round 2) builds the next block on top of the sealing block. Its // blacklist must be sized for the new validator set (2), not inherited from the - // parent (1) — otherwise its blacklist is malformed and the block cannot be - // notarized. + // parent (1), otherwise its blacklist is malformed and the block cannot be notarized. bb.BlockShouldBeBuilt <- struct{}{} block := bb.GetBuiltBlock() require.Equal(t, uint16(2), block.Blacklist().NodeCount, @@ -959,21 +966,17 @@ func TestEpochResizesBlacklistOnEpochChange(t *testing.T) { e.Stop() // Next, create the other node (follower) and ensure it can verify the block. - conf, wal, _ := testutil.DefaultTestNodeEpochConfig(t, NodeID{1}, testutil.NewNoopComm(nodes), bb) - conf.Epoch = 2 - - require.NoError(t, conf.Storage.Index(context.Background(), epoch1Block, Finalization{})) - require.Equal(t, uint16(1), epoch1Block.Blacklist().NodeCount, - "blacklist must contain exactly one node") + conf, wal, _ := testutil.DefaultTestNodeEpochConfig(t, nodes[1], testutil.NewNoopComm(nodes), bb) + require.NoError(t, conf.Storage.Index(context.Background(), epoch0Block, Finalization{})) + require.NoError(t, conf.Storage.Index(context.Background(), sealingBlock, Finalization{})) e, err = NewEpoch(conf) require.NoError(t, err) - e.Epoch = conf.Epoch require.NoError(t, e.Start()) t.Cleanup(e.Stop) - require.Equal(t, uint64(2), e.Metadata().Epoch) + require.Equal(t, uint64(1), e.Metadata().Epoch) - vote, err := testutil.NewTestVote(block, nodes[1]) + vote, err := testutil.NewTestVote(block, nodes[0]) require.NoError(t, err) err = e.HandleMessage(&Message{ @@ -981,10 +984,9 @@ func TestEpochResizesBlacklistOnEpochChange(t *testing.T) { Vote: *vote, Block: block, }, - }, nodes[1]) + }, nodes[0]) require.NoError(t, err) - wal.AssertNotarization(1) - + wal.AssertNotarization(2) } func TestEpochStartedTwice(t *testing.T) { diff --git a/simplex/recovery_test.go b/simplex/recovery_test.go index d456045c..715b8e05 100644 --- a/simplex/recovery_test.go +++ b/simplex/recovery_test.go @@ -547,24 +547,58 @@ func TestRecoveryBlocksIndexed(t *testing.T) { require.Equal(t, thirdBlock.BlockHeader().Digest, e.Metadata().Prev) } +// TestEpochCorrectlyInitializesMetadataFromStorage asserts the next block's metadata is +// derived from the last indexed block. func TestEpochCorrectlyInitializesMetadataFromStorage(t *testing.T) { + tests := []struct { + name string + sealing bool + expectedEpoch uint64 + }{ + { + name: "normal block", + expectedEpoch: 3, + }, + { + name: "sealing block", + sealing: true, + expectedEpoch: 1, + }, + } + ctx := context.Background() - bb := testutil.NewTestBlockBuilder() nodes := []NodeID{{1}, {2}, {3}, {4}} - conf, _, _ := testutil.DefaultTestNodeEpochConfig(t, nodes[0], testutil.NewNoopComm(nodes), bb) - block := testutil.NewTestBlock(ProtocolMetadata{Seq: 0, Round: 0, Epoch: 0}, emptyBlacklist) - require.NoError(t, conf.Storage.Index(ctx, block, Finalization{})) - e, err := NewEpoch(conf) - require.NoError(t, err) - t.Cleanup(e.Stop) - require.Equal(t, uint64(1), e.Storage.NumBlocks()) - require.NoError(t, e.Start()) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bb := testutil.NewTestBlockBuilder() + conf, _, _ := testutil.DefaultTestNodeEpochConfig(t, nodes[0], testutil.NewNoopComm(nodes), bb) + + firstBlock := testutil.NewTestBlock(ProtocolMetadata{Seq: 0, Round: 0, Epoch: 3}, emptyBlacklist) + lastBlock := testutil.NewTestBlock(ProtocolMetadata{Seq: 1, Round: 1, Epoch: 3, Prev: firstBlock.Digest}, emptyBlacklist) + if tt.sealing { + lastBlock.SealingInfo = &SealingBlockInfo{ + ValidatorSet: NodeIDs(nodes).EqualWeightedNodes(), + PrevSealingBlockHash: firstBlock.Digest, + } + } - // ensure the round is properly set - require.Equal(t, uint64(1), e.Metadata().Round) - require.Equal(t, uint64(1), e.Metadata().Seq) - require.Equal(t, block.BlockHeader().Digest, e.Metadata().Prev) + require.NoError(t, conf.Storage.Index(ctx, firstBlock, Finalization{})) + require.NoError(t, conf.Storage.Index(ctx, lastBlock, Finalization{})) + + e, err := NewEpoch(conf) + require.NoError(t, err) + t.Cleanup(e.Stop) + require.Equal(t, uint64(2), e.Storage.NumBlocks()) + require.NoError(t, e.Start()) + + md := e.Metadata() + require.Equal(t, tt.expectedEpoch, md.Epoch) + require.Equal(t, uint64(2), md.Round) + require.Equal(t, uint64(2), md.Seq) + require.Equal(t, lastBlock.BlockHeader().Digest, md.Prev) + }) + } } func TestRecoveryAsLeader(t *testing.T) { From 106e63498c40c2c195f57b6039e8134cf654f238 Mon Sep 17 00:00:00 2001 From: samliok Date: Mon, 7 Sep 2026 23:05:43 -0400 Subject: [PATCH 2/9] remove comment --- simplex/epoch.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/simplex/epoch.go b/simplex/epoch.go index 2fe4d0d1..0ff80d89 100644 --- a/simplex/epoch.go +++ b/simplex/epoch.go @@ -748,9 +748,6 @@ func (e *Epoch) setMetadataFromRecords(records [][]byte) error { } if finalization.Finalization.Round >= highestRound { highestRound = finalization.Finalization.Round - // We do not need to check if this finalization is associated for a sealing block, because it is impossible for a finalization - // for another epoch to exist in our wal, yet the original epoch remains un-sealed. - // This is because we issue te-locks after sealing blocks, and never move onto the next epoch until the original sealing block was indexed. highestEpoch = finalization.Finalization.Epoch found = true } From 48c4b3f3d010bf7ebd390a5ca42a2a2cb82103f1 Mon Sep 17 00:00:00 2001 From: samliok Date: Tue, 8 Sep 2026 11:23:39 -0400 Subject: [PATCH 3/9] add to set metadata --- simplex/epoch.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/simplex/epoch.go b/simplex/epoch.go index 0ff80d89..9d38d416 100644 --- a/simplex/epoch.go +++ b/simplex/epoch.go @@ -700,15 +700,22 @@ func (e *Epoch) setMetadataFromStorage() error { return nil } - e.round = e.lastBlock.VerifiedBlock.BlockHeader().Round + 1 + bh := e.lastBlock.VerifiedBlock.BlockHeader() + e.round = bh.Round + 1 // The last block we indexed was a sealing block, therefore the epoch number is that blocks sequence if e.lastBlock.VerifiedBlock.SealingBlockInfo() != nil { - e.Epoch = e.lastBlock.VerifiedBlock.BlockHeader().Seq + e.Epoch = bh.Seq return nil } - e.Epoch = e.lastBlock.VerifiedBlock.BlockHeader().Epoch + // a zero epoch represents genesis or a non-simplex block + if bh.Epoch == 0 { + e.Epoch = bh.Seq + 1 + return nil + } + + e.Epoch = bh.Epoch return nil } From 8b960f3172b69e3275199767f6d497e8bb0f270d Mon Sep 17 00:00:00 2001 From: samliok Date: Tue, 8 Sep 2026 15:21:56 -0400 Subject: [PATCH 4/9] instance test and other bugs --- instance.go | 8 ++++ instance_helpers_test.go | 9 +++- instance_test.go | 39 ++++++++++++++++++ simplex/epoch.go | 8 ++-- simplex/epoch_test.go | 59 ++++++++++++++++++++++++-- simplex/recovery_test.go | 89 ++++++++++++++++++++++++++++------------ simplex/util_test.go | 7 +--- 7 files changed, 179 insertions(+), 40 deletions(-) diff --git a/instance.go b/instance.go index e59daf85..873bdeb3 100644 --- a/instance.go +++ b/instance.go @@ -550,6 +550,14 @@ func (i *Instance) createEpochConfig(validators common.Nodes) (*epochConfig, err } func (i *Instance) maybeGarbageCollectWAL() error { + lastNonSimplexHeight := i.Config.LastNonSimplexInnerBlock.Height() + numBlocks := i.Config.Storage.NumBlocks() + + // Only fetch the last block if it is a simplex block + if lastNonSimplexHeight+1 == numBlocks { + return nil + } + lastBlock, _, err := LastBlock(i.Config.Storage) if err != nil { return fmt.Errorf("error retrieving last block: %w", err) diff --git a/instance_helpers_test.go b/instance_helpers_test.go index 48f6b017..0e3db94a 100644 --- a/instance_helpers_test.go +++ b/instance_helpers_test.go @@ -625,6 +625,8 @@ type nodeConfig struct { storage *testStorage // wals are pre-existing WALs the instance restores on start. wals []wal.DeletableWAL + + lastNonSimplexBlock avalanchego.VMBlock } // addNode creates and starts a node in the network. @@ -648,8 +650,13 @@ func (n *network) addNodeWithConfig(id common.NodeID, cfg nodeConfig) *node { vm := newBlockBuilderVM(storage, n.pending) wc := &walCreator{t: n.t} + var lastNonSimplex avalanchego.VMBlock = genesisBlock + if cfg.lastNonSimplexBlock != nil { + lastNonSimplex = cfg.lastNonSimplexBlock + } + instance := NewInstance(Config{ - LastNonSimplexInnerBlock: genesisBlock, + LastNonSimplexInnerBlock: lastNonSimplex, ParameterConfig: paramConfig, PlatformChain: n.pChain, Broadcaster: comm, diff --git a/instance_test.go b/instance_test.go index c02e0cda..ef62dd9c 100644 --- a/instance_test.go +++ b/instance_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/ava-labs/simplex/avalanchego" "github.com/ava-labs/simplex/common" metadata "github.com/ava-labs/simplex/msm" "github.com/ava-labs/simplex/simplex" @@ -481,3 +482,41 @@ func TestValidatorRequestsGenesis(t *testing.T) { require.NoError(t, validator.inst.HandleMessage(msg, nonValidatorID.NodeID[:])) } + +func TestValidatorSetsMetadataFromSnowman(t *testing.T) { + validatorID := newNodeMapping(1) + numNonSimplexBlocks := uint64(10) + genesisSet := []metadata.NodeBLSMapping{validatorID} + + pChain := newTestPChain(genesisSet) + network := newNetwork(t, pChain) + network.seq = numNonSimplexBlocks + storage := newTestStorage() + + var lastBlock avalanchego.VMBlock + for i := range numNonSimplexBlocks { + md := common.ProtocolMetadata{ + Epoch: 0, // non-simplex don't have an epoch + Round: 0, // non-simplex blocks don't have rounds + Seq: uint64(i), + } + innerBlock := &testInnerBlock{Height_: uint64(i), TS: time.Now(), Payload: []byte{byte(i)}} + pb := &ParsedBlock{StateMachineBlock: metadata.StateMachineBlock{InnerBlock: innerBlock, Metadata: metadata.StateMachineMetadata{ + SimplexProtocolMetadata: md, + }}} + storage.Index(t.Context(), pb, common.Finalization{}) + lastBlock = innerBlock + } + + config := nodeConfig{ + storage: storage, + lastNonSimplexBlock: lastBlock, + } + + network.addNodeWithConfig(validatorID.NodeID[:], config).sync() + + block, _ := network.acceptNewBlock() + require.Equal(t, numNonSimplexBlocks, block.BlockHeader().Epoch) + require.Equal(t, numNonSimplexBlocks, block.BlockHeader().Round) + require.Equal(t, numNonSimplexBlocks, block.BlockHeader().Seq) +} diff --git a/simplex/epoch.go b/simplex/epoch.go index f2c19831..64679297 100644 --- a/simplex/epoch.go +++ b/simplex/epoch.go @@ -709,9 +709,11 @@ func (e *Epoch) setMetadataFromStorage() error { return nil } - // a zero epoch represents genesis or a non-simplex block - if bh.Epoch == 0 { - e.Epoch = bh.Seq + 1 + // An indexed block without a finalization predates Simplex, so no Simplex block has + // been indexed and the first Simplex epoch is the sequence the first one will occupy. + if e.lastBlock.Finalization.QC == nil { + e.Epoch = e.Storage.NumBlocks() + e.round = e.Storage.NumBlocks() return nil } diff --git a/simplex/epoch_test.go b/simplex/epoch_test.go index cd594235..852634c2 100644 --- a/simplex/epoch_test.go +++ b/simplex/epoch_test.go @@ -1034,9 +1034,13 @@ func TestEpochResizesBlacklistOnEpochChange(t *testing.T) { require.Equal(t, uint16(1), sealingBlock.Blacklist().NodeCount, "blacklist must contain exactly one node") + sigAggregator := &testutil.TestSignatureAggregator{N: len(nodes)} + epoch0Finalization, _ := testutil.NewFinalizationRecord(t, sigAggregator, epoch0Block, nodes) + sealingFinalization, _ := testutil.NewFinalizationRecord(t, sigAggregator, sealingBlock, nodes) + conf, _, _ := testutil.DefaultTestNodeEpochConfig(t, nodes[0], testutil.NewNoopComm(nodes), bb) - require.NoError(t, conf.Storage.Index(context.Background(), epoch0Block, Finalization{})) - require.NoError(t, conf.Storage.Index(context.Background(), sealingBlock, Finalization{})) + require.NoError(t, conf.Storage.Index(context.Background(), epoch0Block, epoch0Finalization)) + require.NoError(t, conf.Storage.Index(context.Background(), sealingBlock, sealingFinalization)) e, err := NewEpoch(conf) require.NoError(t, err) @@ -1054,8 +1058,8 @@ func TestEpochResizesBlacklistOnEpochChange(t *testing.T) { // Next, create the other node (follower) and ensure it can verify the block. conf, wal, _ := testutil.DefaultTestNodeEpochConfig(t, nodes[1], testutil.NewNoopComm(nodes), bb) - require.NoError(t, conf.Storage.Index(context.Background(), epoch0Block, Finalization{})) - require.NoError(t, conf.Storage.Index(context.Background(), sealingBlock, Finalization{})) + require.NoError(t, conf.Storage.Index(context.Background(), epoch0Block, epoch0Finalization)) + require.NoError(t, conf.Storage.Index(context.Background(), sealingBlock, sealingFinalization)) e, err = NewEpoch(conf) require.NoError(t, err) @@ -2742,3 +2746,50 @@ func TestEpochIgnoresReplicatedQuorumRoundsFromOtherEpochs(t *testing.T) { testutil.WaitToEnterRound(t, e, md.Round+2) require.Equal(t, int32(3), ignored.Load(), "quorum rounds from our own epoch must not be ignored") } + +// nonSimplexLedger holds blocks indexed by a previous consensus engine. They sit at +// successive ledger heights but carry no Simplex metadata and no finalization. +type nonSimplexLedger struct { + blocks []VerifiedBlock +} + +func (l *nonSimplexLedger) NumBlocks() uint64 { + return uint64(len(l.blocks)) +} + +func (l *nonSimplexLedger) Retrieve(seq uint64) (VerifiedBlock, Finalization, error) { + if seq >= uint64(len(l.blocks)) { + return nil, Finalization{}, ErrBlockNotFound + } + return l.blocks[seq], Finalization{}, nil +} + +func (l *nonSimplexLedger) Index(context.Context, VerifiedBlock, Finalization) error { + return nil +} + +// TestEpochStartsAfterNonSimplexBlocks asserts the epoch a node starts on when every +// indexed block predates Simplex. Such blocks carry no Simplex metadata, so their tip +// names no epoch, and the first Simplex epoch is the sequence the first Simplex block +// will occupy. +func TestEpochStartsAfterNonSimplexBlocks(t *testing.T) { + nodes := []NodeID{{1}, {2}, {3}, {4}} + bb := testutil.NewTestBlockBuilder() + conf, _, _ := testutil.DefaultTestNodeEpochConfig(t, nodes[0], testutil.NewNoopComm(nodes), bb) + + ledger := &nonSimplexLedger{} + for i := range 3 { + block := testutil.NewTestBlock(ProtocolMetadata{}, emptyBlacklist) + block.Data = []byte{byte(i)} + block.ComputeDigest() + ledger.blocks = append(ledger.blocks, block) + } + conf.Storage = ledger + + e, err := NewEpoch(conf) + require.NoError(t, err) + t.Cleanup(e.Stop) + require.NoError(t, e.Start()) + + require.Equal(t, uint64(3), e.Metadata().Epoch) +} diff --git a/simplex/recovery_test.go b/simplex/recovery_test.go index 715b8e05..d6a8c027 100644 --- a/simplex/recovery_test.go +++ b/simplex/recovery_test.go @@ -157,8 +157,9 @@ func TestRecoverFromWalWithStorage(t *testing.T) { sigAggregrator := &testutil.TestSignatureAggregator{N: 4} conf, wal, storage := testutil.DefaultTestNodeEpochConfig(t, nodes[0], testutil.NewNoopComm(nodes), bb) - err := storage.Index(ctx, testutil.NewTestBlock(ProtocolMetadata{Seq: 0, Round: 0, Epoch: 0}, emptyBlacklist), Finalization{}) - require.NoError(t, err) + indexedBlock := testutil.NewTestBlock(ProtocolMetadata{Seq: 0, Round: 0, Epoch: 0}, emptyBlacklist) + indexedFinalization, _ := testutil.NewFinalizationRecord(t, sigAggregrator, indexedBlock, nodes[0:quorum]) + require.NoError(t, storage.Index(ctx, indexedBlock, indexedFinalization)) e, err := NewEpoch(conf) require.NoError(t, err) @@ -550,53 +551,87 @@ func TestRecoveryBlocksIndexed(t *testing.T) { // TestEpochCorrectlyInitializesMetadataFromStorage asserts the next block's metadata is // derived from the last indexed block. func TestEpochCorrectlyInitializesMetadataFromStorage(t *testing.T) { + ctx := context.Background() + nodes := []NodeID{{1}, {2}, {3}, {4}} + tests := []struct { name string - sealing bool + storage func(t *testing.T) Storage + expectedRound uint64 + expectedSeq uint64 expectedEpoch uint64 }{ { - name: "normal block", - expectedEpoch: 3, + name: "normal block", + storage: func(t *testing.T) Storage { + storage := testutil.NewInMemStorage() + for _, block := range createBlocks(t, nodes, 2) { + require.NoError(t, storage.Index(ctx, block.VerifiedBlock, block.Finalization)) + } + + return storage + }, + expectedRound: 2, + expectedSeq: 2, + expectedEpoch: 0, }, { - name: "sealing block", - sealing: true, - expectedEpoch: 1, + name: "sealing block", + storage: func(t *testing.T) Storage { + storage := testutil.NewInMemStorage() + blocks := createBlocks(t, nodes, 8) + blocks[7].VerifiedBlock.(*testutil.TestBlock).SealingInfo = &SealingBlockInfo{ + ValidatorSet: NodeIDs(nodes).EqualWeightedNodes(), + PrevSealingBlockHash: blocks[0].VerifiedBlock.BlockHeader().Digest, + } + for _, block := range blocks { + require.NoError(t, storage.Index(ctx, block.VerifiedBlock, block.Finalization)) + } + + return storage + }, + expectedRound: 8, + expectedSeq: 8, + // The epoch is the sequence of the last indexed sealing block. + expectedEpoch: 7, }, - } + { + // Blocks predating Simplex carry no finalization, so the round and epoch + // come from the number of indexed blocks. + name: "non simplex blocks", + storage: func(t *testing.T) Storage { + storage := testutil.NewInMemStorage() + for _, block := range createBlocks(t, nodes, 2) { + require.NoError(t, storage.Index(ctx, block.VerifiedBlock, Finalization{})) + } - ctx := context.Background() - nodes := []NodeID{{1}, {2}, {3}, {4}} + return storage + }, + expectedRound: 2, + expectedSeq: 2, + expectedEpoch: 2, + }, + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { bb := testutil.NewTestBlockBuilder() conf, _, _ := testutil.DefaultTestNodeEpochConfig(t, nodes[0], testutil.NewNoopComm(nodes), bb) - - firstBlock := testutil.NewTestBlock(ProtocolMetadata{Seq: 0, Round: 0, Epoch: 3}, emptyBlacklist) - lastBlock := testutil.NewTestBlock(ProtocolMetadata{Seq: 1, Round: 1, Epoch: 3, Prev: firstBlock.Digest}, emptyBlacklist) - if tt.sealing { - lastBlock.SealingInfo = &SealingBlockInfo{ - ValidatorSet: NodeIDs(nodes).EqualWeightedNodes(), - PrevSealingBlockHash: firstBlock.Digest, - } - } - - require.NoError(t, conf.Storage.Index(ctx, firstBlock, Finalization{})) - require.NoError(t, conf.Storage.Index(ctx, lastBlock, Finalization{})) + conf.Storage = tt.storage(t) e, err := NewEpoch(conf) require.NoError(t, err) t.Cleanup(e.Stop) - require.Equal(t, uint64(2), e.Storage.NumBlocks()) require.NoError(t, e.Start()) + tip, err := RetrieveLastIndexFromStorage(e.Storage) + require.NoError(t, err) + md := e.Metadata() require.Equal(t, tt.expectedEpoch, md.Epoch) - require.Equal(t, uint64(2), md.Round) - require.Equal(t, uint64(2), md.Seq) - require.Equal(t, lastBlock.BlockHeader().Digest, md.Prev) + require.Equal(t, tt.expectedRound, md.Round) + require.Equal(t, tt.expectedSeq, md.Seq) + require.Equal(t, tip.VerifiedBlock.BlockHeader().Digest, md.Prev) }) } } diff --git a/simplex/util_test.go b/simplex/util_test.go index ad8373b9..fdf1b264 100644 --- a/simplex/util_test.go +++ b/simplex/util_test.go @@ -17,12 +17,9 @@ import ( ) func TestRetrieveFromStorage(t *testing.T) { + nodes := []NodeID{{1}, {2}, {3}, {4}} block := testutil.NewTestBlock(ProtocolMetadata{Seq: 0}, emptyBlacklist) - finalization := Finalization{ - Finalization: ToBeSignedFinalization{ - BlockHeader: block.BlockHeader(), - }, - } + finalization, _ := testutil.NewFinalizationRecord(t, &testutil.TestSignatureAggregator{N: len(nodes)}, block, nodes[:Quorum(len(nodes))]) normalStorage := testutil.NewInMemStorage() err := normalStorage.Index(context.Background(), block, finalization) require.NoError(t, err) From c2a3cdcc4ae731b87cd13ca8f9e83ba883e3b9f1 Mon Sep 17 00:00:00 2001 From: samliok Date: Tue, 8 Sep 2026 15:24:10 -0400 Subject: [PATCH 5/9] remove test --- simplex/epoch_test.go | 47 ------------------------------------------- 1 file changed, 47 deletions(-) diff --git a/simplex/epoch_test.go b/simplex/epoch_test.go index 852634c2..5ceded7d 100644 --- a/simplex/epoch_test.go +++ b/simplex/epoch_test.go @@ -2746,50 +2746,3 @@ func TestEpochIgnoresReplicatedQuorumRoundsFromOtherEpochs(t *testing.T) { testutil.WaitToEnterRound(t, e, md.Round+2) require.Equal(t, int32(3), ignored.Load(), "quorum rounds from our own epoch must not be ignored") } - -// nonSimplexLedger holds blocks indexed by a previous consensus engine. They sit at -// successive ledger heights but carry no Simplex metadata and no finalization. -type nonSimplexLedger struct { - blocks []VerifiedBlock -} - -func (l *nonSimplexLedger) NumBlocks() uint64 { - return uint64(len(l.blocks)) -} - -func (l *nonSimplexLedger) Retrieve(seq uint64) (VerifiedBlock, Finalization, error) { - if seq >= uint64(len(l.blocks)) { - return nil, Finalization{}, ErrBlockNotFound - } - return l.blocks[seq], Finalization{}, nil -} - -func (l *nonSimplexLedger) Index(context.Context, VerifiedBlock, Finalization) error { - return nil -} - -// TestEpochStartsAfterNonSimplexBlocks asserts the epoch a node starts on when every -// indexed block predates Simplex. Such blocks carry no Simplex metadata, so their tip -// names no epoch, and the first Simplex epoch is the sequence the first Simplex block -// will occupy. -func TestEpochStartsAfterNonSimplexBlocks(t *testing.T) { - nodes := []NodeID{{1}, {2}, {3}, {4}} - bb := testutil.NewTestBlockBuilder() - conf, _, _ := testutil.DefaultTestNodeEpochConfig(t, nodes[0], testutil.NewNoopComm(nodes), bb) - - ledger := &nonSimplexLedger{} - for i := range 3 { - block := testutil.NewTestBlock(ProtocolMetadata{}, emptyBlacklist) - block.Data = []byte{byte(i)} - block.ComputeDigest() - ledger.blocks = append(ledger.blocks, block) - } - conf.Storage = ledger - - e, err := NewEpoch(conf) - require.NoError(t, err) - t.Cleanup(e.Stop) - require.NoError(t, e.Start()) - - require.Equal(t, uint64(3), e.Metadata().Epoch) -} From 3f022a9481697bcb14369e582ae8c7536f95fffe Mon Sep 17 00:00:00 2001 From: samliok Date: Tue, 8 Sep 2026 15:26:06 -0400 Subject: [PATCH 6/9] lint --- instance_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instance_test.go b/instance_test.go index ef62dd9c..924599c5 100644 --- a/instance_test.go +++ b/instance_test.go @@ -504,7 +504,7 @@ func TestValidatorSetsMetadataFromSnowman(t *testing.T) { pb := &ParsedBlock{StateMachineBlock: metadata.StateMachineBlock{InnerBlock: innerBlock, Metadata: metadata.StateMachineMetadata{ SimplexProtocolMetadata: md, }}} - storage.Index(t.Context(), pb, common.Finalization{}) + require.NoError(t, storage.Index(t.Context(), pb, common.Finalization{})) lastBlock = innerBlock } From c340919f4dd569871ba0814176b406115bffe211 Mon Sep 17 00:00:00 2001 From: samliok Date: Wed, 9 Sep 2026 11:45:45 -0400 Subject: [PATCH 7/9] lint --- instance_helpers_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instance_helpers_test.go b/instance_helpers_test.go index 97d2fced..e67d443a 100644 --- a/instance_helpers_test.go +++ b/instance_helpers_test.go @@ -661,7 +661,7 @@ type nodeConfig struct { // storage the node starts from; defaults to a fresh storage holding only genesis. storage *testStorage // wals are pre-existing WALs the instance restores on start. - wals []wal.DeletableWAL + wals []wal.DeletableWAL lastNonSimplexBlock avalanchego.VMBlock // existingNode indicates whether the node is being added to the network for the first time (false) or is a restart of an existing node (true). existingNode bool From 546f365d97ff25329cc465cc89f2f4513f0d25bf Mon Sep 17 00:00:00 2001 From: samliok Date: Wed, 9 Sep 2026 12:30:49 -0400 Subject: [PATCH 8/9] remove round setting --- instance_test.go | 2 +- simplex/epoch.go | 1 - simplex/recovery_test.go | 8 ++++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/instance_test.go b/instance_test.go index fe966e58..b6985fcf 100644 --- a/instance_test.go +++ b/instance_test.go @@ -536,6 +536,6 @@ func TestValidatorSetsMetadataFromSnowman(t *testing.T) { block, _ := network.acceptNewBlock() require.Equal(t, numNonSimplexBlocks, block.BlockHeader().Epoch) - require.Equal(t, numNonSimplexBlocks, block.BlockHeader().Round) + require.Equal(t, uint64(1), block.BlockHeader().Round) require.Equal(t, numNonSimplexBlocks, block.BlockHeader().Seq) } diff --git a/simplex/epoch.go b/simplex/epoch.go index 167e0576..bdcb32a2 100644 --- a/simplex/epoch.go +++ b/simplex/epoch.go @@ -713,7 +713,6 @@ func (e *Epoch) setMetadataFromStorage() error { // been indexed and the first Simplex epoch is the sequence the first one will occupy. if e.lastBlock.Finalization.QC == nil { e.Epoch = e.Storage.NumBlocks() - e.round = e.Storage.NumBlocks() return nil } diff --git a/simplex/recovery_test.go b/simplex/recovery_test.go index d6a8c027..7422a25a 100644 --- a/simplex/recovery_test.go +++ b/simplex/recovery_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/ava-labs/simplex/common" . "github.com/ava-labs/simplex/common" . "github.com/ava-labs/simplex/simplex" "github.com/ava-labs/simplex/testutil" @@ -601,13 +602,16 @@ func TestEpochCorrectlyInitializesMetadataFromStorage(t *testing.T) { name: "non simplex blocks", storage: func(t *testing.T) Storage { storage := testutil.NewInMemStorage() - for _, block := range createBlocks(t, nodes, 2) { + for i, block := range createBlocks(t, nodes, 2) { + block.VerifiedBlock.(*testutil.TestBlock).Metadata = common.ProtocolMetadata{ + Seq: uint64(i), // set the sequence so we can index without error + } require.NoError(t, storage.Index(ctx, block.VerifiedBlock, Finalization{})) } return storage }, - expectedRound: 2, + expectedRound: 1, expectedSeq: 2, expectedEpoch: 2, }, From b2103fabd9c6285a25b00beb3eece2c4e372cac7 Mon Sep 17 00:00:00 2001 From: samliok Date: Wed, 9 Sep 2026 12:33:00 -0400 Subject: [PATCH 9/9] lint --- simplex/recovery_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/simplex/recovery_test.go b/simplex/recovery_test.go index 7422a25a..db91f5f3 100644 --- a/simplex/recovery_test.go +++ b/simplex/recovery_test.go @@ -9,7 +9,6 @@ import ( "testing" "time" - "github.com/ava-labs/simplex/common" . "github.com/ava-labs/simplex/common" . "github.com/ava-labs/simplex/simplex" "github.com/ava-labs/simplex/testutil" @@ -603,7 +602,7 @@ func TestEpochCorrectlyInitializesMetadataFromStorage(t *testing.T) { storage: func(t *testing.T) Storage { storage := testutil.NewInMemStorage() for i, block := range createBlocks(t, nodes, 2) { - block.VerifiedBlock.(*testutil.TestBlock).Metadata = common.ProtocolMetadata{ + block.VerifiedBlock.(*testutil.TestBlock).Metadata = ProtocolMetadata{ Seq: uint64(i), // set the sequence so we can index without error } require.NoError(t, storage.Index(ctx, block.VerifiedBlock, Finalization{}))