Skip to content

Commit 8a99cd4

Browse files
authored
Merge pull request #7893 from multiversx/round-aware-signatures-tracker
Round aware signatures tracker
2 parents 05bbb23 + b945171 commit 8a99cd4

7 files changed

Lines changed: 267 additions & 104 deletions

File tree

consensus/spos/bls/v2/subroundSignature.go

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ func (sr *subroundSignature) sendSignatureForManagedKey(ctx context.Context, idx
348348

349349
// Record the signed nonce before broadcast so competing block detection works
350350
// even if the broadcast itself fails
351-
sr.sentSignatureTracker.RecordSignedNonce(pkBytes, nonce, currentHash)
351+
sr.sentSignatureTracker.RecordSignedNonce(pkBytes, nonce, currentHash, sr.RoundHandler().Index())
352352

353353
// with the equivalent messages feature on, signatures from all managed keys must be broadcast, as the aggregation is done by any participant
354354
ok := sr.createAndSendSignatureMessage(signatureShare, pkBytes)
@@ -390,7 +390,7 @@ func (sr *subroundSignature) doSignatureJobForSingleKey(ctx context.Context) boo
390390

391391
// Record the signed nonce before broadcast so competing block detection works
392392
// even if the broadcast itself fails
393-
sr.sentSignatureTracker.RecordSignedNonce(pkBytes, nonce, currentHash)
393+
sr.sentSignatureTracker.RecordSignedNonce(pkBytes, nonce, currentHash, sr.RoundHandler().Index())
394394

395395
// leader also sends his signature here
396396
ok := sr.createAndSendSignatureMessage(signatureShare, pkBytes)
@@ -402,10 +402,12 @@ func (sr *subroundSignature) doSignatureJobForSingleKey(ctx context.Context) boo
402402
}
403403

