Hyperledger Fabric从源码分析链码查询与调用

前面分析过了链码的安装与实例化,今天来分析一下链码的调用和查询。因为这两个命令最终在底层调用的都是同一个函数,因此我们将这两个链码执行过程放在一起解析。同样,还是有很多函数会和之前所讲的具有相同的功能,在本文中就不做详细说明,可以翻看之前的两篇文章:

  1. Hyperledger Fabric从源码分析链码安装过程

  2. Hyperledger Fabric从源码分析链码实例化过程

好了,下面就开始吧

下面的解析以 invoke 为例


链码调用源码解析

源码入口在 peer/chaincode/invoke.go

先给一个官方 invoke 的例子:

peer chaincode invoke -o orderer.example.com:7050  --tls true --cafile $ORDERER_CA -C mychannel -n mycc -c '{"Args":["invoke", "a","b","10"]}'

来看下命令函数 invokeCmd

// invokeCmd returns the cobra command for Chaincode Invoke
func invokeCmd(cf *ChaincodeCmdFactory) *cobra.Command {
	chaincodeInvokeCmd = &cobra.Command{
		Use:       "invoke",
		Short:     fmt.Sprintf("Invoke the specified %s.", chainFuncName),
		Long:      fmt.Sprintf("Invoke the specified %s. It will try to commit the endorsed transaction to the network.", chainFuncName),
		ValidArgs: []string{"1"},
		RunE: func(cmd *cobra.Command, args []string) error {
      // 最终执行chaincodeInvoke函数
			return chaincodeInvoke(cmd, cf)
		},
	}
	flagList := []string{
		"name",
		"ctor",
		"channelID",
		"peerAddresses",
		"tlsRootCertFiles",
		"connectionProfile",
		"waitForEvent",
		"waitForEventTimeout",
	}
	attachFlags(chaincodeInvokeCmd, flagList)

	return chaincodeInvokeCmd
}

来看下chaincodeInvoke()函数

func chaincodeInvoke(cmd *cobra.Command, cf *ChaincodeCmdFactory) error {
  // 检查channelID是否为空,-C参数指定
	if channelID == "" {
		return errors.New("The required parameter 'channelID' is empty. Rerun the command with -C flag")
	}
	// Parsing of the command line is done so silence cmd usage
	cmd.SilenceUsage = true

	var err error
	if cf == nil {
    // 和之前一样,创建一个CmdFactory
		cf, err = InitCmdFactory(cmd.Name(), true, true)
		if err != nil {
			return err
		}
	}
	defer cf.BroadcastClient.Close()
	
  // 最终invoke和query都会执行这个函数
	return chaincodeInvokeOrQuery(cmd, true, cf)
}

来看下chaincodeInvokeOrQuery()函数,在peer/chaincode/common.go97行:

// 第二个参数,如果是invoke调用,则传true,如果是query查询,则传false
func chaincodeInvokeOrQuery(cmd *cobra.Command, invoke bool, cf *ChaincodeCmdFactory) (err error) {
  // 和之前一样,获取一个链码标准数据结构spec
	spec, err := getChaincodeSpec(cmd)
	if err != nil {
		return err
	}

	// call with empty txid to ensure production code generates a txid.
	// otherwise, tests can explicitly set their own txid
	txID := ""
	
  // 执行ChaincodeInvokeOrQuery函数执行调用,核心逻辑都在该函数中,一会来看下这个函数的实现
	proposalResp, err := ChaincodeInvokeOrQuery(
		spec,
		channelID,
		txID,
		invoke,
		cf.Signer,
		cf.Certificate,
		cf.EndorserClients,
		cf.DeliverClients,
		cf.BroadcastClient)

	if err != nil {
		return errors.Errorf("%s - proposal response: %v", err, proposalResp)
	}
	
  // 根据是否是invoke调用走不同的逻辑,主要是一些相应结果的输出,例如是否调用成功,查询结果是什么,调用失败输出等等
	if invoke {
		logger.Debugf("ESCC invoke result: %v", proposalResp)
		pRespPayload, err := putils.GetProposalResponsePayload(proposalResp.Payload)
		if err != nil {
			return errors.WithMessage(err, "error while unmarshaling proposal response payload")
		}
		ca, err := putils.GetChaincodeAction(pRespPayload.Extension)
		if err != nil {
			return errors.WithMessage(err, "error while unmarshaling chaincode action")
		}
		if proposalResp.Endorsement == nil {
			return errors.Errorf("endorsement failure during invoke. response: %v", proposalResp.Response)
		}
		logger.Infof("Chaincode invoke successful. result: %v", ca.Response)
	} else {
		if proposalResp == nil {
			return errors.New("error during query: received nil proposal response")
		}
		if proposalResp.Endorsement == nil {
			return errors.Errorf("endorsement failure during query. response: %v", proposalResp.Response)
		}

		if chaincodeQueryRaw && chaincodeQueryHex {
			return fmt.Errorf("options --raw (-r) and --hex (-x) are not compatible")
		}
		if chaincodeQueryRaw {
			fmt.Println(proposalResp.Response.Payload)
			return nil
		}
		if chaincodeQueryHex {
			fmt.Printf("%x\n", proposalResp.Response.Payload)
			return nil
		}
		fmt.Println(string(proposalResp.Response.Payload))
	}
	return nil
}

