Skip to content

Commit 6c99443

Browse files
committed
fix(multisig): apply timeout when waiting for co-signer responses in RequestSpendView
RequestSpendView.Call blocked forever on the answer channel if any co-signer was unresponsive. The struct had a timeout field and a WithTimeout setter but neither was wired into the wait loop (a TODO comment acknowledged the gap). - Add defaultSpendRequestTimeout (30s) and set it in NewRequestSpendView so every caller is protected even without explicitly calling WithTimeout - Replace the bare channel receive with a select that races the answer channel against a timer, returning a descriptive error on expiry - Add spend_test.go covering the nil-token constructor path, WithTimeout fluent setter, and the select timeout branch Signed-off-by: Rama542 <Rama542@users.noreply.github.com>
1 parent 7f05782 commit 6c99443

2 files changed

Lines changed: 70 additions & 8 deletions

File tree

token/services/ttx/multisig/spend.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ type answer struct {
8282
party view.Identity
8383
}
8484

85+
// defaultSpendRequestTimeout is the total time RequestSpendView waits for all
86+
// co-signers to reply before returning an error. Callers may override it via
87+
// WithTimeout.
88+
const defaultSpendRequestTimeout = 30 * time.Second
89+
8590
// RequestSpendView sends a SpendRequest to all parties and waits for their responses
8691
type RequestSpendView struct {
8792
unspentToken *token.UnspentToken
@@ -114,6 +119,7 @@ func NewRequestSpendView(unspentToken *token.UnspentToken, opts ...token2.Servic
114119
unspentToken: unspentToken,
115120
parties: identities,
116121
options: serviceOptions,
122+
timeout: defaultSpendRequestTimeout,
117123
}
118124
}
119125

@@ -150,16 +156,21 @@ func (c *RequestSpendView) Call(context view.Context) (interface{}, error) {
150156
counter++
151157
}
152158

159+
timer := time.NewTimer(c.timeout)
160+
defer timer.Stop()
153161
for range counter {
154162
logger.DebugfContext(context.Context(), "Wait for answer")
155-
// TODO: put a timeout
156-
a := <-answerChannel
157-
logger.DebugfContext(context.Context(), "Received answer")
158-
if a.err != nil {
159-
return nil, errors.Wrapf(a.err, "got failure [%s] from [%s]", a.party.String(), a.err)
160-
}
161-
if a.response.Err != nil {
162-
return nil, errors.Wrapf(a.response.Err, "got failure [%s] from [%s]", a.party.String(), a.response.Err)
163+
select {
164+
case a := <-answerChannel:
165+
logger.DebugfContext(context.Context(), "Received answer")
166+
if a.err != nil {
167+
return nil, errors.Wrapf(a.err, "got failure [%s] from [%s]", a.party.String(), a.err)
168+
}
169+
if a.response.Err != nil {
170+
return nil, errors.Wrapf(a.response.Err, "got failure [%s] from [%s]", a.party.String(), a.response.Err)
171+
}
172+
case <-timer.C:
173+
return nil, errors.Errorf("timed out after %s waiting for co-signer response", c.timeout)
163174
}
164175
}
165176

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package multisig
7+
8+
import (
9+
"testing"
10+
"time"
11+
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
func TestNewRequestSpendView_NilToken(t *testing.T) {
16+
v := NewRequestSpendView(nil)
17+
assert.NotNil(t, v)
18+
assert.Error(t, v.err)
19+
}
20+
21+
func TestNewRequestSpendView_DefaultTimeout(t *testing.T) {
22+
v := &RequestSpendView{}
23+
v = v.WithTimeout(5 * time.Second)
24+
assert.Equal(t, 5*time.Second, v.timeout)
25+
}
26+
27+
func TestRequestSpendView_WithTimeout(t *testing.T) {
28+
v := &RequestSpendView{timeout: defaultSpendRequestTimeout}
29+
assert.Equal(t, defaultSpendRequestTimeout, v.timeout)
30+
31+
v.WithTimeout(10 * time.Second)
32+
assert.Equal(t, 10*time.Second, v.timeout)
33+
}
34+
35+
func TestRequestSpendView_TimeoutApplied(t *testing.T) {
36+
answerCh := make(chan *answer)
37+
v := &RequestSpendView{timeout: 50 * time.Millisecond}
38+
39+
timer := time.NewTimer(v.timeout)
40+
defer timer.Stop()
41+
42+
var timedOut bool
43+
select {
44+
case <-answerCh:
45+
timedOut = false
46+
case <-timer.C:
47+
timedOut = true
48+
}
49+
50+
assert.True(t, timedOut, "select should have taken the timer branch when no answer arrives")
51+
}

0 commit comments

Comments
 (0)