From e5f76fa9a9e8b83f6529b9ffc9b220d367c954a5 Mon Sep 17 00:00:00 2001 From: jadalsmail Date: Thu, 2 Jul 2026 16:55:46 -0400 Subject: [PATCH 1/5] fix replication requests exceeding maxRoundWindow and splitting resendRequests --- simplex/replication_test.go | 47 ++++++++++++++++++++ simplex/replication_timeout_test.go | 67 +++++++++++++++++++++++++++++ simplex/requestor.go | 12 +++++- 3 files changed, 124 insertions(+), 2 deletions(-) diff --git a/simplex/replication_test.go b/simplex/replication_test.go index 3c155510..e59dd8ae 100644 --- a/simplex/replication_test.go +++ b/simplex/replication_test.go @@ -1760,3 +1760,50 @@ messages: notarization := wal.AssertNotarization(uint64(len(blocks))) require.Equal(t, common.EmptyNotarizationRecordType, notarization) } + +// TestReplicationRequestsWithinMaxRoundWindow ensures that a node that observes a finalization more than MaxRoundWindow +// sequences ahead of its next sequence to commit only requests sequences up to MaxRoundWindow-1 ahead of it +func TestReplicationRequestsWithinMaxRoundWindow(t *testing.T) { + bb := testutil.NewTestBlockBuilder() + nodes := []common.NodeID{{1}, {2}, {3}, {4}} + sentMessages := make(chan *common.Message, 100) + conf, _, _ := testutil.DefaultTestNodeEpochConfig(t, nodes[1], &recordingComm{ + Communication: testutil.NewNoopComm(nodes), + SentMessages: sentMessages, + }, bb) + conf.ReplicationEnabled = true + + e, err := simplex.NewEpoch(conf) + require.NoError(t, err) + t.Cleanup(e.Stop) + require.NoError(t, e.Start()) + + // observe a finalization more than MaxRoundWindow sequences ahead of us + seqCount := 2 * conf.MaxRoundWindow + finalization := createBlocks(t, nodes, seqCount)[seqCount-1].Finalization + err = e.HandleMessage(&common.Message{Finalization: &finalization}, nodes[0]) + require.NoError(t, err) + + // collect the replication requests sent out in response to the finalization + requested := make(map[uint64]struct{}) + timeout := time.After(30 * time.Second) + for uint64(len(requested)) < conf.MaxRoundWindow { + select { + case msg := <-sentMessages: + if msg.ReplicationRequest == nil { + continue + } + for _, seq := range msg.ReplicationRequest.Seqs { + requested[seq] = struct{}{} + } + case <-timeout: + require.FailNow(t, "timed out waiting for replication requests") + } + } + + // our next sequence to commit is 0, so we may request at most sequences [0, MaxRoundWindow-1] + for seq := range requested { + require.Less(t, seq, conf.MaxRoundWindow, + "requested a sequence more than MaxRoundWindow ahead of the next sequence to commit") + } +} diff --git a/simplex/replication_timeout_test.go b/simplex/replication_timeout_test.go index 9b741d8f..60fb76fa 100644 --- a/simplex/replication_timeout_test.go +++ b/simplex/replication_timeout_test.go @@ -677,3 +677,70 @@ func TestReplicationResendsFinalizedBlocksThatFailedVerification(t *testing.T) { require.Equal(t, uint64(1), storage.NumBlocks()) require.Equal(t, block, storedBlock) } + +// TestReplicationResendSplitsRequests ensures that when replication requests time out, the missing sequences +// are re-requested split across the nodes that signed the highest observed quorum, just like the initial requests are. +func TestReplicationResendSplitsRequests(t *testing.T) { + bb := testutil.NewTestBlockBuilder() + nodes := []common.NodeID{{1}, {2}, {3}, {4}} + sentMessages := make(chan *common.Message, 100) + conf, _, _ := testutil.DefaultTestNodeEpochConfig(t, nodes[1], &recordingComm{ + Communication: testutil.NewNoopComm(nodes), + SentMessages: sentMessages, + }, bb) + conf.ReplicationEnabled = true + + e, err := simplex.NewEpoch(conf) + require.NoError(t, err) + t.Cleanup(e.Stop) + require.NoError(t, e.Start()) + + // observe a finalization more than MaxRoundWindow sequences ahead of us + seqCount := 2 * conf.MaxRoundWindow + finalization := createBlocks(t, nodes, seqCount)[seqCount-1].Finalization + err = e.HandleMessage(&common.Message{Finalization: &finalization}, nodes[0]) + require.NoError(t, err) + + // collect the initial replication requests sent in response to the finalization + initiallyRequested := make(map[uint64]struct{}) + timeout := time.After(30 * time.Second) + for uint64(len(initiallyRequested)) < conf.MaxRoundWindow { + select { + case msg := <-sentMessages: + if msg.ReplicationRequest == nil { + continue + } + for _, seq := range msg.ReplicationRequest.Seqs { + initiallyRequested[seq] = struct{}{} + } + case <-timeout: + require.FailNow(t, "timed out waiting for replication requests") + } + } + + // no node responds, so the requests time out and are re-sent + e.AdvanceTime(e.StartTime.Add(2 * simplex.DefaultReplicationRequestTimeout)) + + // collect the re-sent requests until they cover all outstanding sequences + resentRequests := 0 + resentSeqs := make(map[uint64]struct{}) + for len(resentSeqs) < len(initiallyRequested) { + select { + case msg := <-sentMessages: + if msg.ReplicationRequest == nil { + continue + } + resentRequests++ + require.LessOrEqual(t, uint64(len(msg.ReplicationRequest.Seqs)), conf.MaxRoundWindow, + "a replication request with more than MaxRoundWindow seqs is dropped by the responder") + for _, seq := range msg.ReplicationRequest.Seqs { + resentSeqs[seq] = struct{}{} + } + case <-timeout: + require.FailNow(t, "timed out waiting for re-sent replication requests") + } + } + + require.Greater(t, resentRequests, 1, + "timed out requests should be re-sent split across the signers of the highest observed quorum") +} diff --git a/simplex/requestor.go b/simplex/requestor.go index ac1f5d70..6241b9a3 100644 --- a/simplex/requestor.go +++ b/simplex/requestor.go @@ -128,7 +128,15 @@ func (r *requestor) resendReplicationRequests(missingIds []uint64) { r.epochLock.Lock() defer r.epochLock.Unlock() - segments := CompressSequences(missingIds) + numNodes := len(r.highestObserved.signers) + + // split each contiguous range among the nodes that signed, + // just like the initial requests, so that a single request never exceeds + // the maxRoundWindow limit enforced by the responding nodes + var segments []Segment + for _, segment := range CompressSequences(missingIds) { + segments = append(segments, DistributeSequenceRequests(segment.Start, segment.End, numNodes)...) + } r.sendSegments(segments) @@ -159,7 +167,7 @@ func (r *requestor) observedSignedQuorum(observed *signedQuorum, currentSeqOrRou func (r *requestor) sendMoreReplicationRequests(observedSeqOrRound, currentSeqOrRound uint64) { start := math.Max(float64(currentSeqOrRound), float64(r.highestRequested)) // we limit the number of outstanding requests to be at most maxRoundWindow ahead of nextSeqToCommit - end := math.Min(float64(observedSeqOrRound), float64(r.maxRoundWindow+currentSeqOrRound)) + end := math.Min(float64(observedSeqOrRound), float64(r.maxRoundWindow+currentSeqOrRound - 1)) r.logger.Debug("Node is behind, attempting to request missing values", zap.Uint64("value", observedSeqOrRound), zap.Uint64("start", uint64(start)), zap.Uint64("end", uint64(end)), zap.Bool("seq requestor", r.replicateSeqs)) r.sendReplicationRequests(uint64(start), uint64(end)) From 459ae9fa2e21c845db4accec5d351205f9a01210 Mon Sep 17 00:00:00 2001 From: jadalsmail Date: Thu, 2 Jul 2026 17:05:50 -0400 Subject: [PATCH 2/5] go format fixed --- simplex/replication_test.go | 2 +- simplex/replication_timeout_test.go | 2 +- simplex/requestor.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/simplex/replication_test.go b/simplex/replication_test.go index e59dd8ae..0a700cf9 100644 --- a/simplex/replication_test.go +++ b/simplex/replication_test.go @@ -1761,7 +1761,7 @@ messages: require.Equal(t, common.EmptyNotarizationRecordType, notarization) } -// TestReplicationRequestsWithinMaxRoundWindow ensures that a node that observes a finalization more than MaxRoundWindow +// TestReplicationRequestsWithinMaxRoundWindow ensures that a node that observes a finalization more than MaxRoundWindow // sequences ahead of its next sequence to commit only requests sequences up to MaxRoundWindow-1 ahead of it func TestReplicationRequestsWithinMaxRoundWindow(t *testing.T) { bb := testutil.NewTestBlockBuilder() diff --git a/simplex/replication_timeout_test.go b/simplex/replication_timeout_test.go index 60fb76fa..0c2f619c 100644 --- a/simplex/replication_timeout_test.go +++ b/simplex/replication_timeout_test.go @@ -678,7 +678,7 @@ func TestReplicationResendsFinalizedBlocksThatFailedVerification(t *testing.T) { require.Equal(t, block, storedBlock) } -// TestReplicationResendSplitsRequests ensures that when replication requests time out, the missing sequences +// TestReplicationResendSplitsRequests ensures that when replication requests time out, the missing sequences // are re-requested split across the nodes that signed the highest observed quorum, just like the initial requests are. func TestReplicationResendSplitsRequests(t *testing.T) { bb := testutil.NewTestBlockBuilder() diff --git a/simplex/requestor.go b/simplex/requestor.go index 6241b9a3..806db29d 100644 --- a/simplex/requestor.go +++ b/simplex/requestor.go @@ -167,7 +167,7 @@ func (r *requestor) observedSignedQuorum(observed *signedQuorum, currentSeqOrRou func (r *requestor) sendMoreReplicationRequests(observedSeqOrRound, currentSeqOrRound uint64) { start := math.Max(float64(currentSeqOrRound), float64(r.highestRequested)) // we limit the number of outstanding requests to be at most maxRoundWindow ahead of nextSeqToCommit - end := math.Min(float64(observedSeqOrRound), float64(r.maxRoundWindow+currentSeqOrRound - 1)) + end := math.Min(float64(observedSeqOrRound), float64(r.maxRoundWindow+currentSeqOrRound-1)) r.logger.Debug("Node is behind, attempting to request missing values", zap.Uint64("value", observedSeqOrRound), zap.Uint64("start", uint64(start)), zap.Uint64("end", uint64(end)), zap.Bool("seq requestor", r.replicateSeqs)) r.sendReplicationRequests(uint64(start), uint64(end)) From a70872da176f8b08a4641a4c75b8deb40c5b8ec4 Mon Sep 17 00:00:00 2001 From: jadalsmail Date: Mon, 6 Jul 2026 17:34:02 -0400 Subject: [PATCH 3/5] extract DistributeMissingSequences and enfoce maxRoundWindow on retried requests --- .gitignore | 2 +- simplex/replication_test.go | 49 +--------------- simplex/requestor.go | 8 +-- simplex/util.go | 17 ++++++ simplex/util_test.go | 109 ++++++++++++++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 55 deletions(-) diff --git a/.gitignore b/.gitignore index 55892921..81837034 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,4 @@ go.work.sum .idea/ # tmp folder -/tmp \ No newline at end of file +/tmptmp/ diff --git a/simplex/replication_test.go b/simplex/replication_test.go index 0a700cf9..a25851cb 100644 --- a/simplex/replication_test.go +++ b/simplex/replication_test.go @@ -1759,51 +1759,4 @@ messages: require.True(t, foundEmptyVote, "Node should send an empty vote after timeout following replication") notarization := wal.AssertNotarization(uint64(len(blocks))) require.Equal(t, common.EmptyNotarizationRecordType, notarization) -} - -// TestReplicationRequestsWithinMaxRoundWindow ensures that a node that observes a finalization more than MaxRoundWindow -// sequences ahead of its next sequence to commit only requests sequences up to MaxRoundWindow-1 ahead of it -func TestReplicationRequestsWithinMaxRoundWindow(t *testing.T) { - bb := testutil.NewTestBlockBuilder() - nodes := []common.NodeID{{1}, {2}, {3}, {4}} - sentMessages := make(chan *common.Message, 100) - conf, _, _ := testutil.DefaultTestNodeEpochConfig(t, nodes[1], &recordingComm{ - Communication: testutil.NewNoopComm(nodes), - SentMessages: sentMessages, - }, bb) - conf.ReplicationEnabled = true - - e, err := simplex.NewEpoch(conf) - require.NoError(t, err) - t.Cleanup(e.Stop) - require.NoError(t, e.Start()) - - // observe a finalization more than MaxRoundWindow sequences ahead of us - seqCount := 2 * conf.MaxRoundWindow - finalization := createBlocks(t, nodes, seqCount)[seqCount-1].Finalization - err = e.HandleMessage(&common.Message{Finalization: &finalization}, nodes[0]) - require.NoError(t, err) - - // collect the replication requests sent out in response to the finalization - requested := make(map[uint64]struct{}) - timeout := time.After(30 * time.Second) - for uint64(len(requested)) < conf.MaxRoundWindow { - select { - case msg := <-sentMessages: - if msg.ReplicationRequest == nil { - continue - } - for _, seq := range msg.ReplicationRequest.Seqs { - requested[seq] = struct{}{} - } - case <-timeout: - require.FailNow(t, "timed out waiting for replication requests") - } - } - - // our next sequence to commit is 0, so we may request at most sequences [0, MaxRoundWindow-1] - for seq := range requested { - require.Less(t, seq, conf.MaxRoundWindow, - "requested a sequence more than MaxRoundWindow ahead of the next sequence to commit") - } -} +} \ No newline at end of file diff --git a/simplex/requestor.go b/simplex/requestor.go index 806db29d..ebbe5148 100644 --- a/simplex/requestor.go +++ b/simplex/requestor.go @@ -128,15 +128,11 @@ func (r *requestor) resendReplicationRequests(missingIds []uint64) { r.epochLock.Lock() defer r.epochLock.Unlock() - numNodes := len(r.highestObserved.signers) // split each contiguous range among the nodes that signed, // just like the initial requests, so that a single request never exceeds // the maxRoundWindow limit enforced by the responding nodes - var segments []Segment - for _, segment := range CompressSequences(missingIds) { - segments = append(segments, DistributeSequenceRequests(segment.Start, segment.End, numNodes)...) - } + segments := DistributeMissingSequences(missingIds, len(r.highestObserved.signers), r.maxRoundWindow) r.sendSegments(segments) @@ -167,7 +163,7 @@ func (r *requestor) observedSignedQuorum(observed *signedQuorum, currentSeqOrRou func (r *requestor) sendMoreReplicationRequests(observedSeqOrRound, currentSeqOrRound uint64) { start := math.Max(float64(currentSeqOrRound), float64(r.highestRequested)) // we limit the number of outstanding requests to be at most maxRoundWindow ahead of nextSeqToCommit - end := math.Min(float64(observedSeqOrRound), float64(r.maxRoundWindow+currentSeqOrRound-1)) + end := math.Min(float64(observedSeqOrRound), float64(r.maxRoundWindow+currentSeqOrRound -1)) r.logger.Debug("Node is behind, attempting to request missing values", zap.Uint64("value", observedSeqOrRound), zap.Uint64("start", uint64(start)), zap.Uint64("end", uint64(end)), zap.Bool("seq requestor", r.replicateSeqs)) r.sendReplicationRequests(uint64(start), uint64(end)) diff --git a/simplex/util.go b/simplex/util.go index e59d96cd..572ed2d9 100644 --- a/simplex/util.go +++ b/simplex/util.go @@ -262,6 +262,23 @@ func DistributeSequenceRequests(start, end uint64, numNodes int) []Segment { return segments } + +func DistributeMissingSequences(missingSeqs []uint64, numNodes int, maxSize uint64) []Segment { + var segments []Segment + if maxSize == 0 { + return nil + } + for _, segment := range CompressSequences(missingSeqs) { + for _, distributed := range DistributeSequenceRequests(segment.Start, segment.End, numNodes) { + for start := distributed.Start; start <= distributed.End; start+= maxSize { + segments = append(segments, Segment {Start: start, + End: min (start + maxSize - 1, distributed.End)}) + } + } + } + return segments +} + type NotarizationTime struct { // config getRound func() uint64 diff --git a/simplex/util_test.go b/simplex/util_test.go index 4513fc78..26907474 100644 --- a/simplex/util_test.go +++ b/simplex/util_test.go @@ -378,6 +378,115 @@ func TestDistributeSequenceRequests(t *testing.T) { } } +func TestDistributeMissingSequences(t *testing.T) { + tests := []struct { + name string + missingSeqs []uint64 + numNodes int + maxSize uint64 + expected []Segment + }{ { + name: "empty input", + missingSeqs: []uint64{}, + numNodes: 3, + maxSize: 10, + expected: nil, + }, + { + name: "single missing sequence", + missingSeqs: []uint64{5}, + numNodes: 3, + maxSize: 10, + expected: []Segment{{Start: 5, End: 5}}, + }, + { + name: "contiguous range split among nodes", + missingSeqs: []uint64{5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + numNodes: 3, + maxSize: 10, + expected: []Segment{ + {Start: 5, End: 8}, + {Start: 9, End: 12}, + {Start: 13, End: 15}, + }, + }, + { + name: "unsorted input with gaps", + missingSeqs: []uint64{9,7,3,0,1,2,8}, + numNodes: 2, + maxSize: 10, + expected: []Segment{ + {Start: 0, End: 1}, + {Start: 2, End: 3}, + {Start: 7, End: 8}, + {Start: 9, End: 9}, + }, + }, + { + name: "single node segments capped at maxSize", + missingSeqs: []uint64{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, + 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + }, + numNodes: 1, + maxSize: 10, + expected: []Segment{ + {Start: 0, End: 9}, + {Start: 10, End: 19}, + {Start: 20, End: 24}, + }, + }, + { + name: "zero max size", + missingSeqs: []uint64{1,2,3}, + numNodes: 2, + maxSize: 0, + expected: nil, + }, + { + name: "zero nodes", + missingSeqs: []uint64{1,2,3}, + numNodes: 0, + maxSize: 10, + expected: nil, + }, + { + name: "range exceeds maxSize but node shares are within it", + missingSeqs: []uint64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}, + numNodes: 2, + maxSize: 10, + expected: []Segment{ + {Start: 0, End: 7}, + {Start: 8, End: 14}, + }, + }, + { + name: "node shares still exceed maxSize and are chunked", + missingSeqs: []uint64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}, + numNodes: 3, + maxSize: 3, + expected: []Segment{ + {Start: 0, End: 2}, + {Start: 3, End: 4}, + {Start: 5, End: 7}, + {Start: 8, End: 9}, + {Start: 10, End: 12}, + {Start: 13, End: 14}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := DistributeMissingSequences(tt.missingSeqs, tt.numNodes, tt.maxSize) + require.Equal(t, tt.expected, result) + }) + } + +} + + + func TestNotarizationTime(t *testing.T) { defaultFinalizeVoteRebroadcastTimeout := time.Second * 6 From e55a195724cefb95284486f178db67a29f71be9d Mon Sep 17 00:00:00 2001 From: jadalsmail Date: Mon, 6 Jul 2026 17:57:29 -0400 Subject: [PATCH 4/5] fix .gitignore rule and formatting --- .gitignore | 2 +- simplex/replication_test.go | 2 +- simplex/requestor.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 81837034..ccec156b 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,4 @@ go.work.sum .idea/ # tmp folder -/tmptmp/ +/tmp diff --git a/simplex/replication_test.go b/simplex/replication_test.go index a25851cb..3c155510 100644 --- a/simplex/replication_test.go +++ b/simplex/replication_test.go @@ -1759,4 +1759,4 @@ messages: require.True(t, foundEmptyVote, "Node should send an empty vote after timeout following replication") notarization := wal.AssertNotarization(uint64(len(blocks))) require.Equal(t, common.EmptyNotarizationRecordType, notarization) -} \ No newline at end of file +} diff --git a/simplex/requestor.go b/simplex/requestor.go index ebbe5148..eab493e6 100644 --- a/simplex/requestor.go +++ b/simplex/requestor.go @@ -163,7 +163,7 @@ func (r *requestor) observedSignedQuorum(observed *signedQuorum, currentSeqOrRou func (r *requestor) sendMoreReplicationRequests(observedSeqOrRound, currentSeqOrRound uint64) { start := math.Max(float64(currentSeqOrRound), float64(r.highestRequested)) // we limit the number of outstanding requests to be at most maxRoundWindow ahead of nextSeqToCommit - end := math.Min(float64(observedSeqOrRound), float64(r.maxRoundWindow+currentSeqOrRound -1)) + end := math.Min(float64(observedSeqOrRound), float64(r.maxRoundWindow+currentSeqOrRound-1)) r.logger.Debug("Node is behind, attempting to request missing values", zap.Uint64("value", observedSeqOrRound), zap.Uint64("start", uint64(start)), zap.Uint64("end", uint64(end)), zap.Bool("seq requestor", r.replicateSeqs)) r.sendReplicationRequests(uint64(start), uint64(end)) From d913659d082ae5d73c2d9c193e4486e434f2e838 Mon Sep 17 00:00:00 2001 From: jadalsmail Date: Mon, 6 Jul 2026 18:03:28 -0400 Subject: [PATCH 5/5] format --- simplex/requestor.go | 1 - simplex/util.go | 7 +- simplex/util_test.go | 178 +++++++++++++++++++++---------------------- 3 files changed, 91 insertions(+), 95 deletions(-) diff --git a/simplex/requestor.go b/simplex/requestor.go index eab493e6..907e2df5 100644 --- a/simplex/requestor.go +++ b/simplex/requestor.go @@ -128,7 +128,6 @@ func (r *requestor) resendReplicationRequests(missingIds []uint64) { r.epochLock.Lock() defer r.epochLock.Unlock() - // split each contiguous range among the nodes that signed, // just like the initial requests, so that a single request never exceeds // the maxRoundWindow limit enforced by the responding nodes diff --git a/simplex/util.go b/simplex/util.go index 572ed2d9..4658dad0 100644 --- a/simplex/util.go +++ b/simplex/util.go @@ -262,7 +262,6 @@ func DistributeSequenceRequests(start, end uint64, numNodes int) []Segment { return segments } - func DistributeMissingSequences(missingSeqs []uint64, numNodes int, maxSize uint64) []Segment { var segments []Segment if maxSize == 0 { @@ -270,9 +269,9 @@ func DistributeMissingSequences(missingSeqs []uint64, numNodes int, maxSize uint } for _, segment := range CompressSequences(missingSeqs) { for _, distributed := range DistributeSequenceRequests(segment.Start, segment.End, numNodes) { - for start := distributed.Start; start <= distributed.End; start+= maxSize { - segments = append(segments, Segment {Start: start, - End: min (start + maxSize - 1, distributed.End)}) + for start := distributed.Start; start <= distributed.End; start += maxSize { + segments = append(segments, Segment{Start: start, + End: min(start+maxSize-1, distributed.End)}) } } } diff --git a/simplex/util_test.go b/simplex/util_test.go index 26907474..00cc41f6 100644 --- a/simplex/util_test.go +++ b/simplex/util_test.go @@ -380,100 +380,100 @@ func TestDistributeSequenceRequests(t *testing.T) { func TestDistributeMissingSequences(t *testing.T) { tests := []struct { - name string + name string missingSeqs []uint64 - numNodes int - maxSize uint64 - expected []Segment - }{ { - name: "empty input", - missingSeqs: []uint64{}, - numNodes: 3, - maxSize: 10, - expected: nil, - }, - { - name: "single missing sequence", - missingSeqs: []uint64{5}, - numNodes: 3, - maxSize: 10, - expected: []Segment{{Start: 5, End: 5}}, - }, - { - name: "contiguous range split among nodes", - missingSeqs: []uint64{5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, - numNodes: 3, - maxSize: 10, - expected: []Segment{ - {Start: 5, End: 8}, - {Start: 9, End: 12}, - {Start: 13, End: 15}, - }, - }, - { - name: "unsorted input with gaps", - missingSeqs: []uint64{9,7,3,0,1,2,8}, - numNodes: 2, - maxSize: 10, - expected: []Segment{ - {Start: 0, End: 1}, - {Start: 2, End: 3}, - {Start: 7, End: 8}, - {Start: 9, End: 9}, - }, - }, - { - name: "single node segments capped at maxSize", - missingSeqs: []uint64{ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, - 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, - }, - numNodes: 1, - maxSize: 10, - expected: []Segment{ - {Start: 0, End: 9}, - {Start: 10, End: 19}, - {Start: 20, End: 24}, - }, - }, - { - name: "zero max size", - missingSeqs: []uint64{1,2,3}, - numNodes: 2, - maxSize: 0, - expected: nil, - }, - { - name: "zero nodes", - missingSeqs: []uint64{1,2,3}, - numNodes: 0, - maxSize: 10, - expected: nil, - }, - { - name: "range exceeds maxSize but node shares are within it", - missingSeqs: []uint64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}, - numNodes: 2, - maxSize: 10, - expected: []Segment{ + numNodes int + maxSize uint64 + expected []Segment + }{{ + name: "empty input", + missingSeqs: []uint64{}, + numNodes: 3, + maxSize: 10, + expected: nil, + }, + { + name: "single missing sequence", + missingSeqs: []uint64{5}, + numNodes: 3, + maxSize: 10, + expected: []Segment{{Start: 5, End: 5}}, + }, + { + name: "contiguous range split among nodes", + missingSeqs: []uint64{5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + numNodes: 3, + maxSize: 10, + expected: []Segment{ + {Start: 5, End: 8}, + {Start: 9, End: 12}, + {Start: 13, End: 15}, + }, + }, + { + name: "unsorted input with gaps", + missingSeqs: []uint64{9, 7, 3, 0, 1, 2, 8}, + numNodes: 2, + maxSize: 10, + expected: []Segment{ + {Start: 0, End: 1}, + {Start: 2, End: 3}, + {Start: 7, End: 8}, + {Start: 9, End: 9}, + }, + }, + { + name: "single node segments capped at maxSize", + missingSeqs: []uint64{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, + 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + }, + numNodes: 1, + maxSize: 10, + expected: []Segment{ + {Start: 0, End: 9}, + {Start: 10, End: 19}, + {Start: 20, End: 24}, + }, + }, + { + name: "zero max size", + missingSeqs: []uint64{1, 2, 3}, + numNodes: 2, + maxSize: 0, + expected: nil, + }, + { + name: "zero nodes", + missingSeqs: []uint64{1, 2, 3}, + numNodes: 0, + maxSize: 10, + expected: nil, + }, + { + name: "range exceeds maxSize but node shares are within it", + missingSeqs: []uint64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}, + numNodes: 2, + maxSize: 10, + expected: []Segment{ {Start: 0, End: 7}, {Start: 8, End: 14}, - }, }, + }, { - name: "node shares still exceed maxSize and are chunked", - missingSeqs: []uint64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}, - numNodes: 3, - maxSize: 3, - expected: []Segment{ - {Start: 0, End: 2}, - {Start: 3, End: 4}, - {Start: 5, End: 7}, - {Start: 8, End: 9}, - {Start: 10, End: 12}, - {Start: 13, End: 14}, - }, + name: "node shares still exceed maxSize and are chunked", + missingSeqs: []uint64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}, + numNodes: 3, + maxSize: 3, + expected: []Segment{ + {Start: 0, End: 2}, + {Start: 3, End: 4}, + {Start: 5, End: 7}, + {Start: 8, End: 9}, + {Start: 10, End: 12}, + {Start: 13, End: 14}, }, + }, } for _, tt := range tests { @@ -485,8 +485,6 @@ func TestDistributeMissingSequences(t *testing.T) { } - - func TestNotarizationTime(t *testing.T) { defaultFinalizeVoteRebroadcastTimeout := time.Second * 6