Skip to content

Commit

Permalink
[FAB-10667] Resolve additional endorsers for CC-to-CC
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
bstasyszyn committed Jun 21, 2018
1 parent 327a946 commit e529e89
Show file tree
Hide file tree
Showing 14 changed files with 592 additions and 112 deletions.
10 changes: 10 additions & 0 deletions pkg/client/channel/api.go
Expand Up @@ -10,6 +10,7 @@ import (
reqContext "context"
"time"

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

// RequestOption func for each Opts argument
Expand Down Expand Up @@ -144,3 +146,11 @@ func WithParentContext(parentContext reqContext.Context) RequestOption {
return nil
}
}

//WithChaincodeFilter adds a chaincode filter for figuring out additional endorsers
func WithChaincodeFilter(ccFilter invoke.CCFilter) RequestOption {
return func(ctx context.Client, o *requestOptions) error {
o.CCFilter = ccFilter
return nil
}
}
5 changes: 5 additions & 0 deletions pkg/client/channel/invoke/api.go
Expand Up @@ -18,6 +18,10 @@ import (
pb "github.com/hyperledger/fabric-sdk-go/third_party/github.com/hyperledger/fabric/protos/peer"
)

// CCFilter returns true if the given chaincode should be included
// in the invocation chain when computing endorsers.
type CCFilter func(ccID string) bool

