Skip to content
This repository was archived by the owner on Apr 25, 2025. It is now read-only.

Commit e529e89

Browse files
committed
[FAB-10667] Resolve additional endorsers for CC-to-CC
A client may not know the complete invocation chain when invoking a chaincode (i.e. the target chaincode may invoke other chaincodes) and therefore the transaction may fail due to insufficient endorsements. Additional logic was added to the Channel Client to automatically detect additional invoked chaincodes by reading the RWSets in the proposal response. If additional namespaces are found then the Selection Service recalculates the required set of endorsers and requests are sent to those additional endorsers. Change-Id: I41ec5e63bdbc216266c3ad959263e32127d7d4d1 Signed-off-by: Bob Stasyszyn <Bob.Stasyszyn@securekey.com>
1 parent 327a946 commit e529e89

File tree

14 files changed

+592
-112
lines changed

14 files changed

+592
-112
lines changed

pkg/client/channel/api.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
reqContext "context"
1111
"time"
1212

13+
"github.com/hyperledger/fabric-sdk-go/pkg/client/channel/invoke"
1314
"github.com/hyperledger/fabric-sdk-go/pkg/common/errors/retry"
1415
"github.com/hyperledger/fabric-sdk-go/pkg/common/providers/context"
1516
"github.com/hyperledger/fabric-sdk-go/pkg/common/providers/fab"
@@ -26,6 +27,7 @@ type requestOptions struct {
2627
BeforeRetry retry.BeforeRetryHandler
2728
Timeouts map[fab.TimeoutType]time.Duration //timeout options for channel client operations
2829
ParentContext reqContext.Context //parent grpc context for channel client operations (query, execute, invokehandler)
30+
CCFilter invoke.CCFilter
2931
}
3032

3133
// RequestOption func for each Opts argument
@@ -144,3 +146,11 @@ func WithParentContext(parentContext reqContext.Context) RequestOption {
144146
return nil
145147
}
146148
}
149+
150+
//WithChaincodeFilter adds a chaincode filter for figuring out additional endorsers
151+
func WithChaincodeFilter(ccFilter invoke.CCFilter) RequestOption {
152+
return func(ctx context.Client, o *requestOptions) error {
153+
o.CCFilter = ccFilter
154+
return nil
155+
}
156+
}

pkg/client/channel/invoke/api.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import (
1818
pb "github.com/hyperledger/fabric-sdk-go/third_party/github.com/hyperledger/fabric/protos/peer"
1919
)
2020

