Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions bug_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package simplex_test

import (
"context"
"testing"

. "github.com/ava-labs/simplex"
"github.com/ava-labs/simplex/testutil"
"github.com/stretchr/testify/require"
)

// TestChainBreak tests that a node should not send two finalize votes for the same sequence number
// It does so by advancing round 1 from a notarization, then advancing round 2 using a block built off an supposed empty notarization from round 1.
func TestChainBreak(t *testing.T) {
bb := &testutil.TestBlockBuilder{Out: make(chan *testutil.TestBlock, 1), BlockShouldBeBuilt: make(chan struct{}, 1)}
ctx := context.Background()
nodes := []NodeID{{1}, {2}, {3}, {4}}
initialBlock := createBlocks(t, nodes, 1)[0]
recordingComm := &recordingComm{Communication: testutil.NewNoopComm(nodes), BroadcastMessages: make(chan *Message, 100), SentMessages: make(chan *Message, 100)}
conf, _, storage := testutil.DefaultTestNodeEpochConfig(t, nodes[0], recordingComm, bb)
storage.Index(ctx, initialBlock.VerifiedBlock, initialBlock.Finalization)

e, err := NewEpoch(conf)
require.NoError(t, err)

require.NoError(t, e.Start())
require.Equal(t, uint64(1), e.Metadata().Seq)

// we receive a block and then notarize(this sends out a finalize vote for the block)
advanceRoundFromNotarization(t, e, bb)
require.Equal(t, uint64(2), e.Metadata().Seq)
require.Equal(t, uint64(2), e.Metadata().Round)

// wait for finalize votes
for {
msg := <-recordingComm.BroadcastMessages
if msg.FinalizeVote != nil {
require.Equal(t, uint64(1), msg.FinalizeVote.Finalization.Round)
require.Equal(t, uint64(1), msg.FinalizeVote.Finalization.Seq)
break
}
}

// clear the recorded messages
for len(recordingComm.BroadcastMessages) > 0 {
<-recordingComm.BroadcastMessages
}

advanceRoundWithMD(t, e, bb, true, true, ProtocolMetadata{
Round: 2,
Seq: 1, // next seq is 1 not 2
Prev: initialBlock.VerifiedBlock.BlockHeader().Digest,
})


for {
msg := <-recordingComm.BroadcastMessages
if msg.FinalizeVote != nil {
// we should not have sent two different finalize votes for the same seq
require.NotEqual(t, uint64(2), msg.FinalizeVote.Finalization.Round)
require.NotEqual(t, uint64(1), msg.FinalizeVote.Finalization.Seq)
break
}

if len(recordingComm.BroadcastMessages) == 0 {
break
}
}
}

// returns the seq and the digest we have sent a finalize vote for
func advanceWithFinalizeCheck(t *testing.T, e *Epoch, recordingComm *recordingComm, bb *testutil.TestBlockBuilder) (uint64, Digest){
round := e.Metadata().Round
seq := e.Metadata().Seq
advanceRoundFromNotarization(t, e, bb)

// wait for finalize votes for the round
for {
msg := <-recordingComm.BroadcastMessages
if msg.FinalizeVote != nil {
require.Equal(t, round, msg.FinalizeVote.Finalization.Round)
require.Equal(t, seq, msg.FinalizeVote.Finalization.Seq)
return seq, msg.FinalizeVote.Finalization.Digest
}
}
}

func TestChainBreakLargeGap(t *testing.T) {
for numEmpty := range uint64(5) {
for numNotarizations := range uint64(5) {
for seqToDoubleFinalize := range numNotarizations {
testChainBreakLargeGap(t, numEmpty, numNotarizations, seqToDoubleFinalize)
}
}
}
}

