Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0cc2f78
replicate chains and refactor replication state
samliok Nov 24, 2025
b8552f3
breaking MultiNodeBlacklist Test
samliok Nov 24, 2025
af999d3
failove test uncomment
samliok Nov 24, 2025
24c6462
dont skip replicationafternodedisconnects
samliok Nov 24, 2025
ee838ad
remove cancel since we may not have updated our epoch round yet
samliok Nov 25, 2025
285d2e0
uncomment so we can check ci but test is still flaking
samliok Nov 25, 2025
e75288b
blacklist flake fix
samliok Nov 25, 2025
cfc356e
Merge branch 'main' into cr
samliok Dec 1, 2025
a1129d2
merge conflicts
samliok Dec 1, 2025
f17e136
cleanup old finalized tasks, nits and clarifications from review
samliok Dec 1, 2025
61c8191
add finalized check in process
samliok Dec 1, 2025
f526717
nits
samliok Dec 1, 2025
5a8248e
nits
samliok Dec 1, 2025
27f3b8e
don't vote
samliok Dec 1, 2025
3ad0158
don't vote pt 21
samliok Dec 1, 2025
c2d8ef8
flake
samliok Dec 1, 2025
217f04b
flake
samliok Dec 1, 2025
98edf52
send segments helper
samliok Dec 1, 2025
6a02896
remove digests from map
samliok Dec 1, 2025
78f92c1
add mixing comment
samliok Dec 1, 2025
8ab3735
naming, println, and a few nits from review
samliok Dec 2, 2025
fb5dfb0
simplify timeouthandler and remove the should deleteFunc
samliok Dec 2, 2025
5b34b96
dos check
samliok Dec 2, 2025
943dcd7
add tests to ensure we don't double increment
samliok Dec 2, 2025
519bef6
old todo
samliok Dec 2, 2025
c60bfc7
revert <= change
samliok Dec 2, 2025
f672a6c
update comment
samliok Dec 2, 2025
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
1 change: 0 additions & 1 deletion api.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ type Storage interface {
}

