272 lines
11 KiB
Go
272 lines
11 KiB
Go
/*
|
|
Copyright 2021 IBM All Rights Reserved.
|
|
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/golang/protobuf/proto"
|
|
gp "github.com/hyperledger/fabric-protos-go/gateway"
|
|
"github.com/hyperledger/fabric-protos-go/peer"
|
|
"github.com/hyperledger/fabric/common/flogging"
|
|
"github.com/hyperledger/fabric/protoutil"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// Endorse will collect endorsements by invoking the transaction function specified in the SignedProposal against
|
|
// sufficient Peers to satisfy the endorsement policy.
|
|
func (gs *Server) Endorse(ctx context.Context, request *gp.EndorseRequest) (*gp.EndorseResponse, error) {
|
|
if request == nil {
|
|
return nil, status.Error(codes.InvalidArgument, "an endorse request is required")
|
|
}
|
|
signedProposal := request.GetProposedTransaction()
|
|
if len(signedProposal.GetProposalBytes()) == 0 {
|
|
return nil, status.Error(codes.InvalidArgument, "the proposed transaction must contain a signed proposal")
|
|
}
|
|
proposal, err := protoutil.UnmarshalProposal(signedProposal.GetProposalBytes())
|
|
if err != nil {
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
}
|
|
header, err := protoutil.UnmarshalHeader(proposal.GetHeader())
|
|
if err != nil {
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
}
|
|
channelHeader, err := protoutil.UnmarshalChannelHeader(header.GetChannelHeader())
|
|
if err != nil {
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
}
|
|
payload, err := protoutil.UnmarshalChaincodeProposalPayload(proposal.GetPayload())
|
|
if err != nil {
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
}
|
|
spec, err := protoutil.UnmarshalChaincodeInvocationSpec(payload.GetInput())
|
|
if err != nil {
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
}
|
|
|
|
channel := channelHeader.GetChannelId()
|
|
chaincodeID := spec.GetChaincodeSpec().GetChaincodeId().GetName()
|
|
hasTransientData := len(payload.GetTransientMap()) > 0
|
|
|
|
logger := gs.logger.With("channel", channel, "chaincode", chaincodeID, "txID", request.GetTransactionId())
|
|
|
|
var plan *plan
|
|
var action *peer.ChaincodeEndorsedAction
|
|
if len(request.GetEndorsingOrganizations()) > 0 {
|
|
// The client is specifying the endorsing orgs and taking responsibility for ensuring it meets the signature policy
|
|
plan, err = gs.registry.planForOrgs(channel, chaincodeID, request.GetEndorsingOrganizations())
|
|
if err != nil {
|
|
return nil, status.Error(codes.Unavailable, err.Error())
|
|
}
|
|
} else {
|
|
// The client is delegating choice of endorsers to the gateway.
|
|
plan, err = gs.planFromFirstEndorser(ctx, channel, chaincodeID, hasTransientData, signedProposal, logger)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
for plan.completedLayout == nil {
|
|
// loop through the layouts until one gets satisfied
|
|
endorsers := plan.endorsers()
|
|
if endorsers == nil {
|
|
// no more layouts
|
|
break
|
|
}
|
|
// send to all the endorsers
|
|
waitCh := make(chan bool, len(endorsers))
|
|
for _, e := range endorsers {
|
|
go func(e *endorser) {
|
|
for e != nil {
|
|
if gs.processProposal(ctx, plan, e, signedProposal, logger) {
|
|
break
|
|
}
|
|
e = plan.nextPeerInGroup(e)
|
|
}
|
|
waitCh <- true
|
|
}(e)
|
|
}
|
|
for i := 0; i < len(endorsers); i++ {
|
|
select {
|
|
case <-waitCh:
|
|
// Endorser completedLayout normally
|
|
case <-ctx.Done():
|
|
logger.Warnw("Endorse call timed out while collecting endorsements", "numEndorsers", len(endorsers))
|
|
return nil, newRpcError(codes.DeadlineExceeded, "endorsement timeout expired while collecting endorsements")
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if plan.completedLayout == nil {
|
|
return nil, newRpcError(codes.Aborted, "failed to collect enough transaction endorsements, see attached details for more info", plan.errorDetails...)
|
|
}
|
|
|
|
action = &peer.ChaincodeEndorsedAction{ProposalResponsePayload: plan.responsePayload, Endorsements: uniqueEndorsements(plan.completedLayout.endorsements)}
|
|
|
|
preparedTransaction, err := prepareTransaction(header, payload, action)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Aborted, "failed to assemble transaction: %s", err)
|
|
}
|
|
|
|
return &gp.EndorseResponse{PreparedTransaction: preparedTransaction}, nil
|
|
}
|
|
|
|
type ppResponse struct {
|
|
response *peer.ProposalResponse
|
|
err error
|
|
}
|
|
|
|
// processProposal will invoke the given endorsing peer to process the signed proposal, and will update the plan accordingly.
|
|
// This function will timeout and return false if the given context timeout or the EndorsementTimeout option expires.
|
|
// Returns boolean true if the endorsement was successful.
|
|
func (gs *Server) processProposal(ctx context.Context, plan *plan, endorser *endorser, signedProposal *peer.SignedProposal, logger *flogging.FabricLogger) bool {
|
|
var response *peer.ProposalResponse
|
|
done := make(chan *ppResponse)
|
|
go func() {
|
|
defer close(done)
|
|
logger.Debugw("Sending to endorser:", "MSPID", endorser.mspid, "endpoint", endorser.address)
|
|
ctx, cancel := context.WithTimeout(ctx, gs.options.EndorsementTimeout) // timeout of individual endorsement
|
|
defer cancel()
|
|
response, err := endorser.client.ProcessProposal(ctx, signedProposal)
|
|
done <- &ppResponse{response: response, err: err}
|
|
}()
|
|
select {
|
|
case resp := <-done:
|
|
// Endorser completedLayout normally
|
|
code, message, _, remove := responseStatus(resp.response, resp.err)
|
|
if code != codes.OK {
|
|
logger.Warnw("Endorse call to endorser failed", "MSPID", endorser.mspid, "endpoint", endorser.address, "error", message)
|
|
if remove {
|
|
gs.registry.removeEndorser(endorser)
|
|
}
|
|
plan.addError(errorDetail(endorser.endpointConfig, message))
|
|
return false
|
|
}
|
|
response = resp.response
|
|
logger.Debugw("Endorse call to endorser returned success", "MSPID", endorser.mspid, "endpoint", endorser.address, "status", response.Response.Status, "message", response.Response.Message)
|
|
|
|
responseMessage := response.GetResponse()
|
|
if responseMessage != nil {
|
|
responseMessage.Payload = nil // Remove any duplicate response payload
|
|
}
|
|
|
|
return plan.processEndorsement(endorser, response)
|
|
case <-ctx.Done():
|
|
// Overall endorsement timeout expired
|
|
return false
|
|
}
|
|
}
|
|
|
|
// planFromFirstEndorser implements the gateway's strategy of processing the proposal on a single (preferably local) peer
|
|
// and using the ChaincodeInterest from the response to invoke discovery and build an endorsement plan.
|
|
// Returns the endorsement plan which can be used to request further endorsements, if required.
|
|
func (gs *Server) planFromFirstEndorser(ctx context.Context, channel string, chaincodeID string, hasTransientData bool, signedProposal *peer.SignedProposal, logger *flogging.FabricLogger) (*plan, error) {
|
|
defaultInterest := &peer.ChaincodeInterest{
|
|
Chaincodes: []*peer.ChaincodeCall{{
|
|
Name: chaincodeID,
|
|
}},
|
|
}
|
|
|
|
// 1. Choose an endorser from the gateway's organization
|
|
plan, err := gs.registry.planForOrgs(channel, chaincodeID, []string{gs.registry.localEndorser.mspid})
|
|
if err != nil {
|
|
// No local org endorsers for this channel/chaincode. If transient data is involved, return error
|
|
if hasTransientData {
|
|
return nil, status.Error(codes.FailedPrecondition, "no endorsers found in the gateway's organization; retry specifying endorsing organization(s) to protect transient data")
|
|
}
|
|
// Otherwise, just let discovery pick one.
|
|
plan, err = gs.registry.endorsementPlan(channel, defaultInterest, nil)
|
|
if err != nil {
|
|
return nil, status.Error(codes.FailedPrecondition, err.Error())
|
|
}
|
|
}
|
|
firstEndorser := plan.endorsers()[0]
|
|
|
|
gs.logger.Debugw("Sending to first endorser:", "MSPID", firstEndorser.mspid, "endpoint", firstEndorser.address)
|
|
|
|
// 2. Process the proposal on this endorser
|
|
var firstResponse *peer.ProposalResponse
|
|
var errDetails []proto.Message
|
|
|
|
for firstResponse == nil && firstEndorser != nil {
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, gs.options.EndorsementTimeout)
|
|
defer cancel()
|
|
firstResponse, err = firstEndorser.client.ProcessProposal(ctx, signedProposal)
|
|
code, message, _, remove := responseStatus(firstResponse, err)
|
|
|
|
if code != codes.OK {
|
|
logger.Warnw("Endorse call to endorser failed", "endorserAddress", firstEndorser.address, "endorserMspid", firstEndorser.mspid, "error", message)
|
|
errDetails = append(errDetails, errorDetail(firstEndorser.endpointConfig, message))
|
|
if remove {
|
|
gs.registry.removeEndorser(firstEndorser)
|
|
}
|
|
firstEndorser = plan.nextPeerInGroup(firstEndorser)
|
|
firstResponse = nil
|
|
}
|
|
}()
|
|
select {
|
|
case <-done:
|
|
// Endorser completed normally
|
|
case <-ctx.Done():
|
|
// Overall endorsement timeout expired
|
|
logger.Warn("Endorse call timed out while collecting first endorsement")
|
|
return nil, newRpcError(codes.DeadlineExceeded, "endorsement timeout expired while collecting first endorsement")
|
|
}
|
|
}
|
|
if firstEndorser == nil || firstResponse == nil {
|
|
return nil, newRpcError(codes.Aborted, "failed to endorse transaction, see attached details for more info", errDetails...)
|
|
}
|
|
|
|
// 3. Extract ChaincodeInterest and SBE policies
|
|
// The chaincode interest could be nil for legacy peers and for chaincode functions that don't produce a read-write set
|
|
interest := firstResponse.Interest
|
|
if len(interest.GetChaincodes()) == 0 {
|
|
interest = defaultInterest
|
|
}
|
|
|
|
// 4. If transient data is involved, then we need to ensure that discovery only returns orgs which own the collections involved.
|
|
// Do this by setting NoPrivateReads to false on each collection
|
|
originalInterest := &peer.ChaincodeInterest{}
|
|
var protectedCollections []string
|
|
if hasTransientData {
|
|
for _, call := range interest.GetChaincodes() {
|
|
ccc := *call // shallow copy
|
|
originalInterest.Chaincodes = append(originalInterest.Chaincodes, &ccc)
|
|
if call.NoPrivateReads {
|
|
call.NoPrivateReads = false
|
|
protectedCollections = append(protectedCollections, call.CollectionNames...)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 5. Get a set of endorsers from discovery via the registry
|
|
// The preferred discovery layout will contain the firstEndorser's Org.
|
|
plan, err = gs.registry.endorsementPlan(channel, interest, firstEndorser)
|
|
if err != nil {
|
|
if len(protectedCollections) > 0 {
|
|
// may have failed because of the cautious approach we are taking with transient data - check
|
|
_, err = gs.registry.endorsementPlan(channel, originalInterest, firstEndorser)
|
|
if err == nil {
|
|
return nil, status.Error(codes.FailedPrecondition, fmt.Sprintf("requires endorsement from organisation(s) that are not in the distribution policy of the private data collection(s): %v; retry specifying trusted endorsing organizations to protect transient data", protectedCollections))
|
|
}
|
|
}
|
|
return nil, status.Error(codes.FailedPrecondition, err.Error())
|
|
}
|
|
|
|
// 6. Remove the gateway org's endorser, since we've already done that
|
|
plan.processEndorsement(firstEndorser, firstResponse)
|
|
|
|
return plan, nil
|
|
}
|