Skip to content

Commit 6c31a57

Browse files
authored
Fix exchanger deadlock during partial signature exchange (#4590)
1 parent 611bb0b commit 6c31a57

2 files changed

Lines changed: 125 additions & 43 deletions

File tree

dkg/exchanger.go

Lines changed: 80 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,22 @@ const (
4040
// sigTypeStore is a shorthand for a map of sigType to map of core.PubKey to slice of core.ParSignedData.
4141
type sigTypeStore map[sigType]map[core.PubKey][]core.ParSignedData
4242

43-
// dataByPubkey maps a sigType to its map of public key to slice of core.ParSignedData..
43+
// dataByPubkey holds the partial signatures collected per sigType and the pending exchange queries
44+
// awaiting them. Both are guarded by lock.
4445
type dataByPubkey struct {
45-
store sigTypeStore
46-
lock sync.Mutex
46+
store sigTypeStore
47+
queries []exchangeQuery
48+
lock sync.Mutex
49+
}
50+
51+
// exchangeQuery is a pending exchange call awaiting all its expected partial signatures for a sigType.
52+
type exchangeQuery struct {
53+
sigType sigType
54+
expected int
55+
// response is buffered (size 1) so resolveQueriesUnsafe never blocks, even when it runs on the
56+
// exchange goroutine itself (pushPsigs may resolve queries synchronously via StoreInternal).
57+
response chan<- map[core.PubKey][]core.ParSignedData
58+
cancel <-chan struct{}
4759
}
4860

4961
// exchanger is responsible for exchanging partial signatures between peers on libp2p.
@@ -53,7 +65,6 @@ type exchanger struct {
5365
sigTypes map[sigType]bool
5466
sigData dataByPubkey
5567
dutyGaterFunc func(duty core.Duty) bool
56-
sigDatasChan chan map[core.PubKey][]core.ParSignedData
5768
}
5869

5970
func newExchanger(p2pNode host.Host, peerIdx int, peers []peer.ID, sigTypes []sigType, timeout time.Duration) *exchanger {
@@ -90,7 +101,6 @@ func newExchanger(p2pNode host.Host, peerIdx int, peers []peer.ID, sigTypes []si
90101
lock: sync.Mutex{},
91102
},
92103
dutyGaterFunc: dutyGaterFunc,
93-
sigDatasChan: make(chan map[core.PubKey][]core.ParSignedData, 1),
94104
}
95105

96106
// Wiring core workflow components
@@ -112,60 +122,87 @@ func (e *exchanger) exchange(ctx context.Context, sigType sigType, set core.ParS
112122
return nil, err
113123
}
114124

115-
expectedSigs := len(set)
116-
117-
for {
118-
select {
119-
case sigDatas, ok := <-e.sigDatasChan:
120-
if !ok {
121-
return nil, errors.New("sigdata channel has been closed")
122-
}
123-
124-
if len(sigDatas) == expectedSigs {
125-
// We are done when we have ParSignedData of all the DVs from all each peer
126-
return sigDatas, nil
127-
}
128-
case <-ctx.Done():
129-
return nil, ctx.Err()
125+
cancel := make(chan struct{})
126+
defer close(cancel)
127+
128+
response := make(chan map[core.PubKey][]core.ParSignedData, 1)
129+
130+
// Register the query, then resolve immediately in case all expected signatures are already
131+
// collected (e.g. peers delivered theirs before the StoreInternal above completed the threshold).
132+
e.sigData.lock.Lock()
133+
e.sigData.queries = append(e.sigData.queries, exchangeQuery{
134+
sigType: sigType,
135+
expected: len(set),
136+
response: response,
137+
cancel: cancel,
138+
})
139+
e.resolveQueriesUnsafe()
140+
e.sigData.lock.Unlock()
141+
142+
select {
143+
case <-ctx.Done():
144+
return nil, ctx.Err()
145+
case data := <-response:
146+
return data, nil
147+
}
148+
}
149+
150+
// resolveQueriesUnsafe delivers the collected signatures to every pending query whose sigType has
151+
// reached its expected count, dropping cancelled queries. It must be called with sigData.lock held.
152+
func (e *exchanger) resolveQueriesUnsafe() {
153+
var remaining []exchangeQuery
154+
155+
for _, q := range e.sigData.queries {
156+
if cancelled(q.cancel) {
157+
continue
158+
}
159+
160+
data := e.sigData.store[q.sigType]
161+
if len(data) != q.expected {
162+
remaining = append(remaining, q)
163+
continue
130164
}
165+
166+
// We are done when we have ParSignedData of all the DVs from each peer.
167+
ret := make(map[core.PubKey][]core.ParSignedData, len(data))
168+
maps.Copy(ret, data)
169+
170+
q.response <- ret // Never blocks: response is buffered and each query is resolved at most once.
171+
}
172+
173+
e.sigData.queries = remaining
174+
}
175+
176+
// cancelled returns true if the channel is closed.
177+
func cancelled(ch <-chan struct{}) bool {
178+
select {
179+
case <-ch:
180+
return true
181+
default:
182+
return false
131183
}
132184
}
133185

134-
// pushPsigs is responsible for writing partial signature data to sigChan obtained from other peers.
135-
func (e *exchanger) pushPsigs(ctx context.Context, duty core.Duty, set map[core.PubKey][]core.ParSignedData) error {
186+
// pushPsigs stores partial signature data obtained from peers and resolves any pending query in
187+
// exchange whose expected signatures are now all collected.
188+
func (e *exchanger) pushPsigs(_ context.Context, duty core.Duty, set map[core.PubKey][]core.ParSignedData) error {
136189
sigType := sigType(duty.Slot)
137190

138191
if !e.dutyGaterFunc(duty) {
139192
return errors.New("unrecognized sigType", z.Int("sigType", int(sigType)))
140193
}
141194

142195
e.sigData.lock.Lock()
196+
defer e.sigData.lock.Unlock()
143197

144-
for pk, psigs := range set {
145-
_, ok := e.sigData.store[sigType]
146-
if !ok {
147-
e.sigData.store[sigType] = map[core.PubKey][]core.ParSignedData{}
148-
}
149-
150-
e.sigData.store[sigType][pk] = psigs
151-
}
152-
153-
data, ok := e.sigData.store[sigType]
198+
_, ok := e.sigData.store[sigType]
154199
if !ok {
155-
e.sigData.lock.Unlock()
156-
return nil
200+
e.sigData.store[sigType] = map[core.PubKey][]core.ParSignedData{}
157201
}
158202

159-
ret := make(map[core.PubKey][]core.ParSignedData)
160-
maps.Copy(ret, data)
161-
162-
e.sigData.lock.Unlock()
203+
maps.Copy(e.sigData.store[sigType], set)
163204

164-
select {
165-
case e.sigDatasChan <- ret:
166-
case <-ctx.Done():
167-
return errors.Wrap(ctx.Err(), "feed collected sig data")
168-
}
205+
e.resolveQueriesUnsafe()
169206

170207
return nil
171208
}

dkg/exchanger_internal_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,48 @@ func TestExchanger(t *testing.T) {
179179
// require that we encountered all the sigTypes expected
180180
require.Len(t, actual, len(expectedSigTypes))
181181
}
182+
183+
// TestExchangerPushPsigsNeverBlocks fires pushPsigs repeatedly with no exchange call draining
184+
// results, which must not block. A previous implementation used a shared size-1 channel that
185+
// deadlocked once full, especially when pushPsigs ran synchronously on the exchange goroutine.
186+
func TestExchangerPushPsigsNeverBlocks(t *testing.T) {
187+
ctx := context.Background()
188+
189+
h := testutil.CreateHost(t, testutil.AvailableAddr(t))
190+
peers := []peer.ID{h.ID()}
191+
192+
ex := newExchanger(h, 0, peers, []sigType{sigLock}, time.Second)
193+
194+
duty := core.NewSignatureDuty(uint64(sigLock))
195+
196+
newSet := func() map[core.PubKey][]core.ParSignedData {
197+
return map[core.PubKey][]core.ParSignedData{
198+
testutil.RandomCorePubKey(t): {core.NewPartialSignature(testutil.RandomCoreSignature(), 1)},
199+
}
200+
}
201+
202+
// Fire the threshold subscriber several times without any exchange call consuming results.
203+
const iterations = 5
204+
205+
errs := make(chan error, 1)
206+
done := make(chan struct{})
207+
208+
go func() {
209+
for range iterations {
210+
if err := ex.pushPsigs(ctx, duty, newSet()); err != nil {
211+
errs <- err
212+
return
213+
}
214+
}
215+
216+
close(done)
217+
}()
218+
219+
select {
220+
case <-done:
221+
case err := <-errs:
222+
require.NoError(t, err)
223+
case <-time.After(5 * time.Second):
224+
t.Fatal("pushPsigs deadlocked")
225+
}
226+
}

0 commit comments

Comments
 (0)