fabric源码解析19——ACC的安装
概述
- peer chaincode install命令执行安装命令,命令定义在peer/chaincode/install.go中,这也是安装的起点。另外需要注意的一点是,这个命令是在peer node start,peer channel create,peer channel join命令依次执行完毕之后所执行的,即执行install之时,peer结点的基本的模块(包括SCC)都已初始化完毕,channel也已经建立。
- 根据实例化的原则,从install_test.go中提取了一句实际的install命令:
peer chaincode install -n example02 -p github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02 -v anotherversion
,本篇将以此句命令为例子。安装的ACC是example02,存在于-p指定的路径下,版本是anotherversion。-l没有给,自然取默认的值,为go语言。 - install安装使用的chaincode数据有两种形式,一种是CDS,一种是chaincode package/signpackage命令形成的ccpackfile包。因为我们没有涉及过这两个命令,因此这里我们只以前一种CDS数据包为例,叙述安装过程。
- ACC的安装涉及的图为ACC-Install-DataConstuct.PNG。下文中提及“图中”字眼,均指此图中。
- install能够识别的flag有-l,-c,-p,-n,-v。其中-c不常用,其指定的是ACC所要执行的函数和函数的参数,一般在具体执行的时候,如查询或转账的时候再给定。
- install最终所要做的事情,就是将example02的源码包放入docker容器的安装目录中。
生成签名申请包
以peer/chaincode/install.go的chaincodeInstall(...)
为起点(设定ccpackfile==""
),根据ACC-Install-DataConstuct.PNG,从左上角最原始的命令行数据开始,一路组装数据,至形成下面中路的SignedProposal,然后将SignedProposal通过cf.EndorserClient.ProcessProposal(...)
提交至Endorser服务端。这个冗长的过程,不再详述。只提以下几点:
- chainID在SignedProposal中为空,即安装所要做的仅仅是把example02的压缩包放入容器的指定目录中而已,只有部署的时候才需要指定部署到哪条链上。
- CDS的CodePackage是example02的代码压缩包,包括源码和依赖的第三方库。因为SCC的一切都在项目编译时编进peer中了,所以SCC的CDS的这个字段就是空的,而ACC需要安装源码,所以这个字段在正常的情况下肯定不是空的。这个压缩包最终是在core/chaincode/platforms/golang/platform.go中的
GetDeploymentPayload()
打包的。该程序的复杂之处在于除了要打包example02源码,还要打包example02直接依赖的但go标准库未提供的第三方库,还要打包这些第三方库直接依赖但go标准库未提供的第三方库(即example02间接依赖但go标准库未提供第三方库)。这么做的目的就是让一个chaincode无论放到哪个容器里,不会因为缺少某个第三方库而编译失败。同时这个打包的过程也就要求我们在执行example02安装的时候,要实现把其依赖的第三方库事先放到GOPATH/src下。 - txid是在/protos/utils/proputils.go中的
CreateChaincodeProposalWithTransient(...)
中计算出来的,是哈希(peer结点的MSPID+证书元数据+随机数nonce)的值,可以就把它当作一个唯一的字符串,不用太过深究。
处理安装申请
- Endorser服务端在core/endorser/endorser.go中的
ProcessProposal(...)
处,接收到来自Endorser客户端发送的SignedProposal和一个之后一路都会用到的Context上下文ctxt。 - 在
ProcessProposal(...)
中,从开始至var txsim ledger.TxSimulator
处,之上的代码全部是一边解压抽取SignedProposal中的数据,一边验证这些数据。至于抽取验证了哪些数据,根据代码对照图回溯,在此不做详述。 var txsim ledger.TxSimulator
,var historyQueryExecutor ...
,一个是交易模拟工具,一个是历史查询执行工具。由于是Install,且chainID为空,这两个值在之后都一直为空。if chainID != ""
的分支也不会进入(当部署example02时,chainID不为空,则会进入此分支,根据chainID获取这两个工具,供之后部署使用)。ProcessProposal(...)
所做的主要的两件事就是:(1)e.simulateProposal(...)
,模拟执行申请。(2)e.endorseProposal(...)
,背书申请执行的结果。但是由于chainID为空,所以install命令不会执行此步(同样,部署时会用到),而是直接以ProposalResponse的形式返回(1)中执行的结果。下文将对(1)展开详述。- 在此撇开一笔说一下chaincode的交易使用的结果。该结果定义在core/chaincode/shim/response.go中,目前定义的还相当的简单(也为以后升级留了空间),只定义了三个:OK,ERRORTHRESHOLD,ERROR。其中ERRORTHRESHOLD算是错误标志线,值为400,即小于它的值,表示成功或者还能勉强接受且无伤大雅的异常,而大于等于它的值,则表示是不能接受的错误。
执行申请
- 在此罗列一下传入
e.simulateProposal(...)
的参数:ctx为上文所述的上下文ctxt(至此未有更新);chainID为空;txid为交易ID;signedProp是客户端发送来的原数据;prop是从signedProp中抽取出来的Proposal;hdrExt.ChaincodeId也是抽取出来的数据,只包含一个值为lscc的Name字段;txsim为空。 - 在
e.simulateProposal(...)
中,前期又做了些简单的抽取和检查的事情:cis, err := putils.GetChaincodeInvocationSpec(prop)
从prop中抽取出CIS。if err = e.disableJavaCCInst(cid, cis); err != nil
通过判断example02的CDS.CS.Type来断定要安装的是否为Java源码,关于这点注释说的很清楚,当前版本不支持Java写的chaincode,但这部分在将来会被移除。if e.checkEsccAndVscc(prop); err != nil
,escc和vscc对prop的检查,但是当前版本未作什么实际的检查,以后的版本可能会加入。if !syscc.IsSysCC(cid.Name){...}else{ version = util.GetSysCCVersion() }
,由于cid.Name就是lscc,因此只会进入else分支,得到的为lscc的版本值为1.0.0。 - 最后一步调用了
e.callChaincode(...)
,执行CIS指定的动作。上文所述的另一个函数endorseProposal()
最后也是调用这个函数开始执行申请的任务的。也就是说,e.callChaincode(...)
能实现什么效果,做什么事情,完全是传入的参数决定的,这个函数可以算是实际开始执行申请的起点。在此罗列一下进入e.callChaincode(...)
的参数:ctx依旧为ctxt;chainID=空;version=1.0.0;txid为交易ID;signedProp/prop/cid/txsim不变;cis是第2步新抽取出的CIS。这些参数均在图中可以找到对应数据。 callChaincode()
函数做了三件事:(1)cccid := ccprovider.NewCCContext(...)
,根据传入的参数,创建一个CCContext对象供执行申请所用。(2)chaincode.ExecuteChaincode(...)
,执行申请。(3)if cid.Name == "lscc" && len(cis.ChaincodeSpec.Input.Args) >= 3 && ...
,这一步只有部署,升级的交易才会进入此分支,还记得上文的txsim和概述中提到的-c么,这里的分支就是执行这些对象所承载的任务的,在此不做讨论。下文将对(2)展开详述。ExecuteChaincode(...)
函数,在core/chaincode/chaincodeexec.go中定义,这里可以对看《fabric源码分析18》的部署章节第5步所述的线路问题。spec, err = createCIS(cccid.Name, args)
又根据传入的参数新建了一个CIS,不过仔细看一下createCIS
就可以知道,其新生成的CIS和第2步从prop中抽取出的CIS在内容上是完全一致的,因此我们完全可以将这个spec看作是图中的那个CIS。接着就调用了Execute(ctxt, cccid, spec)
,对应到《fabric源码分析18》的部署章节的第6步,殊途同归,ACC也就此进入了类似于SCC部署所述的机制和道路来进行example02的安装。只不过,传入数据所承载的任务不同,执行的方向也会稍微有所不同。由于执行过程在《fabric源码分析18》详述过,因此下文将以粗线条叙述,重点在于提及不同之处。在此罗列一下进入Execute(...)
的参数:ctxt依旧未有更新;cccid是第4步生成的,对应图中的CCContext;spec可以将其当作图中的CIS。Execute(ctxt, cccid, spec)
仍依次执行theChaincodeSupport.Launch(...)
,theChaincodeSupport.Execute(...)
,但这里spec是第5步生成的CIS,因此cctyp的值为ChaincodeMessage_TRANSACTION,进而生成的供theChaincodeSupport.Execute(...)
(下文若非特指,凡提到的Execute函数均指此函数)使用的ccMsg是ChaincodeMessage_TRANSACTION类型的消息(对应图中的ChaincodeMessage)。依旧,分开讲解两个函数。
Launch
Launch(...)
函数在ACC安装的情况下执行不了太久,canName := cccid.GetCanonicalName()
等到的canName=lscc:1.0.0,此值会是程序进入if chrte, ok = chaincodeSupport.chaincodeHasBeenLaunched(canName); ok
分支,进而进入if chrte.handler.isRunning()
分支而返回。因为负责处理安装example02的lscc已经Launch过了,所以这样安排顺理成章。
Execute
- 继续,在此罗列一下传入
Execute(...)
函数的参数:ctxt依旧位更新;cccid为图中的CCContext;ccMsg是图中的ChaincodeMessage;executetimeout为超时时间。 Execute(...)
函数中,canName := cccid.GetCanonicalName()
再次得到lscc:1.0.0,然后通过chrte, ok := chaincodeSupport.chaincodeHasBeenLaunched(canName)
,获取了lscc在《fabric源码分析18》中已部署过的ServerHandler,再chrte.handler.sendExecuteMessage(...)
开始使用该ServerHandler以触发状态机进入运行,最后进入select-case
等待。在此罗列一下传入sendExecuteMessage(...)
的参数:ctxt依旧未更新;cccid.ChainID为空;msg为图中的ChaincodeMessage;cccid.SignedProposal为图中的SignedProposal;cccid.Proposal为图中的Proposal。- 在
sendExecuteMessage(...)
中,通过调用handler.triggerNextState(msg, true)
,ServerHandler将msg发给自身的handler.nextState
通道以触发ServerHandler的状态机进入下一个状态。 - ServerHandler的
processStream()
收到来自handler.nextState
通道的msg,先交给handler.HandleMessage(in)
处理,ServerHandler状态机无任何变化,然后handler.serialSendAsync(in, errc)
给lscc的ShimHandler发送msg。 - lscc的ShimHandler的
chatWithPeer()
收到msg,交由handler.handleMessage(in)
处理,触发beforeTransaction
事件函数,该事件函数主要调用同文件中的handleTransaction(...)
函数。 handleTransaction(...)
函数主要做的就是根据ShimHandler收到的msg生成并初始化一个ChaincodeStub,对应图中的ChaincodeStub,然后handler.cc.Invoke(stub)
调用lscc的Invoke()
方法对example02进行安装。- 在lscc的
Invoke()
中,args := stub.GetArgs()
获取到的是CIS.CS.Input.Args。这个数组的值来自于protos/utils/proputils.go中createProposalFromCDS()
中的case "install":
中的ccinp。因此Invoke()
中,function := string(args[0])
得到function的值是"install"
,switch function
会进入case INSTALL:
的分支。 case INSTALL:
分支中,首先,lscc.policyChecker.CheckPolicyNoChannel(...)
专门使用了检测未指定Channel的chaincode的函数来检查要安装的example02,这也侧面映证了上文生成签名申请包章节中对chainID的描述。接着,depSpec := args[1]
取出来的就是example02的CDS,不过此时的CDS仍是被Marshal过的。最后,lscc.executeInstall(stub, depSpec)
,调用lscc的函数,依据stub和example02的CDS,执行安装。- 在
executeInstall(...)
中,首先,ccpack,err := ccprovider.GetCCPackage(ccbytes)
,根据example02被Marshal过的CDS创建一个CDSPackage(core/common/ccprovider/cdspackage.go中定义),对应图中的CDSPackage。其次,简单的验证了example02的Name和Version。最后,调用ccpack.PutChaincodeToFS()
将example02源码写入文件系统。 - 在
PutChaincodeToFS(...)
中,一系列if检查之后,先path := fmt.Sprintf("%s/%s.%s", chaincodeInstallPath, ccname, ccversion)
整合出要写入的路径,即在chaincodeInstallPath目录下放入名为example02.anotherversion的文件。然后os.Stat(path)
先查看这个文件名是否可用。最后ioutil.WriteFile(path, ccpack.buf, 0644)
将CDSPackage中成员buf写入path指定的地方。这个buf,从图中就可知道,就是example02源码压缩包。至此,example02的安装申请执行完毕。由此,开始一路返回。 - 一路返回至第6步ShimHandler的事件函数
handleTransaction(...)
中,handler.cc.Invoke(stub)
返回,继续向下执行,将nextStateMsg赋值为ChaincodeMessage_COMPLETED类型的消息,并执行defer中的handler.triggerNextState(nextStateMsg, send)
将该消息发送给自己的状态机。ShimHandler只将ChaincodeMessage_COMPLETED消息发送给ServerHandler之后就再无其他动作或变化。 - ServerHandler收到ChaincodeMessage_COMPLETED消息,通知仍处于等待之中的
Execute(...)
函数,然后等待结束,Execute(...)
函数成功返回。
一路返回
Execute(...)
函数结束之后,就此一路返回,一直返回到core/endorser/endorser.go中的callChaincode(...)
,chaincode.ExecuteChaincode(...)
执行完毕,对应到上文执行申请章节的第4步的(2),由于这一步的(3)install申请不会执行,因此callChaincode(...)
也就此结束。- 继续返回到
simulateProposal
,继续的代码中if txsim != nil
分支不会进入,因此也是直接返回至ProcessProposal()
。 - 继续
ProcessProposal()
,将进入if res != nil
分支,但无法进入if res.Status >= shim.ERROR
分支。因此继续向下走,进入if chainID == ""
分支对要返回给Endorser客户端的应答消息pResp赋值,最后返回pResp给Endorser客户端。 - example02安装申请的起点,peer/chaincode/install.go中的
chaincodeInstall(...)
,所调用的install(...)
中的cf.EndorserClient.ProcessProposal()
,即是Endorser客户端,收到服务端发来的消息,返回后install(...)
随之结束,进而chaincodeInstall(...)
结束。至此,整个example02的安装全部结束。
安装后的状态
- peer结点的chaincodeInstallPath目录下,会有一个名为example02.anotherversion的文件,该文件即为example02源码压缩包。