Skip to content

Commit e4b7f8c

Browse files
committed
avoid triggering twice in parallel
1 parent e3e92ce commit e4b7f8c

11 files changed

Lines changed: 402 additions & 40 deletions

File tree

consensus/interface.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ type KeysHandler interface {
228228
// EquivalentProofsPool defines the behaviour of a proofs pool components
229229
type EquivalentProofsPool interface {
230230
AddProof(headerProof data.HeaderProofHandler) bool
231+
AddProofIfNoneAtNonce(headerProof data.HeaderProofHandler) (bool, data.HeaderProofHandler)
231232
GetProof(shardID uint32, headerHash []byte) (data.HeaderProofHandler, error)
232233
GetProofByNonce(headerNonce uint64, shardID uint32) (data.HeaderProofHandler, error)
233234
HasProof(shardID uint32, headerHash []byte) bool

consensus/spos/bls/v2/proofSelfAssembly.go

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,20 @@ import (
1515
// trySelfAssembleProof aggregates the snapshotted signature shares into a proof for the previous
1616
// round's block; it never creates a second proof: any existing proof at the nonce aborts it
1717
func trySelfAssembleProof(sr *spos.Subround, store signatureEvidenceHandler, ev *roundSignatureEvidence) {
18-
if !ev.assemblyStarted.CompareAndSwap(false, true) {
18+
currentRound := sr.RoundHandler().Index()
19+
if ev.nextAssemblyRound.Load() > currentRound {
20+
return // already attempted this round
21+
}
22+
if !ev.assemblyRunning.CompareAndSwap(false, true) {
1923
return
2024
}
21-
if !shouldNodeSendProofForGroup(sr, ev.consensusGroup) {
25+
defer ev.assemblyRunning.Store(false)
26+
ev.nextAssemblyRound.Store(currentRound + 1)
27+
28+
if abortOnExistingProof(sr, store, ev) {
2229
return
2330
}
24-
if abortOnExistingProof(sr, store, ev, "at entry") {
31+
if !shouldNodeSendProofForGroup(sr, ev.consensusGroup) {
2532
return
2633
}
2734

@@ -54,11 +61,14 @@ func trySelfAssembleProof(sr *spos.Subround, store signatureEvidenceHandler, ev
5461
IsStartOfEpoch: ev.isStartOfEpoch,
5562
}
5663

57-
// re-check right before publish, aggregation took time
58-
if abortOnExistingProof(sr, store, ev, "before publish") {
59-
return
60-
}
61-
if !sr.EquivalentProofsPool().AddProof(proof) {
64+
// atomic add: never overwrites a proof that arrived at the nonce during aggregation
65+
added, existing := sr.EquivalentProofsPool().AddProofIfNoneAtNonce(proof)
66+
if !added {
67+
if !check.IfNil(existing) && !bytes.Equal(existing.GetHeaderHash(), ev.headerHash) {
68+
log.Warn("trySelfAssembleProof: competing proof arrived during assembly, aborting broadcast",
69+
"nonce", ev.nonce, "ownHash", ev.headerHash, "existingHash", existing.GetHeaderHash())
70+
}
71+
store.DropRetained(ev.nonce) // nonce settled either way
6272
return
6373
}
6474
store.DropRetained(ev.nonce)
@@ -74,14 +84,14 @@ func trySelfAssembleProof(sr *spos.Subround, store signatureEvidenceHandler, ev
7484
"nonce", ev.nonce, "headerHash", ev.headerHash)
7585
}
7686

77-
func abortOnExistingProof(sr *spos.Subround, store signatureEvidenceHandler, ev *roundSignatureEvidence, stage string) bool {
87+
func abortOnExistingProof(sr *spos.Subround, store signatureEvidenceHandler, ev *roundSignatureEvidence) bool {
7888
existing, err := sr.EquivalentProofsPool().GetProofByNonce(ev.nonce, ev.shardID)
7989
if err != nil || check.IfNil(existing) {
8090
return false
8191
}
8292

8393
if !bytes.Equal(existing.GetHeaderHash(), ev.headerHash) {
84-
log.Warn("trySelfAssembleProof: competing proof already exists at nonce, aborting "+stage,
94+
log.Warn("trySelfAssembleProof: competing proof already exists at nonce, aborting assembly",
8595
"nonce", ev.nonce, "ownHash", ev.headerHash, "existingHash", existing.GetHeaderHash())
8696
}
8797
store.DropRetained(ev.nonce) // nonce settled either way
@@ -115,10 +125,12 @@ func stripInvalidShares(sr *spos.Subround, ev *roundSignatureEvidence, bitmap []
115125
sem := make(chan struct{}, maxParallelVerifications)
116126
var wg sync.WaitGroup
117127

128+
numVerified := 0
118129
for i := range ev.consensusGroup {
119130
if !isIndexSetInBitmap(i, bitmap) || shares[i] == nil {
120131
continue
121132
}
133+
numVerified++
122134

123135
wg.Add(1)
124136
sem <- struct{}{}
@@ -140,6 +152,14 @@ func stripInvalidShares(sr *spos.Subround, ev *roundSignatureEvidence, bitmap []
140152
}
141153
wg.Wait()
142154

155+
// quorum evidence always holds at least one honest, valid share, so all shares failing
156+
// points to an infrastructure error, not adversarial shares: keep the evidence intact
157+
if numVerified > 0 && len(invalidIndices) == numVerified {
158+
log.Warn("stripInvalidShares: all shares failed verification, keeping evidence for retry",
159+
"nonce", ev.nonce, "numShares", numVerified)
160+
return
161+
}
162+
143163
ev.dropShares(invalidIndices)
144164
}
145165

consensus/spos/bls/v2/proofSelfAssembly_test.go

Lines changed: 140 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"sync/atomic"
77
"testing"
8+
"time"
89

910
"github.com/multiversx/mx-chain-core-go/data"
1011
"github.com/multiversx/mx-chain-core-go/data/block"
@@ -99,9 +100,9 @@ func createSelfAssemblySetup(t *testing.T) *selfAssemblyTestSetup {
99100
GetProofByNonceCalled: func(headerNonce uint64, shardID uint32) (data.HeaderProofHandler, error) {
100101
return nil, errors.New("proof not found")
101102
},
102-
AddProofCalled: func(headerProof data.HeaderProofHandler) bool {
103+
AddProofIfNoneAtNonceCalled: func(headerProof data.HeaderProofHandler) (bool, data.HeaderProofHandler) {
103104
numAdded.Add(1)
104-
return true
105+
return true, nil
105106
},
106107
}
107108
container.SetEquivalentProofsPool(proofsPool)
@@ -211,7 +212,7 @@ func TestTrySelfAssembleProof_HappyPath(t *testing.T) {
211212
assert.Nil(t, err, "the self-assembled aggregated signature must verify")
212213
}
213214

214-
func TestTrySelfAssembleProof_AssemblyStartedOnlyOnce(t *testing.T) {
215+
func TestTrySelfAssembleProof_SingleAttemptPerRound(t *testing.T) {
215216
t.Parallel()
216217

217218
setup := createSelfAssemblySetup(t)
@@ -220,7 +221,7 @@ func TestTrySelfAssembleProof_AssemblyStartedOnlyOnce(t *testing.T) {
220221
trySelfAssembleProof(setup.subround, setup.store, ev)
221222
trySelfAssembleProof(setup.subround, setup.store, ev)
222223

223-
assert.Equal(t, int32(1), setup.numAdded.Load(), "the CAS guard must prevent a second assembly")
224+
assert.Equal(t, int32(1), setup.numAdded.Load(), "a second attempt in the same round must be skipped")
224225
}
225226

226227
func TestTrySelfAssembleProof_StripsInvalidSharesAndRetries(t *testing.T) {
@@ -297,18 +298,20 @@ func TestTrySelfAssembleProof_AbortsOnCompetingProof(t *testing.T) {
297298
setup := createSelfAssemblySetup(t)
298299
ev := setup.createRealEvidence(t, []byte("header hash"), 7)
299300

300-
var numChecks atomic.Int32
301-
setup.proofsPool.GetProofByNonceCalled = func(headerNonce uint64, shardID uint32) (data.HeaderProofHandler, error) {
302-
if numChecks.Add(1) == 1 {
303-
return nil, errors.New("proof not found")
304-
}
305-
return &block.HeaderProof{HeaderHash: []byte("competing hash"), HeaderNonce: headerNonce}, nil
301+
// entry check finds no proof, the atomic add is beaten by a competing proof
302+
setup.proofsPool.AddProofIfNoneAtNonceCalled = func(headerProof data.HeaderProofHandler) (bool, data.HeaderProofHandler) {
303+
return false, &block.HeaderProof{HeaderHash: []byte("competing hash"), HeaderNonce: headerProof.GetHeaderNonce()}
306304
}
307305

306+
setup.store.Capture(ev)
307+
setup.store.Capture(nil)
308+
308309
trySelfAssembleProof(setup.subround, setup.store, ev)
309310

310-
assert.Nil(t, setup.broadcast.Load(), "broadcast must be aborted by the pre-publish re-check")
311-
assert.Equal(t, int32(0), setup.numAdded.Load())
311+
assert.Nil(t, setup.broadcast.Load(), "broadcast must be aborted by the atomic add")
312+
313+
_, ok := setup.store.GetRetainedQuorumEvidence(ev.nonce)
314+
assert.False(t, ok, "settled nonce must drop the retained slot")
312315
})
313316

314317
t.Run("proof for the own hash means already done", func(t *testing.T) {
@@ -348,6 +351,131 @@ func TestTrySelfAssembleProof_EpochBoundaryUsesSnapshotGroup(t *testing.T) {
348351
assert.Nil(t, err)
349352
}
350353

354+
func TestTrySelfAssembleProof_InProgressAttemptBlocksUntilDone(t *testing.T) {
355+
t.Parallel()
356+
357+
var currentRound atomic.Int64
358+
currentRound.Store(10)
359+
360+
proceed := make(chan struct{})
361+
started := make(chan struct{}, 4)
362+
var numAggregations atomic.Int32
363+
364+
container := consensusMocks.InitConsensusCore()
365+
container.SetRoundHandler(&testscommon.RoundHandlerMock{
366+
IndexCalled: func() int64 {
367+
return currentRound.Load()
368+
},
369+
})
370+
container.SetSigningHandler(&consensusMocks.SigningHandlerStub{
371+
AggregateSigsWithKeysCalled: func(pubKeys []string, bitmap []byte, sigShares [][]byte, epoch uint32) ([]byte, error) {
372+
numAggregations.Add(1)
373+
started <- struct{}{}
374+
<-proceed
375+
return []byte("agg"), nil
376+
},
377+
})
378+
container.SetEquivalentProofsPool(createUnsettledProofsPool())
379+
380+
sr := createFlowSubround(t, container, bls.SrSignature, "(SIGNATURE)")
381+
store, _ := newSignatureEvidenceStore(createUnsettledProofsPool())
382+
ev := createTestEvidence(9, 5, 7, 8)
383+
384+
// first attempt starts in round 10 and blocks inside the aggregation
385+
go trySelfAssembleProof(sr, store, ev)
386+
<-started
387+
388+
// next round while the first attempt is still executing: no second attempt
389+
currentRound.Store(11)
390+
trySelfAssembleProof(sr, store, ev)
391+
assert.Equal(t, int32(1), numAggregations.Load())
392+
393+
close(proceed)
394+
require.Eventually(t, func() bool {
395+
return !ev.assemblyRunning.Load()
396+
}, time.Second, time.Millisecond)
397+
398+
// round 11 had no attempt started, so a retry is allowed after completion
399+
trySelfAssembleProof(sr, store, ev)
400+
assert.Equal(t, int32(2), numAggregations.Load())
401+
402+
// a second attempt in the same round is skipped
403+
trySelfAssembleProof(sr, store, ev)
404+
assert.Equal(t, int32(2), numAggregations.Load())
405+
406+
// next round: retried again
407+
currentRound.Store(12)
408+
trySelfAssembleProof(sr, store, ev)
409+
assert.Equal(t, int32(3), numAggregations.Load())
410+
}
411+
412+
func TestTrySelfAssembleProof_SettledNonceDropsRetainedEvenWhenIneligible(t *testing.T) {
413+
t.Parallel()
414+
415+
var numAggregations atomic.Int32
416+
container := consensusMocks.InitConsensusCore()
417+
container.SetSigningHandler(&consensusMocks.SigningHandlerStub{
418+
AggregateSigsWithKeysCalled: func(pubKeys []string, bitmap []byte, sigShares [][]byte, epoch uint32) ([]byte, error) {
419+
numAggregations.Add(1)
420+
return []byte("agg"), nil
421+
},
422+
})
423+
container.SetEquivalentProofsPool(createSettledProofsPool([]byte("competing hash")))
424+
425+
sr := createFlowSubround(t, container, bls.SrSignature, "(SIGNATURE)")
426+
store, _ := newSignatureEvidenceStore(createUnsettledProofsPool())
427+
428+
ev := createTestEvidence(9, 5, 7, 8)
429+
for i := range ev.consensusGroup {
430+
ev.consensusGroup[i] = "other_" + ev.consensusGroup[i]
431+
}
432+
store.Capture(ev)
433+
store.Capture(nil)
434+
_, ok := store.GetRetainedQuorumEvidence(ev.nonce)
435+
require.True(t, ok)
436+
437+
trySelfAssembleProof(sr, store, ev)
438+
439+
assert.Equal(t, int32(0), numAggregations.Load())
440+
_, ok = store.GetRetainedQuorumEvidence(ev.nonce)
441+
assert.False(t, ok, "the settled nonce must drop the retained slot before the eligibility check")
442+
}
443+
444+
func TestTrySelfAssembleProof_KeepsEvidenceWhenAllSharesFailVerification(t *testing.T) {
445+
t.Parallel()
446+
447+
var numAdded atomic.Int32
448+
container := consensusMocks.InitConsensusCore()
449+
container.SetSigningHandler(&consensusMocks.SigningHandlerStub{
450+
AggregateSigsWithKeysCalled: func(pubKeys []string, bitmap []byte, sigShares [][]byte, epoch uint32) ([]byte, error) {
451+
return nil, errors.New("infrastructure error")
452+
},
453+
VerifySigShareWithKeyCalled: func(pubKey []byte, sigShare []byte, message []byte, epoch uint32) error {
454+
return errors.New("infrastructure error")
455+
},
456+
})
457+
proofsPool := createUnsettledProofsPool()
458+
proofsPool.AddProofIfNoneAtNonceCalled = func(headerProof data.HeaderProofHandler) (bool, data.HeaderProofHandler) {
459+
numAdded.Add(1)
460+
return true, nil
461+
}
462+
container.SetEquivalentProofsPool(proofsPool)
463+
464+
sr := createFlowSubround(t, container, bls.SrSignature, "(SIGNATURE)")
465+
store, _ := newSignatureEvidenceStore(createUnsettledProofsPool())
466+
467+
ev := createTestEvidence(9, 5, 7, 8)
468+
store.Capture(ev)
469+
store.Capture(nil)
470+
471+
trySelfAssembleProof(sr, store, ev)
472+
473+
assert.Equal(t, 8, ev.getCount(), "evidence must stay intact on an all-shares failure")
474+
assert.Equal(t, int32(0), numAdded.Load())
475+
_, ok := store.GetRetainedQuorumEvidence(ev.nonce)
476+
assert.True(t, ok, "no demotion on an all-shares failure")
477+
}
478+
351479
func TestTrySelfAssembleProof_NotEligibleWithoutGroupKeys(t *testing.T) {
352480
t.Parallel()
353481

consensus/spos/bls/v2/signatureEvidence.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,19 @@ import (
1313
// roundSignatureEvidence is a snapshot of the signature shares observed for one block,
1414
// taken at the start of the following round, before the consensus state is reset.
1515
type roundSignatureEvidence struct {
16-
roundIndex int64
17-
nonce uint64
18-
headerHash []byte
19-
epoch uint32
20-
headerRound uint64
21-
shardID uint32
22-
isStartOfEpoch bool
23-
threshold int
24-
consensusGroup []string
25-
assemblyStarted atomic.Bool
16+
roundIndex int64
17+
nonce uint64
18+
headerHash []byte
19+
epoch uint32
20+
headerRound uint64
21+
shardID uint32
22+
isStartOfEpoch bool
23+
threshold int
24+
consensusGroup []string
25+
// assemblyRunning is true strictly while an assembly attempt executes;
26+
// nextAssemblyRound is the first round allowed to start the next attempt
27+
assemblyRunning atomic.Bool
28+
nextAssemblyRound atomic.Int64
2629

2730
mut sync.RWMutex
2831
bitmap []byte

consensus/spos/bls/v2/signatureEvidenceFlow_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,15 @@ func TestSubroundStartRound_CaptureSignatureEvidence(t *testing.T) {
179179
func TestSubroundStartRound_CaptureTriggersSelfAssemblyOnQuorum(t *testing.T) {
180180
t.Parallel()
181181

182+
var chronologyRound atomic.Int64
183+
chronologyRound.Store(11)
184+
182185
container := consensusMocks.InitConsensusCore()
186+
container.SetRoundHandler(&testscommon.RoundHandlerMock{
187+
IndexCalled: func() int64 {
188+
return chronologyRound.Load()
189+
},
190+
})
183191
container.SetSigningHandler(&consensusMocks.SigningHandlerStub{})
184192
container.SetEquivalentProofsPool(createUnsettledProofsPool())
185193

@@ -213,7 +221,14 @@ func TestSubroundStartRound_CaptureTriggersSelfAssemblyOnQuorum(t *testing.T) {
213221
require.Fail(t, "self-assembly was not triggered at round start")
214222
}
215223

224+
ev, found := store.GetPreviousRoundEvidence(5, 11)
225+
require.True(t, found)
226+
require.Eventually(t, func() bool {
227+
return !ev.assemblyRunning.Load()
228+
}, time.Second, time.Millisecond)
229+
216230
// retained evidence re-fires on the next round while the nonce stays unsettled
231+
chronologyRound.Store(12)
217232
srStartRound.SetHeader(nil)
218233
srStartRound.SetData(nil)
219234
srStartRound.captureSignatureEvidence()

consensus/spos/bls/v2/subroundStartRound.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,6 @@ func (sr *subroundStartRound) captureSignatureEvidence() {
128128
return
129129
}
130130

131-
// allow one assembly attempt per round while the nonce stays unsettled
132-
ev.assemblyStarted.Store(false)
133131
go trySelfAssembleProof(sr.Subround, sr.signatureEvidence, ev)
134132
}
135133

@@ -330,7 +328,7 @@ func (sr *subroundStartRound) computeNumManagedKeysInConsensusGroup(pubKeys []st
330328
return numMultiKeysInConsensusGroup
331329
}
332330

333-
func (sr *subroundStartRound) indexRoundIfNeeded(pubKeys []string) {
331+
func (sr *subroundStartRound) indexRoundIfNeeded(_ []string) {
334332
sr.outportMutex.RLock()
335333
defer sr.outportMutex.RUnlock()
336334

0 commit comments

Comments
 (0)