Skip to content
26 changes: 16 additions & 10 deletions instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -437,10 +436,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")
Expand All @@ -454,7 +453,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)
Expand Down Expand Up @@ -524,7 +523,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.
Expand Down Expand Up @@ -552,6 +550,14 @@ func (i *Instance) createEpochConfig(epoch uint64, validators common.Nodes) (*ep
}

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)
Expand All @@ -572,9 +578,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()
Expand Down
10 changes: 8 additions & 2 deletions instance_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,8 @@ 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
}
Expand All @@ -682,8 +683,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,
Expand Down
39 changes: 39 additions & 0 deletions instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -500,3 +501,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,
}}}
require.NoError(t, 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, uint64(1), block.BlockHeader().Round)
require.Equal(t, numNonSimplexBlocks, block.BlockHeader().Seq)
}
22 changes: 19 additions & 3 deletions simplex/epoch.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,15 @@ type EpochConfig struct {
Storage common.Storage
WAL common.WriteAheadLog
BlockBuilder common.BlockBuilder
Epoch uint64
StartTime time.Time
ReplicationEnabled bool
RandomSource *rand.Rand
}

type Epoch struct {
EpochConfig

Epoch uint64
// Runtime
blockBuilder common.BlockBuilder
epochSealed atomic.Bool
Expand Down Expand Up @@ -699,8 +700,23 @@ func (e *Epoch) setMetadataFromStorage() error {
return nil
}

e.round = e.lastBlock.VerifiedBlock.BlockHeader().Round + 1
e.Epoch = e.lastBlock.VerifiedBlock.BlockHeader().Epoch
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 = bh.Seq
return nil
}

// 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()
return nil
}

e.Epoch = bh.Epoch
return nil
}

Expand Down
50 changes: 28 additions & 22 deletions simplex/epoch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1020,58 +1020,64 @@ 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why is this change needed?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

because we can no longer artificially set the epoch in the config, so we need to go through with actual messages/storage

// 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")

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, epoch0Finalization))
require.NoError(t, conf.Storage.Index(context.Background(), sealingBlock, sealingFinalization))

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,
"blacklist must be resized to the new epoch's validator count")
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, epoch0Finalization))
require.NoError(t, conf.Storage.Index(context.Background(), sealingBlock, sealingFinalization))

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{
BlockMessage: &BlockMessage{
Vote: *vote,
Block: block,
},
}, nodes[1])
}, nodes[0])
require.NoError(t, err)
wal.AssertNotarization(1)

wal.AssertNotarization(2)
}

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