Skip to content

Commit 3e665a1

Browse files
AKRAM@il.ibm.comAkramBitar
authored andcommitted
Fix: Ensure Done() is called on external wallets even on errors
Use defer to guarantee that Done() is called on all external wallet signers regardless of whether errors occur during signature collection, auditing, or approval phases. This prevents resource leaks when the endorsement process fails early. Previously, if requestSignaturesOnIssues or requestSignaturesOnTransfers returned an error, any external wallets already added to the map would not have their Done() method called, potentially leaving resources uncleaned. Signed-off-by: AkramBitar <akram@il.ibm.com>
1 parent a3e86c3 commit 3e665a1

File tree

2 files changed

+141
-9
lines changed

2 files changed

+141
-9
lines changed

token/services/ttx/collectendorsements.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ func (c *CollectEndorsementsView) Call(context view.Context) (interface{}, error
8484
metrics := GetMetrics(context)
8585

8686
externalWallets := make(map[string]ExternalWalletSigner)
87+
88+
// Ensure Done() is called on all external wallets regardless of errors
89+
defer c.CleanupExternalWallets(context, externalWallets)
90+
8791
// 1. First collect signatures on the token request
8892
issueSigmas, err := c.requestSignaturesOnIssues(context, externalWallets)
8993
if err != nil {
@@ -95,15 +99,6 @@ func (c *CollectEndorsementsView) Call(context view.Context) (interface{}, error
9599
return nil, errors.WithMessagef(err, "failed requesting signatures on transfers")
96100
}
97101

98-
// signal the external wallets that the process is completed
99-
logger.DebugfContext(context.Context(), "Inform external wallets that endorsement is complete")
100-
for id, signer := range externalWallets {
101-
if err := signer.Done(); err != nil {
102-
logger.ErrorfContext(context.Context(), "failed to signal done external wallet [%s]", id)
103-
logger.Errorf("failed to signal done external wallet [%s]", id)
104-
}
105-
}
106-
107102
// Add the signatures to the token request
108103
logger.DebugfContext(context.Context(), "Add the signatures to the token request")
109104
if !c.tx.TokenRequest.SetSignatures(mergeSigmas(issueSigmas, transferSigmas)) {
@@ -665,3 +660,13 @@ func TransferDistributionList(r *token.Request) []view.Identity {
665660
}
666661
return distributionList
667662
}
663+
664+
// CleanupExternalWallets calls Done() on all external wallets to signal completion
665+
func (c *CollectEndorsementsView) CleanupExternalWallets(context view.Context, externalWallets map[string]ExternalWalletSigner) {
666+
logger.DebugfContext(context.Context(), "Inform external wallets that endorsement is complete")
667+
for id, signer := range externalWallets {
668+
if err := signer.Done(); err != nil {
669+
logger.ErrorfContext(context.Context(), "failed to signal done external wallet [%s]", id)
670+
}
671+
}
672+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package ttx_test
8+
9+
import (
10+
"testing"
11+
12+
"github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors"
13+
"github.com/hyperledger-labs/fabric-smart-client/platform/view/view"
14+
"github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx"
15+
mock2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx/dep/mock"
16+
"github.com/stretchr/testify/assert"
17+
)
18+
19+
// mockExternalWalletSigner is a mock implementation of ExternalWalletSigner for testing
20+
type mockExternalWalletSigner struct {
21+
signFunc func(party view.Identity, message []byte) ([]byte, error)
22+
doneFunc func() error
23+
doneCalled bool
24+
signCalled bool
25+
}
26+
27+
func (m *mockExternalWalletSigner) Sign(party view.Identity, message []byte) ([]byte, error) {
28+
m.signCalled = true
29+
if m.signFunc != nil {
30+
return m.signFunc(party, message)
31+
}
32+
return []byte("mock_signature"), nil
33+
}
34+
35+
func (m *mockExternalWalletSigner) Done() error {
36+
m.doneCalled = true
37+
if m.doneFunc != nil {
38+
return m.doneFunc()
39+
}
40+
return nil
41+
}
42+
43+
// TestCleanupExternalWallets_Success tests that CleanupExternalWallets calls Done() on all wallets
44+
func TestCleanupExternalWallets_Success(t *testing.T) {
45+
wallet := &mockExternalWalletSigner{}
46+
47+
externalWallets := map[string]ttx.ExternalWalletSigner{
48+
"wallet1": wallet,
49+
}
50+
51+
// Create a minimal view and context
52+
view := &ttx.CollectEndorsementsView{}
53+
ctx := &mock2.Context{}
54+
ctx.ContextReturns(t.Context())
55+
56+
// Call the cleanup method
57+
view.CleanupExternalWallets(ctx, externalWallets)
58+
59+
assert.True(t, wallet.doneCalled, "Done() should have been called on the wallet")
60+
}
61+
62+
// TestCleanupExternalWallets_MultipleWallets tests that Done() is called on all wallets
63+
func TestCleanupExternalWallets_MultipleWallets(t *testing.T) {
64+
wallet1 := &mockExternalWalletSigner{}
65+
wallet2 := &mockExternalWalletSigner{}
66+
wallet3 := &mockExternalWalletSigner{}
67+
68+
externalWallets := map[string]ttx.ExternalWalletSigner{
69+
"wallet1": wallet1,
70+
"wallet2": wallet2,
71+
"wallet3": wallet3,
72+
}
73+
74+
view := &ttx.CollectEndorsementsView{}
75+
ctx := &mock2.Context{}
76+
ctx.ContextReturns(t.Context())
77+
78+
view.CleanupExternalWallets(ctx, externalWallets)
79+
80+
assert.True(t, wallet1.doneCalled, "Done() should have been called on wallet1")
81+
assert.True(t, wallet2.doneCalled, "Done() should have been called on wallet2")
82+
assert.True(t, wallet3.doneCalled, "Done() should have been called on wallet3")
83+
}
84+
85+
// TestCleanupExternalWallets_DoneError tests that errors from Done() don't stop cleanup
86+
func TestCleanupExternalWallets_DoneError(t *testing.T) {
87+
wallet1 := &mockExternalWalletSigner{
88+
doneFunc: func() error {
89+
return errors.New("wallet1 done failed")
90+
},
91+
}
92+
wallet2 := &mockExternalWalletSigner{}
93+
wallet3 := &mockExternalWalletSigner{
94+
doneFunc: func() error {
95+
return errors.New("wallet3 done failed")
96+
},
97+
}
98+
99+
externalWallets := map[string]ttx.ExternalWalletSigner{
100+
"wallet1": wallet1,
101+
"wallet2": wallet2,
102+
"wallet3": wallet3,
103+
}
104+
105+
view := &ttx.CollectEndorsementsView{}
106+
ctx := &mock2.Context{}
107+
ctx.ContextReturns(t.Context())
108+
109+
// Should not panic even if Done() returns errors
110+
view.CleanupExternalWallets(ctx, externalWallets)
111+
112+
assert.True(t, wallet1.doneCalled, "Done() should have been called on wallet1 despite error")
113+
assert.True(t, wallet2.doneCalled, "Done() should have been called on wallet2")
114+
assert.True(t, wallet3.doneCalled, "Done() should have been called on wallet3 despite error")
115+
}
116+
117+
// TestCleanupExternalWallets_EmptyMap tests cleanup with no wallets
118+
func TestCleanupExternalWallets_EmptyMap(t *testing.T) {
119+
externalWallets := map[string]ttx.ExternalWalletSigner{}
120+
121+
view := &ttx.CollectEndorsementsView{}
122+
ctx := &mock2.Context{}
123+
ctx.ContextReturns(t.Context())
124+
125+
// Should not panic with empty map
126+
view.CleanupExternalWallets(ctx, externalWallets)
127+
}

0 commit comments

Comments
 (0)