这个函数很简单,没什么处理逻辑,主要处理逻辑都在ChaincodeInvokeOrQuery()函数中,下面来看看这个函数,在peer/chaincode/common.go431行:

// ChaincodeInvokeOrQuery invokes or queries the chaincode. If successful, the
// INVOKE form prints the ProposalResponse to STDOUT, and the QUERY form prints
// the query result on STDOUT. A command-line flag (-r, --raw) determines
// whether the query result is output as raw bytes, or as a printable string.
// The printable form is optionally (-x, --hex) a hexadecimal representation
// of the query response. If the query response is NIL, nothing is output.
//
// NOTE - Query will likely go away as all interactions with the endorser are
// Proposal and ProposalResponses

// 先解释一下参数吧
// spec,chaincodeInvokeOrQuery中创建的spec
// cID,channelID
// txID,chaincodeInvokeOrQuery中设置了空
// invoke,指定是invoke还是query
// 后面的参数都是CmdFactory的一些成员
func ChaincodeInvokeOrQuery(
	spec *pb.ChaincodeSpec,
	cID string,
	txID string,
	invoke bool,
	signer msp.SigningIdentity,
	certificate tls.Certificate,
	endorserClients []pb.EndorserClient,
	deliverClients []api.PeerDeliverClient,
	bc common.BroadcastClient,
) (*pb.ProposalResponse, error) {
	// Build the ChaincodeInvocationSpec message
  // 创建cis,即链码调用标准数据结构
	invocation := &pb.ChaincodeInvocationSpec{ChaincodeSpec: spec}
	
  // 与之前一样获取签名者
	creator, err := signer.Serialize()
	if err != nil {
		return nil, errors.WithMessage(err, fmt.Sprintf("error serializing identity for %s", signer.GetIdentifier()))
	}
	
  // 这个值我看了一下没有什么用,主要是为了输出打印一些信息
	funcName := "invoke"
	if !invoke {
		funcName = "query"
	}

	// extract the transient field if it exists
  // 私有数据相关的设置
	var tMap map[string][]byte
	if transient != "" {
		if err := json.Unmarshal([]byte(transient), &tMap); err != nil {
			return nil, errors.Wrap(err, "error parsing transient string")
		}
	}
	
  // 与之前一样生成一个提案,txid为根据nonce和creator生成的一个txid
	prop, txid, err := putils.CreateChaincodeProposalWithTxIDAndTransient(pcommon.HeaderType_ENDORSER_TRANSACTION, cID, invocation, creator, txID, tMap)
	if err != nil {
		return nil, errors.WithMessage(err, fmt.Sprintf("error creating proposal for %s", funcName))
	}

  // 对提案进行签名
	signedProp, err := putils.GetSignedProposal(prop, signer)
	if err != nil {
		return nil, errors.WithMessage(err, fmt.Sprintf("error creating signed proposal for %s", funcName))
	}
	var responses []*pb.ProposalResponse
  // 对每个指定的背书节点,都调用ProcessProposal发送一个提案,并收集它们所有的提案相应
  // 这里与install和instantiate不同,可以注意到后两者是采用endorserClients[0].ProcessProposal去调用的,只会指定单独的背书节点
	for _, endorser := range endorserClients {
		proposalResp, err := endorser.ProcessProposal(context.Background(), signedProp)
		if err != nil {
			return nil, errors.WithMessage(err, fmt.Sprintf("error endorsing %s", funcName))
		}
		responses = append(responses, proposalResp)
	}

	if len(responses) == 0 {
		// this should only happen if some new code has introduced a bug
		return nil, errors.New("no proposal responses received - this might indicate a bug")
	}
	// all responses will be checked when the signed transaction is created.
	// for now, just set this so we check the first response's status
  // 按注释的意思是现在只检查第一个响应的status
  // 因为正确情况下,所有的response应该都是一样的,所以在返回的时候返回第一个就可以了
	proposalResp := responses[0]
	
   // invoke走这里,因为还有发送交易给排序节点,query就直接返回proposalResp就可以了
	if invoke {
    // invoke走这里
		if proposalResp != nil {
			if proposalResp.Response.Status >= shim.ERRORTHRESHOLD {
				return proposalResp, nil
			}
			// assemble a signed transaction (it's an Envelope message)
      // 创建一个签名的交易
			env, err := putils.CreateSignedTx(prop, signer, responses...)
			if err != nil {
				return proposalResp, errors.WithMessage(err, "could not assemble transaction")
			}
			var dg *deliverGroup
			var ctx context.Context
			if waitForEvent {
        // 这个参数是事件相关,命令中可以设置--waitForEvent参数,表示是否监听事件
				var cancelFunc context.CancelFunc
				ctx, cancelFunc = context.WithTimeout(context.Background(), waitForEventTimeout)
				defer cancelFunc()

				dg = newDeliverGroup(deliverClients, peerAddresses, certificate, channelID, txid)
				// connect to deliver service on all peers
        // 连接所有peer节点上的deliver service
				err := dg.Connect(ctx)
				if err != nil {
					return nil, err
				}
			}

			// send the envelope for ordering
      // 将交易发送给排序节点
			if err = bc.Send(env); err != nil {
				return proposalResp, errors.WithMessage(err, fmt.Sprintf("error sending transaction for %s", funcName))
			}

			if dg != nil && ctx != nil {
				// wait for event that contains the txid from all peers
        // 等待事件被记录到账本中
				err = dg.Wait(ctx)
				if err != nil {
					return nil, err
				}
			}
		}
	}

	return proposalResp, nil
}