type Communication interface {

// Nodes returns all nodes that participate in the epoch.
Nodes() []NodeID

Expand Down
18 changes: 0 additions & 18 deletions block_scheduler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,6 @@ const (
defaultWaitDuration = 500 * time.Millisecond
)

func waitNoReceive(t *testing.T, ch <-chan struct{}) {
select {
case <-ch:
t.Fatal("channel unexpectedly signaled")
case <-time.After(defaultWaitDuration):
// good
}
}

func waitReceive(t *testing.T, ch <-chan struct{}) {
select {
case <-ch:
// good
case <-time.After(defaultWaitDuration):
t.Fatal("timed out waiting for signal")
}
}

func TestBlockVerificationScheduler(t *testing.T) {
t.Run("Schedules immediately when no dependencies", func(t *testing.T) {
scheduler := simplex.NewScheduler(testutil.MakeLogger(t), defaultMaxDeps)
Expand Down
435 changes: 294 additions & 141 deletions epoch.go

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions epoch_multinode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package simplex_test

import (
"fmt"
"sync"
"sync/atomic"
"testing"
Expand Down Expand Up @@ -155,10 +156,6 @@ func TestSimplexMultiNodeBlacklist(t *testing.T) {
testutil.NewSimplexNode(t, nodes[2], net, testEpochConfig)
testutil.NewSimplexNode(t, nodes[3], net, testEpochConfig)

for _, n := range net.Instances[:3] {
n.Silence()
}

net.StartInstances()

// Advance to the fourth node's turn by building three blocks
Expand Down Expand Up @@ -353,6 +350,9 @@ func TestSplitVotes(t *testing.T) {
}
}

// allow outstanding messages to be dropped
time.Sleep(100 * time.Millisecond)

net.SetAllNodesMessageFilter(testutil.AllowAllMessages)

time2 := splitNode2.E.StartTime
Expand All @@ -374,7 +374,7 @@ func TestSplitVotes(t *testing.T) {
splitNode3.WAL.AssertNotarization(0)

for _, n := range net.Instances {
require.Equal(t, uint64(0), n.Storage.NumBlocks())
require.Equal(t, uint64(0), n.Storage.NumBlocks(), fmt.Sprintf("node %s should not have", n.E.ID))
require.Equal(t, uint64(1), n.E.Metadata().Round)
require.Equal(t, uint64(1), n.E.Metadata().Seq)
}
Expand Down
48 changes: 46 additions & 2 deletions epoch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1263,14 +1263,12 @@ func TestEpochVotesForEquivocatedVotes(t *testing.T) {
equivocatedBlock.Data = []byte{1, 2, 3}
equivocatedBlock.ComputeDigest()
testutil.InjectTestVote(t, e, equivocatedBlock, nodes[1])
eqbh := equivocatedBlock.BlockHeader()

// We should not have sent a notarization yet, since we have not received enough votes for the block we received from the leader
require.Never(t, func() bool {
select {
case msg := <-recordedMessages:
if msg.Notarization != nil {
fmt.Println(msg.Notarization.Vote.BlockHeader.Equals(&eqbh))
return true
}
default:
Expand Down Expand Up @@ -1301,6 +1299,52 @@ func TestEpochVotesForEquivocatedVotes(t *testing.T) {
}
}

// Ensures we don't double increment the round on persisting a notarization
func TestDoubleIncrementOnPersistNotarization(t *testing.T) {
// add an empty notarization, then a notarization for a previous round
bb := &testutil.TestBlockBuilder{Out: make(chan *testutil.TestBlock, 1)}
nodes := []NodeID{{1}, {2}, {3}, {4}}
conf, wal, _ := testutil.DefaultTestNodeEpochConfig(t, nodes[3], testutil.NewNoopComm(nodes), bb)
conf.ReplicationEnabled = true

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

require.NoError(t, e.Start())

advanceRoundFromEmpty(t, e)
require.Equal(t, uint64(1), e.Metadata().Round)

// create a notarization for round 0
md := ProtocolMetadata{
Epoch: 0,
Round: 0,
Seq: 0,
}
_, ok := bb.BuildBlock(context.Background(), md, emptyBlacklist)
require.True(t, ok)

block := <-bb.Out
notarization, err := testutil.NewNotarization(conf.Logger, conf.SignatureAggregator, block, nodes)
require.NoError(t, err)

err = e.HandleMessage(&Message{
ReplicationResponse: &ReplicationResponse{
Data: []QuorumRound{
{
Block: block,
Notarization: &notarization,
},
},
},
}, nodes[0])
require.NoError(t, err)

wal.AssertWALSize(2)
// ensure the round is still 1
require.Equal(t, uint64(1), e.Metadata().Round)
}

// ListnerComm is a comm that listens for incoming messages
// and sends them to the [in] channel
type listenerComm struct {
Expand Down
33 changes: 22 additions & 11 deletions msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,18 +223,22 @@ type QuorumCertificate interface {
}

type ReplicationRequest struct {
Seqs []uint64 // sequences we are requesting
LatestRound uint64 // latest round that we are aware of
Seqs []uint64 // sequences we are requesting
Rounds []uint64 // rounds we are requesting
LatestRound uint64 // latest round that we are aware of
LatestFinalizedSeq uint64 // latest finalized sequence that we are aware of
}

type ReplicationResponse struct {
Data []QuorumRound
LatestRound *QuorumRound
LatestSeq *QuorumRound
}

type VerifiedReplicationResponse struct {
Data []VerifiedQuorumRound
LatestRound *VerifiedQuorumRound
Data []VerifiedQuorumRound
LatestRound *VerifiedQuorumRound
LatestFinalizedSeq *VerifiedQuorumRound
}

// QuorumRound represents a round that has achieved quorum on either
Expand All @@ -250,13 +254,15 @@ type QuorumRound struct {
// (block, notarization) or (block, finalization) or
// (empty notarization)
func (q *QuorumRound) IsWellFormed() error {
if q.EmptyNotarization != nil && q.Block == nil {
return nil
} else if q.Block != nil && (q.Notarization != nil || q.Finalization != nil) {
return nil
if q.Block == nil && q.EmptyNotarization == nil {
return fmt.Errorf("malformed QuorumRound, empty block and notarization fields")
}

if q.Block != nil && (q.Notarization == nil && q.Finalization == nil) {
return fmt.Errorf("malformed QuorumRound, block but no notarization or finalization")
}

return fmt.Errorf("malformed QuorumRound")
return nil
}

func (q *QuorumRound) GetRound() uint64 {
Expand Down Expand Up @@ -284,10 +290,15 @@ func (q *QuorumRound) VerifyQCConsistentWithBlock() error {
return err
}

if q.EmptyNotarization != nil {
if q.Block == nil {
return nil
}

// if an empty notarization is included, ensure the round is equal to the block round
if q.EmptyNotarization != nil && q.EmptyNotarization.Vote.Round != q.Block.BlockHeader().Round {
return fmt.Errorf("empty round does not match block round")
}

// ensure the finalization or notarization we get relates to the block
blockDigest := q.Block.BlockHeader().Digest

Expand All @@ -314,7 +325,7 @@ func (q *QuorumRound) String() string {
if err != nil {
return fmt.Sprintf("QuorumRound{Error: %s}", err)
} else {
return fmt.Sprintf("QuorumRound{Round: %d, Seq: %d}", q.GetRound(), q.GetSequence())
return fmt.Sprintf("QuorumRound{Round: %d, Seq: %d, Finalized: %t}", q.GetRound(), q.GetSequence(), q.Finalization != nil)
}
}

Expand Down
18 changes: 18 additions & 0 deletions msg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,24 @@ func TestQuorumRoundMalformed(t *testing.T) {
},
expectedErr: true,
},
{
name: "block and notarization and empty notarization",
qr: simplex.QuorumRound{
Block: &testutil.TestBlock{},
Notarization: &simplex.Notarization{},
EmptyNotarization: &simplex.EmptyNotarization{},
},
expectedErr: false,
},
{
name: "block and finalization and empty notarization",
qr: simplex.QuorumRound{
Block: &testutil.TestBlock{},
Finalization: &simplex.Finalization{},
EmptyNotarization: &simplex.EmptyNotarization{},
},
expectedErr: false,
},
}

for _, test := range tests {
Expand Down
8 changes: 3 additions & 5 deletions pos_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ package simplex_test

import (
"bytes"
"fmt"
"testing"
"time"

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

func TestPoS(t *testing.T) {
Expand Down Expand Up @@ -133,8 +133,6 @@ func TestPoS(t *testing.T) {
testutil.WaitToEnterRound(t, n.E, 15)
}

fmt.Println(simplex.LeaderForRound(nodes, 15))

// Now, disconnect the node with the highest stake (node 3) and observe the network is stuck
net.Disconnect(nodes[2])
net.TriggerLeaderBlockBuilder(15)
Expand Down
Loading