// Opts allows the user to specify more advanced options
type Opts struct {
Targets []fab.Peer // targets
Expand All @@ -26,6 +30,7 @@ type Opts struct {
BeforeRetry retry.BeforeRetryHandler
Timeouts map[fab.TimeoutType]time.Duration
ParentContext reqContext.Context //parent grpc context
CCFilter CCFilter
}

// Request contains the parameters to execute transaction
Expand Down
234 changes: 234 additions & 0 deletions pkg/client/channel/invoke/selectendorsehandler.go
@@ -0,0 +1,234 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package invoke

import (
selectopts "github.com/hyperledger/fabric-sdk-go/pkg/client/common/selection/options"
"github.com/hyperledger/fabric-sdk-go/pkg/common/options"
"github.com/hyperledger/fabric-sdk-go/pkg/common/providers/fab"
"github.com/hyperledger/fabric-sdk-go/pkg/fab/peer"
"github.com/pkg/errors"

"github.com/golang/protobuf/proto"
"github.com/hyperledger/fabric-sdk-go/pkg/common/logging"
"github.com/hyperledger/fabric-sdk-go/third_party/github.com/hyperledger/fabric/core/ledger/kvledger/txmgmt/rwsetutil"
pb "github.com/hyperledger/fabric-sdk-go/third_party/github.com/hyperledger/fabric/protos/peer"
)

var logger = logging.NewLogger("fabsdk/client")

var lsccFilter = func(ccID string) bool {
return ccID != "lscc"
}

// SelectAndEndorseHandler selects endorsers according to the policies of the chaincodes in the provided invocation chain
// and then sends the proposal to those endorsers. The read/write sets from the responses are then checked to see if additional
// chaincodes were invoked that were not in the original invocation chain. If so, a new endorser set is computed with the
// additional chaincodes and (if necessary) endorsements are requested from those additional endorsers.
type SelectAndEndorseHandler struct {
*EndorsementHandler
next Handler
}

// NewSelectAndEndorseHandler returns a new SelectAndEndorseHandler
func NewSelectAndEndorseHandler(next ...Handler) Handler {
return &SelectAndEndorseHandler{
EndorsementHandler: NewEndorsementHandler(),
next: getNext(next),
}
}

// Handle selects endorsers and sends proposals to the endorsers
func (e *SelectAndEndorseHandler) Handle(requestContext *RequestContext, clientContext *ClientContext) {
var ccCalls []*fab.ChaincodeCall
targets := requestContext.Opts.Targets
if len(targets) == 0 {
var err error
ccCalls, requestContext.Opts.Targets, err = getEndorsers(requestContext, clientContext)
if err != nil {
requestContext.Error = err
return
}
}

e.EndorsementHandler.Handle(requestContext, clientContext)

if requestContext.Error != nil {
return
}

if len(targets) == 0 && len(requestContext.Response.Responses) > 0 {
additionalEndorsers, err := getAdditionalEndorsers(requestContext, clientContext, ccCalls)
if err != nil {
requestContext.Error = errors.WithMessage(err, "error getting additional endorsers")
return
}

if len(additionalEndorsers) > 0 {
requestContext.Opts.Targets = additionalEndorsers
logger.Debugf("...getting additional endorsements from %d target(s)", len(additionalEndorsers))
additionalResponses, err := clientContext.Transactor.SendTransactionProposal(requestContext.Response.Proposal, peer.PeersToTxnProcessors(additionalEndorsers))
if err != nil {
requestContext.Error = errors.WithMessage(err, "error sending transaction proposal")
return
}

// Add the new endorsements to the list of responses
requestContext.Response.Responses = append(requestContext.Response.Responses, additionalResponses...)
} else {
logger.Debugf("...no additional endorsements are required.")
}
}

if e.next != nil {
e.next.Handle(requestContext, clientContext)
}
}

//NewChainedCCFilter returns a chaincode filter that chains
//multiple filters together. False is returned if at least one
//of the filters in the chain returns false.
func NewChainedCCFilter(filters ...CCFilter) CCFilter {
return func(ccID string) bool {
for _, filter := range filters {
if !filter(ccID) {
return false
}
}
return true
}
}

func getEndorsers(requestContext *RequestContext, clientContext *ClientContext) (ccCalls []*fab.ChaincodeCall, peers []fab.Peer, err error) {
var selectionOpts []options.Opt
if requestContext.SelectionFilter != nil {
selectionOpts = append(selectionOpts, selectopts.WithPeerFilter(requestContext.SelectionFilter))
}

ccCalls = newChaincodeCalls(requestContext.Request)
peers, err = clientContext.Selection.GetEndorsersForChaincode(ccCalls, selectionOpts...)
return
}

func getAdditionalEndorsers(requestContext *RequestContext, clientContext *ClientContext, ccCalls []*fab.ChaincodeCall) ([]fab.Peer, error) {
ccIDs, err := getChaincodes(requestContext.Response.Responses[0])
if err != nil {
return nil, err
}

additionalCalls := getAdditionalCalls(ccCalls, ccIDs, getCCFilter(requestContext))
if len(additionalCalls) == 0 {
return nil, nil
}

logger.Debugf("Checking if additional endorsements are required...")
requestContext.Request.InvocationChain = append(requestContext.Request.InvocationChain, additionalCalls...)

_, endorsers, err := getEndorsers(requestContext, clientContext)
if err != nil {
return nil, err
}

var additionalEndorsers []fab.Peer
for _, endorser := range endorsers {
if !containsMSP(requestContext.Opts.Targets, endorser.MSPID()) {
logger.Debugf("Will ask for additional endorsement from [%s] in order to satisfy the chaincode policy", endorser.URL())
additionalEndorsers = append(additionalEndorsers, endorser)
}
}
return additionalEndorsers, nil
}

func getCCFilter(requestContext *RequestContext) CCFilter {
if requestContext.Opts.CCFilter != nil {
return NewChainedCCFilter(lsccFilter, requestContext.Opts.CCFilter)
}
return lsccFilter
}

func containsMSP(peers []fab.Peer, mspID string) bool {
for _, p := range peers {
if p.MSPID() == mspID {
return true
}
}
return false
}

func getChaincodes(response *fab.TransactionProposalResponse) ([]string, error) {
rwSets, err := getRWSetsFromProposalResponse(response.ProposalResponse)
if err != nil {
return nil, err
}
return getNamespaces(rwSets), nil
}

func getRWSetsFromProposalResponse(response *pb.ProposalResponse) ([]*rwsetutil.NsRwSet, error) {
if response == nil {
return nil, nil
}

prp := &pb.ProposalResponsePayload{}
err := proto.Unmarshal(response.Payload, prp)
if err != nil {
return nil, err
}

chaincodeAction := &pb.ChaincodeAction{}
err = proto.Unmarshal(prp.Extension, chaincodeAction)
if err != nil {
return nil, err
}

if len(chaincodeAction.Results) == 0 {
return nil, nil
}

txRWSet := &rwsetutil.TxRwSet{}
if err := txRWSet.FromProtoBytes(chaincodeAction.Results); err != nil {
return nil, err
}

return txRWSet.NsRwSets, nil
}

func getNamespaces(rwSets []*rwsetutil.NsRwSet) []string {
namespaceMap := make(map[string]bool)
for _, rwSet := range rwSets {
namespaceMap[rwSet.NameSpace] = true
}

var namespaces []string
for ns := range namespaceMap {
namespaces = append(namespaces, ns)
}
return namespaces
}

func getAdditionalCalls(ccCalls []*fab.ChaincodeCall, namespaces []string, filter CCFilter) []*fab.ChaincodeCall {
var additionalCalls []*fab.ChaincodeCall
for _, ccID := range namespaces {
if !filter(ccID) {
logger.Debugf("Ignoring chaincode [%s] in the RW set since it was filtered out", ccID)
continue
}
if !containsCC(ccCalls, ccID) {
logger.Debugf("Found additional chaincode [%s] in the RW set that was not part of the original invocation chain", ccID)
additionalCalls = append(additionalCalls, &fab.ChaincodeCall{ID: ccID})
}
}
return additionalCalls
}

func containsCC(ccCalls []*fab.ChaincodeCall, ccID string) bool {
for _, ccCall := range ccCalls {
if ccCall.ID == ccID {
return true
}
}
return false
}
8 changes: 3 additions & 5 deletions pkg/client/channel/invoke/txnhandler.go
Expand Up @@ -196,11 +196,9 @@ func NewQueryHandler(next ...Handler) Handler {

//NewExecuteHandler returns query handler with EndorseTxHandler, EndorsementValidationHandler & CommitTxHandler Chained
func NewExecuteHandler(next ...Handler) Handler {
return NewProposalProcessorHandler(
NewEndorsementHandler(
NewEndorsementValidationHandler(
NewSignatureValidationHandler(NewCommitHandler(next...)),
),
return NewSelectAndEndorseHandler(
NewEndorsementValidationHandler(
NewSignatureValidationHandler(NewCommitHandler(next...)),
),
)
}
Expand Down
16 changes: 14 additions & 2 deletions pkg/client/channel/invoke/txnhandler_test.go
Expand Up @@ -56,14 +56,26 @@ func TestQueryHandlerSuccess(t *testing.T) {
}

func TestExecuteTxHandlerSuccess(t *testing.T) {
ccID1 := "test"
ccID2 := "invokedcc"
ccID3 := "lscc"
ccID4 := "somescc"

//Sample request
request := Request{ChaincodeID: "test", Fcn: "invoke", Args: [][]byte{[]byte("move"), []byte("a"), []byte("b"), []byte("1")}}
request := Request{ChaincodeID: ccID1, Fcn: "invoke", Args: [][]byte{[]byte("move"), []byte("a"), []byte("b"), []byte("1")}}

// Add a chaincode filter that will ignore ccID4 when examining the RWSet
ccFilter := func(ccID string) bool {
return ccID != ccID4
}

//Prepare context objects for handler
requestContext := prepareRequestContext(request, Opts{}, t)
requestContext := prepareRequestContext(request, Opts{CCFilter: ccFilter}, t)

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

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

Expand Down
Expand Up @@ -224,7 +224,7 @@ func (s *Service) query(req *discclient.Request, chaincodes []*fab.ChaincodeCall
return chResp, nil
}

logger.Warn(lastErr.Error())
logger.Debug(lastErr.Error())

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

0 comments on commit e529e89

Please sign in to comment.