到这里,invokequery的处理逻辑就完了,有几个陌生的地方就是invoke时会设置相关的事件,这部分本文暂不讨论,后续会专门写一篇文章解析 Fabric1.4 中的链码事件机制

链码调用和查询总结

下面总结一下链码的调用和查询过程

  1. 用户执行调用或查询命令
  2. 初始化一个链码命令工厂,包含背书客户端,分发客户端,tls证书,签名者,广播客户端等成员信息
  3. 生成ChaincodeSpec对象 spec
  4. 调用 ChaincodeInvokeOrQuery()函数
    1. 生成一个ChaincodeInvocationSpec对象 invocation
    2. 获取签名者 creator
    3. 从参数中获取私有数据(如果有)
    4. 执行CreateChaincodeProposalWithTxIDAndTransient()函数生成一个交易提案Proposal
    5. Proposal进行签名,得到一个 SignedProposal对象signedProp
    6. signedProp通过EndorserClient.ProcessProposal方法发往指定的背书节点,即需要安装链码的节点,由背书节点的 EndorserServer进行处理
    7. 如果是query则直接执行9
    8. 如果是invoke
      1. 根据响应生成签名交易
      2. 如果客户端有等待事件,则设置时间等待
      3. 将交易发往排序节点,并等待所有 peer 节点将事件记录到账本中以后返回
    9. 返回所有响应中的第一个响应(因为每个响应结果都一样)
  5. 根据执行结果输出一定的日志信息,比如 query 则输出查询信息,invoke 则输出状态信息

猜你喜欢

转载自blog.csdn.net/lvyibin890/article/details/107004167