go_study/fabric-main/internal/pkg/gateway/endorse.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
}