前面分析过了链码的安装与实例化,今天来分析一下链码的调用和查询。因为这两个命令最终在底层调用的都是同一个函数,因此我们将这两个链码执行过程放在一起解析。同样,还是有很多函数会和之前所讲的具有相同的功能,在本文中就不做详细说明,可以翻看之前的两篇文章:
好了,下面就开始吧
下面的解析以 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.go
97行:
// 第二个参数,如果是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.go
431行:
// 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
}
到这里,invoke
和 query
的处理逻辑就完了,有几个陌生的地方就是invoke
时会设置相关的事件,这部分本文暂不讨论,后续会专门写一篇文章解析 Fabric1.4 中的链码事件机制
链码调用和查询总结
下面总结一下链码的调用和查询过程
- 用户执行调用或查询命令
- 初始化一个链码命令工厂,包含背书客户端,分发客户端,tls证书,签名者,广播客户端等成员信息
- 生成
ChaincodeSpec
对象spec
- 调用
ChaincodeInvokeOrQuery()
函数- 生成一个
ChaincodeInvocationSpec
对象invocation
- 获取签名者
creator
- 从参数中获取私有数据(如果有)
- 执行
CreateChaincodeProposalWithTxIDAndTransient()
函数生成一个交易提案Proposal
- 对
Proposal
进行签名,得到一个SignedProposal
对象signedProp
- 将
signedProp
通过EndorserClient.ProcessProposal
方法发往指定的背书节点,即需要安装链码的节点,由背书节点的EndorserServer
进行处理 - 如果是
query
则直接执行9 - 如果是
invoke
- 根据响应生成签名交易
- 如果客户端有等待事件,则设置时间等待
- 将交易发往排序节点,并等待所有
peer
节点将事件记录到账本中以后返回
- 返回所有响应中的第一个响应(因为每个响应结果都一样)
- 生成一个
- 根据执行结果输出一定的日志信息,比如
query
则输出查询信息,invoke
则输出状态信息