func testChainBreakLargeGap(t *testing.T, numEmptyNotarizations uint64, numNotarizations uint64, seqToDoubleFinalize uint64) {
bb := &testutil.TestBlockBuilder{Out: make(chan *testutil.TestBlock, 1), BlockShouldBeBuilt: make(chan struct{}, 1)}
ctx := context.Background()
nodes := []NodeID{{1}, {2}, {3}, {4}}
initialBlock := createBlocks(t, nodes, 1)[0]
recordingComm := &recordingComm{Communication: testutil.NewNoopComm(nodes), BroadcastMessages: make(chan *Message, 100), SentMessages: make(chan *Message, 100)}
conf, _, storage := testutil.DefaultTestNodeEpochConfig(t, nodes[0], recordingComm, bb)
storage.Index(ctx, initialBlock.VerifiedBlock, initialBlock.Finalization)

finalizeVoteSeqs := make(map[uint64]Digest)
e, err := NewEpoch(conf)
require.NoError(t, err)

require.NoError(t, e.Start())
require.Equal(t, uint64(1), e.Metadata().Seq)

notarizationsLeft := numNotarizations
for i := uint64(0); i < numEmptyNotarizations; i++ {
leader := LeaderForRound(e.Comm.Nodes(), e.Metadata().Round)
if e.ID.Equals(leader) {
require.NotZero(t, notarizationsLeft)
seq, digest := advanceWithFinalizeCheck(t, e, recordingComm, bb)
finalizeVoteSeqs[seq] = digest
notarizationsLeft--
i--
continue
}

advanceRoundFromEmpty(t, e)
}

for range notarizationsLeft {
seq, digest := advanceWithFinalizeCheck(t, e, recordingComm, bb)
finalizeVoteSeqs[seq] = digest
}

require.Equal(t, 1+numEmptyNotarizations+numNotarizations, e.Metadata().Round)
require.Equal(t, 1 + numNotarizations, e.Metadata().Seq)

// clear the recorded messages
for len(recordingComm.BroadcastMessages) > 0 {
<-recordingComm.BroadcastMessages
}

advanceRoundWithMD(t, e, bb, true, true, ProtocolMetadata{
Round: e.Metadata().Round,
Seq: seqToDoubleFinalize,
Prev: finalizeVoteSeqs[seqToDoubleFinalize-1],
})

for {
msg := <-recordingComm.BroadcastMessages
if msg.FinalizeVote != nil {
// we should not have sent two different finalize votes for the same seq
// require.NotEqual(t, e, msg.FinalizeVote.Finalization.Round)
require.NotEqual(t, seqToDoubleFinalize, msg.FinalizeVote.Finalization.Seq)
break
}

if len(recordingComm.BroadcastMessages) == 0 {
break
}
}
}
67 changes: 67 additions & 0 deletions epoch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ func TestEpochStartedTwice(t *testing.T) {
require.ErrorIs(t, e.Start(), ErrAlreadyStarted)
}


func advanceRoundFromEmpty(t *testing.T, e *Epoch) {
leader := LeaderForRound(e.Comm.Nodes(), e.Metadata().Round)
require.False(t, e.ID.Equals(leader), "epoch cannot be the leader for the empty round")
Expand Down Expand Up @@ -995,6 +996,72 @@ func (b *listenerComm) Send(msg *Message, id NodeID) {
b.in <- msg
}


// garbageCollectSuspectedNodes progresses [e] to a new round. If [notarize] is set, the round will progress due to a notarization.
// If [finalize] is set, the round will advance and the block will be indexed to storage.
func advanceRoundWithMD(t *testing.T, e *simplex.Epoch, bb *testutil.TestBlockBuilder, notarize bool, finalize bool, md simplex.ProtocolMetadata) (simplex.VerifiedBlock, *simplex.Notarization) {
require.True(t, notarize || finalize, "must either notarize or finalize a round to advance")
nextSeqToCommit := e.Storage.NumBlocks()
nodes := e.Comm.Nodes()
quorum := simplex.Quorum(len(nodes))
// leader is the proposer of the new block for the given round
leader := simplex.LeaderForRound(nodes, md.Round)
// only create blocks if we are not the node running the epoch
isEpochNode := leader.Equals(e.ID)
if !isEpochNode {
_, ok := bb.BuildBlock(context.Background(), md, simplex.Blacklist{
NodeCount: uint16(len(e.EpochConfig.Comm.Nodes())),
})
require.True(t, ok)
}

block := <-bb.Out

if !isEpochNode {
// send node a message from the leader
vote, err := testutil.NewTestVote(block, leader)
require.NoError(t, err)
err = e.HandleMessage(&simplex.Message{
BlockMessage: &simplex.BlockMessage{
Vote: *vote,
Block: block,
},
}, leader)
require.NoError(t, err)
}

var notarization *simplex.Notarization
if notarize {
// start at one since our node has already voted
n, err := testutil.NewNotarization(e.Logger, e.SignatureAggregator, block, nodes[0:quorum])
testutil.InjectTestNotarization(t, e, n, nodes[1])

e.WAL.(*testutil.TestWAL).AssertNotarization(block.Metadata.Round)
require.NoError(t, err)
notarization = &n
}

if finalize {
for i := 0; i <= quorum; i++ {
if nodes[i].Equals(e.ID) {
continue
}
testutil.InjectTestFinalizeVote(t, e, block, nodes[i])
}

if nextSeqToCommit != block.Metadata.Seq {
testutil.WaitToEnterRound(t, e, block.Metadata.Round+1)
return block, notarization
}

blockFromStorage := e.Storage.(*testutil.InMemStorage).WaitForBlockCommit(block.Metadata.Seq)
require.Equal(t, block, blockFromStorage)
}

return block, notarization
}


// garbageCollectSuspectedNodes progresses [e] to a new round. If [notarize] is set, the round will progress due to a notarization.
// If [finalize] is set, the round will advance and the block will be indexed to storage.
func advanceRound(t *testing.T, e *simplex.Epoch, bb *testutil.TestBlockBuilder, notarize bool, finalize bool) (simplex.VerifiedBlock, *simplex.Notarization) {
Expand Down
Loading