21+
// CCFilter returns true if the given chaincode should be included
22+
// in the invocation chain when computing endorsers.
23+
type CCFilter func(ccID string) bool
24+
2125
// Opts allows the user to specify more advanced options
2226
type Opts struct {
2327
Targets []fab.Peer // targets
@@ -26,6 +30,7 @@ type Opts struct {
2630
BeforeRetry retry.BeforeRetryHandler
2731
Timeouts map[fab.TimeoutType]time.Duration
2832
ParentContext reqContext.Context //parent grpc context
33+
CCFilter CCFilter
2934
}
3035

3136
// Request contains the parameters to execute transaction
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/*
2+
Copyright SecureKey Technologies Inc. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package invoke
8+
9+
import (
10+
selectopts "github.com/hyperledger/fabric-sdk-go/pkg/client/common/selection/options"
11+
"github.com/hyperledger/fabric-sdk-go/pkg/common/options"
12+
"github.com/hyperledger/fabric-sdk-go/pkg/common/providers/fab"
13+
"github.com/hyperledger/fabric-sdk-go/pkg/fab/peer"
14+
"github.com/pkg/errors"
15+
16+
"github.com/golang/protobuf/proto"
17+
"github.com/hyperledger/fabric-sdk-go/pkg/common/logging"
18+
"github.com/hyperledger/fabric-sdk-go/third_party/github.com/hyperledger/fabric/core/ledger/kvledger/txmgmt/rwsetutil"
19+
pb "github.com/hyperledger/fabric-sdk-go/third_party/github.com/hyperledger/fabric/protos/peer"
20+
)
21+
22+
var logger = logging.NewLogger("fabsdk/client")
23+
24+
var lsccFilter = func(ccID string) bool {
25+
return ccID != "lscc"
26+
}
27+
28+
// SelectAndEndorseHandler selects endorsers according to the policies of the chaincodes in the provided invocation chain
29+
// and then sends the proposal to those endorsers. The read/write sets from the responses are then checked to see if additional
30+
// chaincodes were invoked that were not in the original invocation chain. If so, a new endorser set is computed with the
31+
// additional chaincodes and (if necessary) endorsements are requested from those additional endorsers.
32+
type SelectAndEndorseHandler struct {
33+
*EndorsementHandler
34+
next Handler
35+
}
36+
37+
// NewSelectAndEndorseHandler returns a new SelectAndEndorseHandler
38+
func NewSelectAndEndorseHandler(next ...Handler) Handler {
39+
return &SelectAndEndorseHandler{
40+
EndorsementHandler: NewEndorsementHandler(),
41+
next: getNext(next),
42+
}
43+
}
44+
45+
// Handle selects endorsers and sends proposals to the endorsers
46+
func (e *SelectAndEndorseHandler) Handle(requestContext *RequestContext, clientContext *ClientContext) {
47+
var ccCalls []*fab.ChaincodeCall
48+
targets := requestContext.Opts.Targets
49+
if len(targets) == 0 {
50+
var err error
51+
ccCalls, requestContext.Opts.Targets, err = getEndorsers(requestContext, clientContext)
52+
if err != nil {
53+
requestContext.Error = err
54+
return
55+
}
56+
}
57+
58+
e.EndorsementHandler.Handle(requestContext, clientContext)
59+
60+
if requestContext.Error != nil {
61+
return
62+
}
63+
64+
if len(targets) == 0 && len(requestContext.Response.Responses) > 0 {
65+
additionalEndorsers, err := getAdditionalEndorsers(requestContext, clientContext, ccCalls)
66+
if err != nil {
67+
requestContext.Error = errors.WithMessage(err, "error getting additional endorsers")
68+
return
69+
}
70+
71+
if len(additionalEndorsers) > 0 {
72+
requestContext.Opts.Targets = additionalEndorsers
73+
logger.Debugf("...getting additional endorsements from %d target(s)", len(additionalEndorsers))
74+
additionalResponses, err := clientContext.Transactor.SendTransactionProposal(requestContext.Response.Proposal, peer.PeersToTxnProcessors(additionalEndorsers))
75+
if err != nil {
76+
requestContext.Error = errors.WithMessage(err, "error sending transaction proposal")
77+
return
78+
}
79+
80+
// Add the new endorsements to the list of responses
81+
requestContext.Response.Responses = append(requestContext.Response.Responses, additionalResponses...)
82+
} else {
83+
logger.Debugf("...no additional endorsements are required.")
84+
}
85+
}
86+
87+
if e.next != nil {
88+
e.next.Handle(requestContext, clientContext)
89+
}
90+
}
91+
92+
//NewChainedCCFilter returns a chaincode filter that chains
93+
//multiple filters together. False is returned if at least one
94+
//of the filters in the chain returns false.
95+
func NewChainedCCFilter(filters ...CCFilter) CCFilter {
96+
return func(ccID string) bool {
97+
for _, filter := range filters {
98+
if !filter(ccID) {
99+
return false
100+
}
101+
}
102+
return true
103+
}
104+
}
105+
106+
func getEndorsers(requestContext *RequestContext, clientContext *ClientContext) (ccCalls []*fab.ChaincodeCall, peers []fab.Peer, err error) {
107+
var selectionOpts []options.Opt
108+
if requestContext.SelectionFilter != nil {
109+
selectionOpts = append(selectionOpts, selectopts.WithPeerFilter(requestContext.SelectionFilter))
110+
}
111+
112+
ccCalls = newChaincodeCalls(requestContext.Request)
113+
peers, err = clientContext.Selection.GetEndorsersForChaincode(ccCalls, selectionOpts...)
114+
return
115+
}
116+
117+
func getAdditionalEndorsers(requestContext *RequestContext, clientContext *ClientContext, ccCalls []*fab.ChaincodeCall) ([]fab.Peer, error) {
118+
ccIDs, err := getChaincodes(requestContext.Response.Responses[0])
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
additionalCalls := getAdditionalCalls(ccCalls, ccIDs, getCCFilter(requestContext))
124+
if len(additionalCalls) == 0 {
125+
return nil, nil
126+
}
127+
128+
logger.Debugf("Checking if additional endorsements are required...")
129+
requestContext.Request.InvocationChain = append(requestContext.Request.InvocationChain, additionalCalls...)
130+
131+
_, endorsers, err := getEndorsers(requestContext, clientContext)
132+
if err != nil {
133+
return nil, err
134+
}
135+
136+
var additionalEndorsers []fab.Peer
137+
for _, endorser := range endorsers {
138+
if !containsMSP(requestContext.Opts.Targets, endorser.MSPID()) {
139+
logger.Debugf("Will ask for additional endorsement from [%s] in order to satisfy the chaincode policy", endorser.URL())
140+
additionalEndorsers = append(additionalEndorsers, endorser)
141+
}
142+
}
143+
return additionalEndorsers, nil
144+
}
145+
146+
func getCCFilter(requestContext *RequestContext) CCFilter {
147+
if requestContext.Opts.CCFilter != nil {
148+
return NewChainedCCFilter(lsccFilter, requestContext.Opts.CCFilter)
149+
}
150+
return lsccFilter
151+
}
152+
153+
func containsMSP(peers []fab.Peer, mspID string) bool {
154+
for _, p := range peers {
155+
if p.MSPID() == mspID {
156+
return true
157+
}
158+
}
159+
return false
160+
}
161+
162+
func getChaincodes(response *fab.TransactionProposalResponse) ([]string, error) {
163+
rwSets, err := getRWSetsFromProposalResponse(response.ProposalResponse)
164+
if err != nil {
165+
return nil, err
166+
}
167+
return getNamespaces(rwSets), nil
168+
}
169+
170+
func getRWSetsFromProposalResponse(response *pb.ProposalResponse) ([]*rwsetutil.NsRwSet, error) {
171+
if response == nil {
172+
return nil, nil
173+
}
174+
175+
prp := &pb.ProposalResponsePayload{}
176+
err := proto.Unmarshal(response.Payload, prp)
177+
if err != nil {
178+
return nil, err
179+
}
180+
181+
chaincodeAction := &pb.ChaincodeAction{}
182+
err = proto.Unmarshal(prp.Extension, chaincodeAction)
183+
if err != nil {
184+
return nil, err
185+
}
186+
187+
if len(chaincodeAction.Results) == 0 {
188+
return nil, nil
189+
}
190+
191+
txRWSet := &rwsetutil.TxRwSet{}
192+
if err := txRWSet.FromProtoBytes(chaincodeAction.Results); err != nil {
193+
return nil, err
194+
}
195+
196+
return txRWSet.NsRwSets, nil
197+
}
198+
199+
func getNamespaces(rwSets []*rwsetutil.NsRwSet) []string {
200+
namespaceMap := make(map[string]bool)
201+
for _, rwSet := range rwSets {
202+
namespaceMap[rwSet.NameSpace] = true
203+
}
204+
205+
var namespaces []string
206+
for ns := range namespaceMap {
207+
namespaces = append(namespaces, ns)
208+
}
209+
return namespaces
210+
}
211+
212+
func getAdditionalCalls(ccCalls []*fab.ChaincodeCall, namespaces []string, filter CCFilter) []*fab.ChaincodeCall {
213+
var additionalCalls []*fab.ChaincodeCall
214+
for _, ccID := range namespaces {
215+
if !filter(ccID) {
216+
logger.Debugf("Ignoring chaincode [%s] in the RW set since it was filtered out", ccID)
217+
continue
218+
}
219+
if !containsCC(ccCalls, ccID) {
220+
logger.Debugf("Found additional chaincode [%s] in the RW set that was not part of the original invocation chain", ccID)
221+
additionalCalls = append(additionalCalls, &fab.ChaincodeCall{ID: ccID})
222+
}
223+
}
224+
return additionalCalls
225+
}
226+
227+
func containsCC(ccCalls []*fab.ChaincodeCall, ccID string) bool {
228+
for _, ccCall := range ccCalls {
229+
if ccCall.ID == ccID {
230+
return true
231+
}
232+
}
233+
return false
234+
}

pkg/client/channel/invoke/txnhandler.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,9 @@ func NewQueryHandler(next ...Handler) Handler {
196196

197197
//NewExecuteHandler returns query handler with EndorseTxHandler, EndorsementValidationHandler & CommitTxHandler Chained
198198
func NewExecuteHandler(next ...Handler) Handler {
199-
return NewProposalProcessorHandler(
200-
NewEndorsementHandler(
201-
NewEndorsementValidationHandler(
202-
NewSignatureValidationHandler(NewCommitHandler(next...)),
203-
),
199+
return NewSelectAndEndorseHandler(
200+
NewEndorsementValidationHandler(
201+
NewSignatureValidationHandler(NewCommitHandler(next...)),
204202
),
205203
)
206204
}

pkg/client/channel/invoke/txnhandler_test.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,26 @@ func TestQueryHandlerSuccess(t *testing.T) {
5656
}
5757

5858
func TestExecuteTxHandlerSuccess(t *testing.T) {
59+
ccID1 := "test"
60+
ccID2 := "invokedcc"
61+
ccID3 := "lscc"
62+
ccID4 := "somescc"
63+
5964
//Sample request
60-
request := Request{ChaincodeID: "test", Fcn: "invoke", Args: [][]byte{[]byte("move"), []byte("a"), []byte("b"), []byte("1")}}
65+
request := Request{ChaincodeID: ccID1, Fcn: "invoke", Args: [][]byte{[]byte("move"), []byte("a"), []byte("b"), []byte("1")}}
66+
67+
// Add a chaincode filter that will ignore ccID4 when examining the RWSet
68+
ccFilter := func(ccID string) bool {
69+
return ccID != ccID4
70+
}
6171

6272
//Prepare context objects for handler
63-
requestContext := prepareRequestContext(request, Opts{}, t)
73+
requestContext := prepareRequestContext(request, Opts{CCFilter: ccFilter}, t)
6474

6575
mockPeer1 := &fcmocks.MockPeer{MockName: "Peer1", MockURL: "http://peer1.com", MockRoles: []string{}, MockCert: nil, MockMSP: "Org1MSP", Status: 200, Payload: []byte("value")}
76+
mockPeer1.SetRwSets(fcmocks.NewRwSet(ccID1), fcmocks.NewRwSet(ccID2), fcmocks.NewRwSet(ccID3), fcmocks.NewRwSet(ccID4))
6677
mockPeer2 := &fcmocks.MockPeer{MockName: "Peer2", MockURL: "http://peer2.com", MockRoles: []string{}, MockCert: nil, MockMSP: "Org1MSP", Status: 200, Payload: []byte("value")}
78+
mockPeer2.SetRwSets(mockPeer1.RwSets...)
6779

6880
clientContext := setupChannelClientContext(nil, nil, []fab.Peer{mockPeer1, mockPeer2}, t)
6981

pkg/client/common/selection/fabricselection/fabricselection.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ func (s *Service) query(req *discclient.Request, chaincodes []*fab.ChaincodeCall
224224
return chResp, nil
225225
}
226226

227-
logger.Warn(lastErr.Error())
227+
logger.Debug(lastErr.Error())
228228

229229
if strings.Contains(lastErr.Error(), "failed constructing descriptor for chaincodes") {
230230
errMsg := fmt.Sprintf("error received from Discovery Server: %s", lastErr)

0 commit comments

Comments
 (0)