404404
func (sr *subroundSignature) getPkForCompetingBlock(nonce uint64, currentHash []byte) []byte {
405+
currentRound := sr.RoundHandler().Index()
406+
405407
// check self key
406408
selfPk := []byte(sr.SelfPubKey())
407-
previousHash, exists := sr.sentSignatureTracker.GetSignedHash(selfPk, nonce)
408-
if exists && !bytes.Equal(previousHash, currentHash) {
409+
previousHash, round, exists := sr.sentSignatureTracker.GetSignedNonceInfo(selfPk, nonce)
410+
if exists && !bytes.Equal(previousHash, currentHash) && round >= currentRound-1 {
409411
return selfPk
410412
}
411413

@@ -416,19 +418,20 @@ func (sr *subroundSignature) getPkForCompetingBlock(nonce uint64, currentHash []
416418
continue
417419
}
418420

419-
previousHash, exists := sr.sentSignatureTracker.GetSignedHash(pkBytes, nonce)
420-
if exists && !bytes.Equal(previousHash, currentHash) {
421+
previousHash, round, exists := sr.sentSignatureTracker.GetSignedNonceInfo(pkBytes, nonce)
422+
if exists && !bytes.Equal(previousHash, currentHash) && round >= currentRound-1 {
421423
return pkBytes
422424
}
423425
}
424426

425427
return nil
426428
}
427429

428-
// waitIfCompetingBlock waits if this node already signed a different block for the same nonce.
429-
// The delay is measured from round start. Returns true if signing should be aborted.
430+
// waitIfCompetingBlock waits if this node already signed a different block for the same nonce
431+
// in the current or previous round. The delay is measured from round start.
432+
// Returns true if signing should be aborted.
430433
func (sr *subroundSignature) waitIfCompetingBlock(ctx context.Context, pkBytes []byte, nonce uint64, currentHash []byte) bool {
431-
previousHash, exists := sr.sentSignatureTracker.GetSignedHash(pkBytes, nonce)
434+
previousHash, round, exists := sr.sentSignatureTracker.GetSignedNonceInfo(pkBytes, nonce)
432435
if !exists {
433436
return false
434437
}
@@ -437,6 +440,15 @@ func (sr *subroundSignature) waitIfCompetingBlock(ctx context.Context, pkBytes [
437440
return false
438441
}
439442

443+
currentRound := sr.RoundHandler().Index()
444+
if round < currentRound-1 {
445+
log.Debug("waitIfCompetingBlock: signed hash is from an older round, signing immediately",
446+
"nonce", nonce,
447+
"signedRound", round,
448+
"currentRound", currentRound)
449+
return false
450+
}
451+
440452
// Delay is measured from round start, not from when this function is called
441453
roundStart := sr.GetRoundTimeStamp()
442454
targetTime := time.Duration(float64(sr.RoundHandler().TimeDuration()) * competingBlockSignDelay)
@@ -452,7 +464,7 @@ func (sr *subroundSignature) waitIfCompetingBlock(ctx context.Context, pkBytes [
452464
safetyMargin := 10 * time.Millisecond
453465
maxDelay := remaining - safetyMargin
454466
if maxDelay <= 0 {
455-
log.Debug("waitIfCompetingBlock: no time remaining in signature subround, proceeding to sign")
467+
log.Debug("waitIfCompetingBlock: no time remaining before signature send deadline, proceeding to sign")
456468
return false
457469
}
458470
if delay > maxDelay {

consensus/spos/bls/v2/subroundSignatureCompetingBlock_test.go

Lines changed: 133 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ func TestWaitIfCompetingBlock_NoPreviousHashExists(t *testing.T) {
7878

7979
sr := createSubroundSignatureForCompetingBlockTests(
8080
&testscommon.SentSignatureTrackerStub{
81-
GetSignedHashCalled: func(pkBytes []byte, nonce uint64) ([]byte, bool) {
82-
return nil, false
81+
GetSignedNonceInfoCalled: func(pkBytes []byte, nonce uint64) ([]byte, int64, bool) {
82+
return nil, 0, false
8383
},
8484
},
8585
nil,
@@ -96,8 +96,8 @@ func TestWaitIfCompetingBlock_PreviousHashEqualsCurrent(t *testing.T) {
9696
currentHash := []byte("same_hash")
9797
sr := createSubroundSignatureForCompetingBlockTests(
9898
&testscommon.SentSignatureTrackerStub{
99-
GetSignedHashCalled: func(pkBytes []byte, nonce uint64) ([]byte, bool) {
100-
return currentHash, true
99+
GetSignedNonceInfoCalled: func(pkBytes []byte, nonce uint64) ([]byte, int64, bool) {
100+
return currentHash, 0, true
101101
},
102102
},
103103
nil,
@@ -113,8 +113,8 @@ func TestWaitIfCompetingBlock_AlreadyPastDelayDeadline(t *testing.T) {
113113

114114
sr := createSubroundSignatureForCompetingBlockTests(
115115
&testscommon.SentSignatureTrackerStub{
116-
GetSignedHashCalled: func(pkBytes []byte, nonce uint64) ([]byte, bool) {
117-
return []byte("previous_hash"), true
116+
GetSignedNonceInfoCalled: func(pkBytes []byte, nonce uint64) ([]byte, int64, bool) {
117+
return []byte("previous_hash"), 0, true
118118
},
119119
},
120120
nil,
@@ -133,13 +133,13 @@ func TestWaitIfCompetingBlock_AlreadyPastDelayDeadline(t *testing.T) {
133133
assert.False(t, result, "should return false (proceed to sign) when already past delay deadline")
134134
}
135135

136-
func TestWaitIfCompetingBlock_NoTimeRemainingInSubround(t *testing.T) {
136+
func TestWaitIfCompetingBlock_NoTimeRemainingBeforeSendDeadline(t *testing.T) {
137137
t.Parallel()
138138

139139
sr := createSubroundSignatureForCompetingBlockTests(
140140
&testscommon.SentSignatureTrackerStub{
141-
GetSignedHashCalled: func(pkBytes []byte, nonce uint64) ([]byte, bool) {
142-
return []byte("previous_hash"), true
141+
GetSignedNonceInfoCalled: func(pkBytes []byte, nonce uint64) ([]byte, int64, bool) {
142+
return []byte("previous_hash"), 0, true
143143
},
144144
},
145145
nil,
@@ -153,7 +153,6 @@ func TestWaitIfCompetingBlock_NoTimeRemainingInSubround(t *testing.T) {
153153
if maxTime > 200*time.Millisecond {
154154
return 200 * time.Millisecond
155155
}
156-
// No time remaining in signature subround
157156
return 0
158157
},
159158
},
@@ -168,8 +167,8 @@ func TestWaitIfCompetingBlock_ContextCancelled(t *testing.T) {
168167

169168
sr := createSubroundSignatureForCompetingBlockTests(
170169
&testscommon.SentSignatureTrackerStub{
171-
GetSignedHashCalled: func(pkBytes []byte, nonce uint64) ([]byte, bool) {
172-
return []byte("previous_hash"), true
170+
GetSignedNonceInfoCalled: func(pkBytes []byte, nonce uint64) ([]byte, int64, bool) {
171+
return []byte("previous_hash"), 0, true
173172
},
174173
},
175174
&dataRetriever.ProofsPoolMock{
@@ -202,8 +201,8 @@ func TestWaitIfCompetingBlock_ProofArrivesForPreviousBlock(t *testing.T) {
202201

203202
sr := createSubroundSignatureForCompetingBlockTests(
204203
&testscommon.SentSignatureTrackerStub{
205-
GetSignedHashCalled: func(pkBytes []byte, nonce uint64) ([]byte, bool) {
206-
return previousHash, true
204+
GetSignedNonceInfoCalled: func(pkBytes []byte, nonce uint64) ([]byte, int64, bool) {
205+
return previousHash, 0, true
207206
},
208207
},
209208
&dataRetriever.ProofsPoolMock{
@@ -243,8 +242,8 @@ func TestWaitIfCompetingBlock_DeadlineExpiresNoProof(t *testing.T) {
243242

244243
sr := createSubroundSignatureForCompetingBlockTests(
245244
&testscommon.SentSignatureTrackerStub{
246-
GetSignedHashCalled: func(pkBytes []byte, nonce uint64) ([]byte, bool) {
247-
return []byte("previous_hash"), true
245+
GetSignedNonceInfoCalled: func(pkBytes []byte, nonce uint64) ([]byte, int64, bool) {
246+
return []byte("previous_hash"), 0, true
248247
},
249248
},
250249
&dataRetriever.ProofsPoolMock{
@@ -274,13 +273,13 @@ func TestWaitIfCompetingBlock_DeadlineExpiresNoProof(t *testing.T) {
274273
assert.GreaterOrEqual(t, elapsed, 40*time.Millisecond, "should have waited at least ~50ms")
275274
}
276275

277-
func TestWaitIfCompetingBlock_DelayCappedBySubroundRemaining(t *testing.T) {
276+
func TestWaitIfCompetingBlock_DelayCappedBySendDeadline(t *testing.T) {
278277
t.Parallel()
279278

280279
sr := createSubroundSignatureForCompetingBlockTests(
281280
&testscommon.SentSignatureTrackerStub{
282-
GetSignedHashCalled: func(pkBytes []byte, nonce uint64) ([]byte, bool) {
283-
return []byte("previous_hash"), true
281+
GetSignedNonceInfoCalled: func(pkBytes []byte, nonce uint64) ([]byte, int64, bool) {
282+
return []byte("previous_hash"), 0, true
284283
},
285284
},
286285
&dataRetriever.ProofsPoolMock{
@@ -310,6 +309,107 @@ func TestWaitIfCompetingBlock_DelayCappedBySubroundRemaining(t *testing.T) {
310309
assert.Less(t, elapsed, 150*time.Millisecond, "delay should be capped, not full 300ms")
311310
}
312311

312+
func TestWaitIfCompetingBlock_OlderRoundSignsImmediately(t *testing.T) {
313+
t.Parallel()
314+
315+
// currentRound = 5, entry was signed at round = 3 (= currentRound-2), so not competing.
316+
sr := createSubroundSignatureForCompetingBlockTests(
317+
&testscommon.SentSignatureTrackerStub{
318+
GetSignedNonceInfoCalled: func(pkBytes []byte, nonce uint64) ([]byte, int64, bool) {
319+
return []byte("old_hash"), 3, true // round 3, two rounds behind
320+
},
321+
},
322+
nil,
323+
&testscommon.RoundHandlerMock{
324+
IndexCalled: func() int64 {
325+
return 5 // currentRound = 5, currentRound-1 = 4
326+
},
327+
TimeDurationCalled: func() time.Duration {
328+
return 600 * time.Millisecond
329+
},
330+
RemainingTimeCalled: func(startTime time.Time, maxTime time.Duration) time.Duration {
331+
t.Fatalf("RemainingTime should not be called when signed round is older than currentRound-1")
332+
return 0
333+
},
334+
},
335+
)
336+
337+
result := sr.WaitIfCompetingBlock(context.Background(), []byte("pk"), 100, []byte("current_hash"))
338+
assert.False(t, result)
339+
}
340+
341+
func TestWaitIfCompetingBlock_PreviousRoundWaits(t *testing.T) {
342+
t.Parallel()
343+
344+
// currentRound = 5, entry was signed at round = 4 (= currentRound-1), so it is competing.
345+
sr := createSubroundSignatureForCompetingBlockTests(
346+
&testscommon.SentSignatureTrackerStub{
347+
GetSignedNonceInfoCalled: func(pkBytes []byte, nonce uint64) ([]byte, int64, bool) {
348+
return []byte("previous_hash"), 4, true // round 4, one round behind
349+
},
350+
},
351+
&dataRetriever.ProofsPoolMock{
352+
HasProofCalled: func(shardID uint32, headerHash []byte) bool {
353+
return false
354+
},
355+
},
356+
&testscommon.RoundHandlerMock{
357+
IndexCalled: func() int64 {
358+
return 5 // currentRound = 5, currentRound-1 = 4
359+
},
360+
TimeDurationCalled: func() time.Duration {
361+
return 100 * time.Millisecond
362+
},
363+
RemainingTimeCalled: func(startTime time.Time, maxTime time.Duration) time.Duration {
364+
return maxTime
365+
},
366+
},
367+
)
368+
369+
start := time.Now()
370+
result := sr.WaitIfCompetingBlock(context.Background(), []byte("pk"), 100, []byte("current_hash"))
371+
elapsed := time.Since(start)
372+
373+
assert.False(t, result)
374+
assert.GreaterOrEqual(t, elapsed, 40*time.Millisecond)
375+
}
376+
377+
func TestWaitIfCompetingBlock_SameRoundWaits(t *testing.T) {
378+
t.Parallel()
379+
380+
// currentRound = 5, entry was signed in round = 5 (same round), so it is competing.
381+
sr := createSubroundSignatureForCompetingBlockTests(
382+
&testscommon.SentSignatureTrackerStub{
383+
GetSignedNonceInfoCalled: func(pkBytes []byte, nonce uint64) ([]byte, int64, bool) {
384+
return []byte("previous_hash"), 5, true // same round
385+
},
386+
},
387+
&dataRetriever.ProofsPoolMock{
388+
HasProofCalled: func(shardID uint32, headerHash []byte) bool {
389+
return false
390+
},
391+
},
392+
&testscommon.RoundHandlerMock{
393+
IndexCalled: func() int64 {
394+
return 5
395+
},
396+
TimeDurationCalled: func() time.Duration {
397+
return 100 * time.Millisecond
398+
},
399+
RemainingTimeCalled: func(startTime time.Time, maxTime time.Duration) time.Duration {
400+
return maxTime
401+
},
402+
},
403+
)
404+
405+
start := time.Now()
406+
result := sr.WaitIfCompetingBlock(context.Background(), []byte("pk"), 100, []byte("current_hash"))
407+
elapsed := time.Since(start)
408+
409+
assert.False(t, result)
410+
assert.GreaterOrEqual(t, elapsed, 40*time.Millisecond)
411+
}
412+
313413
func TestWaitIfCompetingBlock_RecordSignedNonceCalledBeforeBroadcast(t *testing.T) {
314414
t.Parallel()
315415

@@ -356,7 +456,7 @@ func TestWaitIfCompetingBlock_RecordSignedNonceCalledBeforeBroadcast(t *testing.
356456
sr,
357457
&statusHandler.AppStatusHandlerStub{},
358458
&testscommon.SentSignatureTrackerStub{
359-
RecordSignedNonceCalled: func(pkBytes []byte, nonce uint64, headerHash []byte) {
459+
RecordSignedNonceCalled: func(pkBytes []byte, nonce uint64, headerHash []byte, roundIndex int64) {
360460
recordCalled = true
361461
},
362462
},
@@ -375,8 +475,8 @@ func TestWaitIfCompetingBlock_NoCompetingBlockForAnyKey(t *testing.T) {
375475

376476
sr := createSubroundSignatureForCompetingBlockTests(
377477
&testscommon.SentSignatureTrackerStub{
378-
GetSignedHashCalled: func(pkBytes []byte, nonce uint64) ([]byte, bool) {
379-
return nil, false // no key has previously signed
478+
GetSignedNonceInfoCalled: func(pkBytes []byte, nonce uint64) ([]byte, int64, bool) {
479+
return nil, 0, false // no key has previously signed
380480
},
381481
},
382482
nil,
@@ -393,8 +493,8 @@ func TestWaitIfCompetingBlock_SameHashForAllKeys(t *testing.T) {
393493
currentHash := []byte("current_hash")
394494
sr := createSubroundSignatureForCompetingBlockTests(
395495
&testscommon.SentSignatureTrackerStub{
396-
GetSignedHashCalled: func(pkBytes []byte, nonce uint64) ([]byte, bool) {
397-
return currentHash, true // all keys signed the same hash
496+
GetSignedNonceInfoCalled: func(pkBytes []byte, nonce uint64) ([]byte, int64, bool) {
497+
return currentHash, 0, true // all keys signed the same hash
398498
},
399499
},
400500
nil,
@@ -442,11 +542,11 @@ func TestWaitIfCompetingBlock_SelfKeyHasCompetingBlock(t *testing.T) {
442542
sr,
443543
&statusHandler.AppStatusHandlerStub{},
444544
&testscommon.SentSignatureTrackerStub{
445-
GetSignedHashCalled: func(pkBytes []byte, nonce uint64) ([]byte, bool) {
545+
GetSignedNonceInfoCalled: func(pkBytes []byte, nonce uint64) ([]byte, int64, bool) {
446546
if string(pkBytes) == selfPk {
447-
return []byte("different_hash"), true
547+
return []byte("different_hash"), 0, true
448548
}
449-
return nil, false
549+
return nil, 0, false
450550
},
451551
},
452552
&consensusMocks.SposWorkerMock{},
@@ -510,16 +610,16 @@ func TestWaitIfCompetingBlock_ManagedKeyHasCompetingBlock(t *testing.T) {
510610
sr,
511611
&statusHandler.AppStatusHandlerStub{},
512612
&testscommon.SentSignatureTrackerStub{
513-
GetSignedHashCalled: func(pkBytes []byte, nonce uint64) ([]byte, bool) {
613+
GetSignedNonceInfoCalled: func(pkBytes []byte, nonce uint64) ([]byte, int64, bool) {
514614
if string(pkBytes) == selfPk {
515615
// Self key: no competing block
516-
return nil, false
616+
return nil, 0, false
517617
}
518618
if string(pkBytes) == "A" {
519619
// Managed key "A": has competing block
520-
return []byte("old_hash"), true
620+
return []byte("old_hash"), 0, true
521621
}
522-
return nil, false
622+
return nil, 0, false
523623
},
524624
},
525625
&consensusMocks.SposWorkerMock{},
@@ -577,9 +677,9 @@ func TestWaitIfCompetingBlock_WaitsOnceNotPerKey(t *testing.T) {
577677
sr,
578678
&statusHandler.AppStatusHandlerStub{},
579679
&testscommon.SentSignatureTrackerStub{
580-
GetSignedHashCalled: func(pkBytes []byte, nonce uint64) ([]byte, bool) {
680+
GetSignedNonceInfoCalled: func(pkBytes []byte, nonce uint64) ([]byte, int64, bool) {
581681
// ALL keys have signed a different hash
582-
return []byte("old_hash"), true
682+
return []byte("old_hash"), 0, true
583683
},
584684
},
585685
&consensusMocks.SposWorkerMock{},

consensus/spos/interface.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,8 @@ type PeerBlackListCacher interface {
178178
type SentSignaturesTracker interface {
179179
StartRound()
180180
SignatureSent(pkBytes []byte)
181-
RecordSignedNonce(pkBytes []byte, nonce uint64, headerHash []byte)
182-
GetSignedHash(pkBytes []byte, nonce uint64) ([]byte, bool)
181+
RecordSignedNonce(pkBytes []byte, nonce uint64, headerHash []byte, roundIndex int64)
182+
GetSignedNonceInfo(pkBytes []byte, nonce uint64) ([]byte, int64, bool)
183183
IsInterfaceNil() bool
184184
}
185185

0 commit comments

Comments
 (0)