github.com/hechain20/hechain@v0.0.0-20220316014945-b544036ba106/internal/pkg/gateway/api.go (about)

     1  /*
     2  Copyright 2021 IBM All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package gateway
     8  
     9  import (
    10  	"context"
    11  	"fmt"
    12  	"io"
    13  	"math/rand"
    14  	"strings"
    15  
    16  	"github.com/golang/protobuf/proto"
    17  	"github.com/hechain20/hechain/common/flogging"
    18  	"github.com/hechain20/hechain/common/ledger"
    19  	"github.com/hechain20/hechain/core/aclmgmt/resources"
    20  	"github.com/hechain20/hechain/core/chaincode"
    21  	"github.com/hechain20/hechain/internal/pkg/gateway/event"
    22  	"github.com/hechain20/hechain/protoutil"
    23  	"github.com/hyperledger/fabric-protos-go/common"
    24  	gp "github.com/hyperledger/fabric-protos-go/gateway"
    25  	ab "github.com/hyperledger/fabric-protos-go/orderer"
    26  	"github.com/hyperledger/fabric-protos-go/peer"
    27  	"google.golang.org/grpc/codes"
    28  	"google.golang.org/grpc/status"
    29  )
    30  
    31  type endorserResponse struct {
    32  	action         *peer.ChaincodeEndorsedAction
    33  	err            *gp.ErrorDetail
    34  	timeoutExpired bool
    35  }
    36  
    37  // Evaluate will invoke the transaction function as specified in the SignedProposal
    38  func (gs *Server) Evaluate(ctx context.Context, request *gp.EvaluateRequest) (*gp.EvaluateResponse, error) {
    39  	if request == nil {
    40  		return nil, status.Error(codes.InvalidArgument, "an evaluate request is required")
    41  	}
    42  	signedProposal := request.GetProposedTransaction()
    43  	channel, chaincodeID, hasTransientData, err := getChannelAndChaincodeFromSignedProposal(signedProposal)
    44  	if err != nil {
    45  		return nil, status.Errorf(codes.InvalidArgument, "failed to unpack transaction proposal: %s", err)
    46  	}
    47  
    48  	err = gs.registry.connectChannelPeers(channel, false)
    49  	if err != nil {
    50  		return nil, status.Errorf(codes.Unavailable, "%s", err)
    51  	}
    52  
    53  	targetOrgs := request.GetTargetOrganizations()
    54  	transientProtected := false
    55  	if hasTransientData && targetOrgs == nil {
    56  		targetOrgs = []string{gs.registry.localEndorser.mspid}
    57  		transientProtected = true
    58  	}
    59  
    60  	plan, err := gs.registry.evaluator(channel, chaincodeID, targetOrgs)
    61  	if err != nil {
    62  		if transientProtected {
    63  			return nil, status.Errorf(codes.FailedPrecondition, "no endorsers found in the gateway's organization; retry specifying target organization(s) to protect transient data: %s", err)
    64  		}
    65  		return nil, status.Errorf(codes.FailedPrecondition, "%s", err)
    66  	}
    67  
    68  	endorser := plan.endorsers()[0]
    69  	var response *peer.Response
    70  	var errDetails []proto.Message
    71  	for response == nil {
    72  		gs.logger.Debugw("Sending to peer:", "channel", channel, "chaincode", chaincodeID, "txID", request.TransactionId, "MSPID", endorser.mspid, "endpoint", endorser.address)
    73  
    74  		done := make(chan error)
    75  		go func() {
    76  			defer close(done)
    77  			ctx, cancel := context.WithTimeout(ctx, gs.options.EndorsementTimeout)
    78  			defer cancel()
    79  			pr, err := endorser.client.ProcessProposal(ctx, signedProposal)
    80  			code, message, retry, remove := responseStatus(pr, err)
    81  			if code == codes.OK {
    82  				response = pr.Response
    83  				// Prefer result from proposal response as Response.Payload is not required to be transaction result
    84  				if result, err := getResultFromProposalResponse(pr); err == nil {
    85  					response.Payload = result
    86  				} else {
    87  					logger.Warnw("Successful proposal response contained no transaction result", "error", err.Error(), "chaincode", chaincodeID, "channel", channel, "txID", request.TransactionId, "endorserAddress", endorser.endpointConfig.address, "endorserMspid", endorser.endpointConfig.mspid, "status", response.Status, "message", response.Message)
    88  				}
    89  			} else {
    90  				logger.Debugw("Evaluate call to endorser failed", "chaincode", chaincodeID, "channel", channel, "txID", request.TransactionId, "endorserAddress", endorser.endpointConfig.address, "endorserMspid", endorser.endpointConfig.mspid, "error", message)
    91  				errDetails = append(errDetails, errorDetail(endorser.endpointConfig, message))
    92  				if remove {
    93  					gs.registry.removeEndorser(endorser)
    94  				}
    95  				if retry {
    96  					endorser = plan.nextPeerInGroup(endorser)
    97  				} else {
    98  					done <- newRpcError(code, "evaluate call to endorser returned error: "+message, errDetails...)
    99  				}
   100  				if endorser == nil {
   101  					done <- newRpcError(code, "failed to evaluate transaction, see attached details for more info", errDetails...)
   102  				}
   103  			}
   104  		}()
   105  		select {
   106  		case status := <-done:
   107  			if status != nil {
   108  				return nil, status
   109  			}
   110  		case <-ctx.Done():
   111  			// Overall evaluation timeout expired
   112  			logger.Warnw("Evaluate call timed out while processing request", "channel", request.ChannelId, "txID", request.TransactionId)
   113  			return nil, newRpcError(codes.DeadlineExceeded, "evaluate timeout expired")
   114  		}
   115  	}
   116  
   117  	evaluateResponse := &gp.EvaluateResponse{
   118  		Result: response,
   119  	}
   120  
   121  	logger.Debugw("Evaluate call to endorser returned success", "channel", request.ChannelId, "txID", request.TransactionId, "endorserAddress", endorser.endpointConfig.address, "endorserMspid", endorser.endpointConfig.mspid, "status", response.GetStatus(), "message", response.GetMessage())
   122  	return evaluateResponse, nil
   123  }
   124  
   125  // Endorse will collect endorsements by invoking the transaction function specified in the SignedProposal against
   126  // sufficient Peers to satisfy the endorsement policy.
   127  func (gs *Server) Endorse(ctx context.Context, request *gp.EndorseRequest) (*gp.EndorseResponse, error) {
   128  	if request == nil {
   129  		return nil, status.Error(codes.InvalidArgument, "an endorse request is required")
   130  	}
   131  	signedProposal := request.GetProposedTransaction()
   132  	if signedProposal == nil {
   133  		return nil, status.Error(codes.InvalidArgument, "the proposed transaction must contain a signed proposal")
   134  	}
   135  	proposal, err := protoutil.UnmarshalProposal(signedProposal.ProposalBytes)
   136  	if err != nil {
   137  		return nil, status.Error(codes.InvalidArgument, err.Error())
   138  	}
   139  	header, err := protoutil.UnmarshalHeader(proposal.Header)
   140  	if err != nil {
   141  		return nil, status.Error(codes.InvalidArgument, err.Error())
   142  	}
   143  	channelHeader, err := protoutil.UnmarshalChannelHeader(header.ChannelHeader)
   144  	if err != nil {
   145  		return nil, status.Error(codes.InvalidArgument, err.Error())
   146  	}
   147  	payload, err := protoutil.UnmarshalChaincodeProposalPayload(proposal.Payload)
   148  	if err != nil {
   149  		return nil, status.Error(codes.InvalidArgument, err.Error())
   150  	}
   151  	spec, err := protoutil.UnmarshalChaincodeInvocationSpec(payload.Input)
   152  	if err != nil {
   153  		return nil, status.Error(codes.InvalidArgument, err.Error())
   154  	}
   155  
   156  	channel := channelHeader.ChannelId
   157  	chaincodeID := spec.GetChaincodeSpec().GetChaincodeId().GetName()
   158  	hasTransientData := len(payload.GetTransientMap()) > 0
   159  
   160  	logger := gs.logger.With("channel", channel, "chaincode", chaincodeID, "txID", request.TransactionId)
   161  
   162  	var plan *plan
   163  	var action *peer.ChaincodeEndorsedAction
   164  	if len(request.EndorsingOrganizations) > 0 {
   165  		// The client is specifying the endorsing orgs and taking responsibility for ensuring it meets the signature policy
   166  		plan, err = gs.registry.planForOrgs(channel, chaincodeID, request.EndorsingOrganizations)
   167  		if err != nil {
   168  			return nil, status.Error(codes.Unavailable, err.Error())
   169  		}
   170  	} else {
   171  		// The client is delegating choice of endorsers to the gateway.
   172  		plan, err = gs.planFromFirstEndorser(ctx, channel, chaincodeID, hasTransientData, signedProposal, logger)
   173  		if err != nil {
   174  			return nil, err
   175  		}
   176  	}
   177  
   178  	for plan.completedLayout == nil {
   179  		// loop through the layouts until one gets satisfied
   180  		endorsers := plan.endorsers()
   181  		if endorsers == nil {
   182  			// no more layouts
   183  			break
   184  		}
   185  		// send to all the endorsers
   186  		waitCh := make(chan bool, len(endorsers))
   187  		for _, e := range endorsers {
   188  			go func(e *endorser) {
   189  				for e != nil {
   190  					if gs.processProposal(ctx, plan, e, signedProposal, logger) {
   191  						break
   192  					}
   193  					e = plan.nextPeerInGroup(e)
   194  				}
   195  				waitCh <- true
   196  			}(e)
   197  		}
   198  		for i := 0; i < len(endorsers); i++ {
   199  			select {
   200  			case <-waitCh:
   201  				// Endorser completedLayout normally
   202  			case <-ctx.Done():
   203  				logger.Warnw("Endorse call timed out while collecting endorsements", "numEndorsers", len(endorsers))
   204  				return nil, newRpcError(codes.DeadlineExceeded, "endorsement timeout expired while collecting endorsements")
   205  			}
   206  		}
   207  
   208  	}
   209  
   210  	if plan.completedLayout == nil {
   211  		return nil, newRpcError(codes.Aborted, "failed to collect enough transaction endorsements, see attached details for more info", plan.errorDetails...)
   212  	}
   213  
   214  	action = &peer.ChaincodeEndorsedAction{ProposalResponsePayload: plan.responsePayload, Endorsements: uniqueEndorsements(plan.completedLayout.endorsements)}
   215  
   216  	preparedTransaction, err := prepareTransaction(header, payload, action)
   217  	if err != nil {
   218  		return nil, status.Errorf(codes.Aborted, "failed to assemble transaction: %s", err)
   219  	}
   220  
   221  	return &gp.EndorseResponse{PreparedTransaction: preparedTransaction}, nil
   222  }
   223  
   224  type ppResponse struct {
   225  	response *peer.ProposalResponse
   226  	err      error
   227  }
   228  
   229  // processProposal will invoke the given endorsing peer to process the signed proposal, and will update the plan accordingly.
   230  // This function will timeout and return false if the given context timeout or the EndorsementTimeout option expires.
   231  // Returns boolean true if the endorsement was successful.
   232  func (gs *Server) processProposal(ctx context.Context, plan *plan, endorser *endorser, signedProposal *peer.SignedProposal, logger *flogging.FabricLogger) bool {
   233  	var response *peer.ProposalResponse
   234  	done := make(chan *ppResponse)
   235  	go func() {
   236  		defer close(done)
   237  		logger.Debugw("Sending to endorser:", "MSPID", endorser.mspid, "endpoint", endorser.address)
   238  		ctx, cancel := context.WithTimeout(ctx, gs.options.EndorsementTimeout) // timeout of individual endorsement
   239  		defer cancel()
   240  		response, err := endorser.client.ProcessProposal(ctx, signedProposal)
   241  		done <- &ppResponse{response: response, err: err}
   242  	}()
   243  	select {
   244  	case resp := <-done:
   245  		// Endorser completedLayout normally
   246  		code, message, _, remove := responseStatus(resp.response, resp.err)
   247  		if code != codes.OK {
   248  			logger.Warnw("Endorse call to endorser failed", "MSPID", endorser.mspid, "endpoint", endorser.address, "error", message)
   249  			if remove {
   250  				gs.registry.removeEndorser(endorser)
   251  			}
   252  			plan.addError(errorDetail(endorser.endpointConfig, message))
   253  			return false
   254  		}
   255  		response = resp.response
   256  		logger.Debugw("Endorse call to endorser returned success", "MSPID", endorser.mspid, "endpoint", endorser.address, "status", response.Response.Status, "message", response.Response.Message)
   257  
   258  		responseMessage := response.GetResponse()
   259  		if responseMessage != nil {
   260  			responseMessage.Payload = nil // Remove any duplicate response payload
   261  		}
   262  
   263  		return plan.processEndorsement(endorser, response)
   264  	case <-ctx.Done():
   265  		// Overall endorsement timeout expired
   266  		return false
   267  	}
   268  }
   269  
   270  // planFromFirstEndorser implements the gateway's strategy of processing the proposal on a single (preferably local) peer
   271  // and using the ChaincodeInterest from the response to invoke discovery and build an endorsement plan.
   272  // Returns the endorsement plan which can be used to request further endorsements, if required.
   273  func (gs *Server) planFromFirstEndorser(ctx context.Context, channel string, chaincodeID string, hasTransientData bool, signedProposal *peer.SignedProposal, logger *flogging.FabricLogger) (*plan, error) {
   274  	defaultInterest := &peer.ChaincodeInterest{
   275  		Chaincodes: []*peer.ChaincodeCall{{
   276  			Name: chaincodeID,
   277  		}},
   278  	}
   279  
   280  	// 1. Choose an endorser from the gateway's organization
   281  	plan, err := gs.registry.planForOrgs(channel, chaincodeID, []string{gs.registry.localEndorser.mspid})
   282  	if err != nil {
   283  		// No local org endorsers for this channel/chaincode. If transient data is involved, return error
   284  		if hasTransientData {
   285  			return nil, status.Error(codes.FailedPrecondition, "no endorsers found in the gateway's organization; retry specifying endorsing organization(s) to protect transient data")
   286  		}
   287  		// Otherwise, just let discovery pick one.
   288  		plan, err = gs.registry.endorsementPlan(channel, defaultInterest, nil)
   289  		if err != nil {
   290  			return nil, status.Error(codes.FailedPrecondition, err.Error())
   291  		}
   292  	}
   293  	firstEndorser := plan.endorsers()[0]
   294  
   295  	gs.logger.Debugw("Sending to first endorser:", "MSPID", firstEndorser.mspid, "endpoint", firstEndorser.address)
   296  
   297  	// 2. Process the proposal on this endorser
   298  	var firstResponse *peer.ProposalResponse
   299  	var errDetails []proto.Message
   300  
   301  	for firstResponse == nil && firstEndorser != nil {
   302  		done := make(chan struct{})
   303  		go func() {
   304  			defer close(done)
   305  
   306  			ctx, cancel := context.WithTimeout(ctx, gs.options.EndorsementTimeout)
   307  			defer cancel()
   308  			firstResponse, err = firstEndorser.client.ProcessProposal(ctx, signedProposal)
   309  			code, message, _, remove := responseStatus(firstResponse, err)
   310  
   311  			if code != codes.OK {
   312  				logger.Warnw("Endorse call to endorser failed", "endorserAddress", firstEndorser.address, "endorserMspid", firstEndorser.mspid, "error", message)
   313  				errDetails = append(errDetails, errorDetail(firstEndorser.endpointConfig, message))
   314  				if remove {
   315  					gs.registry.removeEndorser(firstEndorser)
   316  				}
   317  				firstEndorser = plan.nextPeerInGroup(firstEndorser)
   318  				firstResponse = nil
   319  			}
   320  		}()
   321  		select {
   322  		case <-done:
   323  			// Endorser completedLayout normally
   324  		case <-ctx.Done():
   325  			// Overall endorsement timeout expired
   326  			logger.Warn("Endorse call timed out while collecting first endorsement")
   327  			return nil, newRpcError(codes.DeadlineExceeded, "endorsement timeout expired while collecting first endorsement")
   328  		}
   329  	}
   330  	if firstEndorser == nil || firstResponse == nil {
   331  		return nil, newRpcError(codes.Aborted, "failed to endorse transaction, see attached details for more info", errDetails...)
   332  	}
   333  
   334  	// 3. Extract ChaincodeInterest and SBE policies
   335  	// The chaincode interest could be nil for legacy peers and for chaincode functions that don't produce a read-write set
   336  	interest := firstResponse.Interest
   337  	if len(interest.GetChaincodes()) == 0 {
   338  		interest = defaultInterest
   339  	}
   340  
   341  	// 4. If transient data is involved, then we need to ensure that discovery only returns orgs which own the collections involved.
   342  	// Do this by setting NoPrivateReads to false on each collection
   343  	if hasTransientData {
   344  		for _, call := range interest.GetChaincodes() {
   345  			call.NoPrivateReads = false
   346  		}
   347  	}
   348  
   349  	// 5. Get a set of endorsers from discovery via the registry
   350  	// The preferred discovery layout will contain the firstEndorser's Org.
   351  	plan, err = gs.registry.endorsementPlan(channel, interest, firstEndorser)
   352  	if err != nil {
   353  		return nil, status.Error(codes.FailedPrecondition, err.Error())
   354  	}
   355  
   356  	// 6. Remove the gateway org's endorser, since we've already done that
   357  	plan.processEndorsement(firstEndorser, firstResponse)
   358  
   359  	return plan, nil
   360  }
   361  
   362  // responseStatus unpacks the proposal response and error values that are returned from ProcessProposal and
   363  // determines how the gateway should react (retry?, close connection?).
   364  // Uses the grpc canonical status error codes and their recommended actions.
   365  // Returns:
   366  // - response status code, with codes.OK indicating success and other values indicating likely error type
   367  // - error message extracted from the err or generated from 500 proposal response (string)
   368  // - should the gateway retry (only the Evaluate() uses this) (bool)
   369  // - should the gateway close the connection and remove the peer from its registry (bool)
   370  func responseStatus(response *peer.ProposalResponse, err error) (statusCode codes.Code, message string, retry bool, remove bool) {
   371  	if err != nil {
   372  		if response == nil {
   373  			// there is no ProposalResponse, so this must have been generated by grpc in response to an unavailable peer
   374  			// - close the connection and retry on another
   375  			return codes.Unavailable, err.Error(), true, true
   376  		}
   377  		// there is a response and an err, so it must have been from the unpackProposal() or preProcess() stages
   378  		// preProcess does all the signature and ACL checking. In either case, no point retrying, or closing the connection (it's a client error)
   379  		return codes.FailedPrecondition, err.Error(), false, false
   380  	}
   381  	if response.Response.Status < 200 || response.Response.Status >= 400 {
   382  		if response.Payload == nil && response.Response.Status == 500 {
   383  			// there's a error 500 response but no payload, so the response was generated in the peer rather than the chaincode
   384  			if strings.HasSuffix(response.Response.Message, chaincode.ErrorStreamTerminated) {
   385  				// chaincode container crashed probably. Close connection and retry on another peer
   386  				return codes.Aborted, response.Response.Message, true, true
   387  			}
   388  			// some other error - retry on another peer
   389  			return codes.Aborted, response.Response.Message, true, false
   390  		} else {
   391  			// otherwise it must be an error response generated by the chaincode
   392  			return codes.Unknown, fmt.Sprintf("chaincode response %d, %s", response.Response.Status, response.Response.Message), false, false
   393  		}
   394  	}
   395  	// anything else is a success
   396  	return codes.OK, "", false, false
   397  }
   398  
   399  // Submit will send the signed transaction to the ordering service. The response indicates whether the transaction was
   400  // successfully received by the orderer. This does not imply successful commit of the transaction, only that is has
   401  // been delivered to the orderer.
   402  func (gs *Server) Submit(ctx context.Context, request *gp.SubmitRequest) (*gp.SubmitResponse, error) {
   403  	if request == nil {
   404  		return nil, status.Error(codes.InvalidArgument, "a submit request is required")
   405  	}
   406  	txn := request.GetPreparedTransaction()
   407  	if txn == nil {
   408  		return nil, status.Error(codes.InvalidArgument, "a prepared transaction is required")
   409  	}
   410  	if len(txn.Signature) == 0 {
   411  		return nil, status.Error(codes.InvalidArgument, "prepared transaction must be signed")
   412  	}
   413  	orderers, err := gs.registry.orderers(request.ChannelId)
   414  	if err != nil {
   415  		return nil, status.Errorf(codes.FailedPrecondition, "%s", err)
   416  	}
   417  
   418  	if len(orderers) == 0 {
   419  		return nil, status.Errorf(codes.Unavailable, "no orderer nodes available")
   420  	}
   421  
   422  	// try each orderer in random order
   423  	var errDetails []proto.Message
   424  	for _, index := range rand.Perm(len(orderers)) {
   425  		orderer := orderers[index]
   426  		logger.Infow("Sending transaction to orderer", "txID", request.TransactionId, "endpoint", orderer.address)
   427  		err := gs.broadcast(ctx, orderer, txn)
   428  		if err == nil {
   429  			return &gp.SubmitResponse{}, nil
   430  		}
   431  
   432  		logger.Warnw("Error sending transaction to orderer", "txID", request.TransactionId, "endpoint", orderer.address, "err", err)
   433  		errDetails = append(errDetails, errorDetail(orderer.endpointConfig, err.Error()))
   434  
   435  		errStatus := toRpcStatus(err)
   436  		if errStatus.Code() != codes.Unavailable {
   437  			return nil, newRpcError(errStatus.Code(), errStatus.Message(), errDetails...)
   438  		}
   439  	}
   440  
   441  	return nil, newRpcError(codes.Unavailable, "no orderers could successfully process transaction", errDetails...)
   442  }
   443  
   444  func (gs *Server) broadcast(ctx context.Context, orderer *orderer, txn *common.Envelope) error {
   445  	broadcast, err := orderer.client.Broadcast(ctx)
   446  	if err != nil {
   447  		return err
   448  	}
   449  
   450  	if err := broadcast.Send(txn); err != nil {
   451  		return err
   452  	}
   453  
   454  	response, err := broadcast.Recv()
   455  	if err != nil {
   456  		return err
   457  	}
   458  
   459  	if response.GetStatus() != common.Status_SUCCESS {
   460  		return status.Errorf(codes.Aborted, "received unsuccessful response from orderer: %s", common.Status_name[int32(response.GetStatus())])
   461  	}
   462  
   463  	return nil
   464  }
   465  
   466  // CommitStatus returns the validation code for a specific transaction on a specific channel. If the transaction is
   467  // already committed, the status will be returned immediately; otherwise this call will block and return only when
   468  // the transaction commits or the context is cancelled.
   469  //
   470  // If the transaction commit status cannot be returned, for example if the specified channel does not exist, a
   471  // FailedPrecondition error will be returned.
   472  func (gs *Server) CommitStatus(ctx context.Context, signedRequest *gp.SignedCommitStatusRequest) (*gp.CommitStatusResponse, error) {
   473  	if signedRequest == nil {
   474  		return nil, status.Error(codes.InvalidArgument, "a commit status request is required")
   475  	}
   476  
   477  	request := &gp.CommitStatusRequest{}
   478  	if err := proto.Unmarshal(signedRequest.Request, request); err != nil {
   479  		return nil, status.Errorf(codes.InvalidArgument, "invalid status request: %v", err)
   480  	}
   481  
   482  	signedData := &protoutil.SignedData{
   483  		Data:      signedRequest.Request,
   484  		Identity:  request.Identity,
   485  		Signature: signedRequest.Signature,
   486  	}
   487  	if err := gs.policy.CheckACL(resources.Gateway_CommitStatus, request.ChannelId, signedData); err != nil {
   488  		return nil, status.Error(codes.PermissionDenied, err.Error())
   489  	}
   490  
   491  	txStatus, err := gs.commitFinder.TransactionStatus(ctx, request.ChannelId, request.TransactionId)
   492  	if err != nil {
   493  		return nil, toRpcError(err, codes.Aborted)
   494  	}
   495  
   496  	response := &gp.CommitStatusResponse{
   497  		Result:      txStatus.Code,
   498  		BlockNumber: txStatus.BlockNumber,
   499  	}
   500  	return response, nil
   501  }
   502  
   503  // ChaincodeEvents supplies a stream of responses, each containing all the events emitted by the requested chaincode
   504  // for a specific block. The streamed responses are ordered by ascending block number. Responses are only returned for
   505  // blocks that contain the requested events, while blocks not containing any of the requested events are skipped. The
   506  // events within each response message are presented in the same order that the transactions that emitted them appear
   507  // within the block.
   508  func (gs *Server) ChaincodeEvents(signedRequest *gp.SignedChaincodeEventsRequest, stream gp.Gateway_ChaincodeEventsServer) error {
   509  	if signedRequest == nil {
   510  		return status.Error(codes.InvalidArgument, "a chaincode events request is required")
   511  	}
   512  
   513  	request := &gp.ChaincodeEventsRequest{}
   514  	if err := proto.Unmarshal(signedRequest.Request, request); err != nil {
   515  		return status.Errorf(codes.InvalidArgument, "invalid chaincode events request: %v", err)
   516  	}
   517  
   518  	signedData := &protoutil.SignedData{
   519  		Data:      signedRequest.Request,
   520  		Identity:  request.Identity,
   521  		Signature: signedRequest.Signature,
   522  	}
   523  	if err := gs.policy.CheckACL(resources.Gateway_ChaincodeEvents, request.ChannelId, signedData); err != nil {
   524  		return status.Error(codes.PermissionDenied, err.Error())
   525  	}
   526  
   527  	ledger, err := gs.ledgerProvider.Ledger(request.GetChannelId())
   528  	if err != nil {
   529  		return status.Error(codes.NotFound, err.Error())
   530  	}
   531  
   532  	startBlock, err := startBlockFromLedgerPosition(ledger, request.GetStartPosition())
   533  	if err != nil {
   534  		return err
   535  	}
   536  
   537  	ledgerIter, err := ledger.GetBlocksIterator(startBlock)
   538  	if err != nil {
   539  		return status.Error(codes.Aborted, err.Error())
   540  	}
   541  
   542  	eventsIter := event.NewChaincodeEventsIterator(ledgerIter)
   543  	defer eventsIter.Close()
   544  
   545  	for {
   546  		response, err := eventsIter.Next()
   547  		if err != nil {
   548  			return status.Error(codes.Aborted, err.Error())
   549  		}
   550  
   551  		var matchingEvents []*peer.ChaincodeEvent
   552  
   553  		for _, event := range response.Events {
   554  			if event.GetChaincodeId() == request.GetChaincodeId() {
   555  				matchingEvents = append(matchingEvents, event)
   556  			}
   557  		}
   558  
   559  		if len(matchingEvents) == 0 {
   560  			continue
   561  		}
   562  
   563  		response.Events = matchingEvents
   564  
   565  		if err := stream.Send(response); err != nil {
   566  			if err == io.EOF {
   567  				// Stream closed by the client
   568  				return status.Error(codes.Canceled, err.Error())
   569  			}
   570  			return err
   571  		}
   572  	}
   573  }
   574  
   575  func startBlockFromLedgerPosition(ledger ledger.Ledger, position *ab.SeekPosition) (uint64, error) {
   576  	switch seek := position.GetType().(type) {
   577  	case nil:
   578  	case *ab.SeekPosition_NextCommit:
   579  	case *ab.SeekPosition_Specified:
   580  		return seek.Specified.GetNumber(), nil
   581  	default:
   582  		return 0, status.Errorf(codes.InvalidArgument, "invalid start position type: %T", seek)
   583  	}
   584  
   585  	ledgerInfo, err := ledger.GetBlockchainInfo()
   586  	if err != nil {
   587  		return 0, status.Error(codes.Aborted, err.Error())
   588  	}
   589  
   590  	return ledgerInfo.GetHeight(), nil
   591  }