fabric源码解析20——ACC的部署

fabric源码解析20——ACC的部署

概述

  • peer chaincode instantiate命令执行部署命令,命令定义在peer/chaincode/instantiate.go中,这也是部署的起点。另外需要注意的一点是,这个命令是在peer node start,peer channel create,peer channel join,peer chaincode install命令依次执行完毕之后所执行的,即执行instantiate之时,peer结点的基本的模块(包括SCC)都已初始化完毕,channel已经建立,ACC也已安装。
  • 根据实例化的原则,从instantiate_test.go中和官方文档中提取了整合了(尽量能用的flag都用上)一句实际的instantiate命令:peer chaincode instantiate -n example02 -v anotherversion -o orderer.example.com:7050 -C testchain -c '{"Args":["init","a", "100", "b","200"]}' -P "OR ('Org1MSP.member','Org2MSP.member')",本篇将以此句命令为例子。-n指定部署的ACC是example02,-v指定版本是anotherversion,-o指定连接的orderer服务实例的端点是orderer.example.com:7050,-C指定要部署的链是testchain,-c指定执行的函数和函数的参数,-P指定策略。
  • 同样,本文将重点放在不同于install之处。
  • ACC的部署涉及的图为ACC-Deploy-DataConstuct.PNG。下文中提及“图中”字眼,均指此图中。
  • instantiate能够识别的flag有-l,-c,-n,-v,-P,-E,-V,-C。相对于install,这里就指定了-c,要求部署的时候初始化a,b两个账户。
  • ACC的部署是存储在docker容器中的,所启动的两端Handler通过grpc进行通信。
  • instantiate最终要做三件事情,涉及三条链码:(1)lscc执行部署交易将example02的源码放入自己的写集。(2)example执行自身的部署交易,启动example02容器并与peer结点通过grpc通信进行初始化,最后将初始化的状态写入自己的写集。(3)获取lscc,example02的读写集,使用escc进行背书,然后将签名,读写集等部署产生的数据封装成Envelope,发送到orderer结点交由其处理。

起点

  1. 起点在peer/chaincode/instantiate.go中的chaincodeDeploy(...),主要做了两件事:(1)env, err := instantiate(cmd, cf),与install命令执行的线路类似,对example02进行部署,并返回部署结果env。(2)cf.BroadcastClient.Send(env),在部署成功的前提下,向各个结点广播部署结果env。下文将分别详述。

部署

  1. instantiate()是部署的起点,同install一样,从图中左上角最原始的命令行数据开始,一路组装数据,至SignedProposal,依旧,可一边看图一边对看源码。这里需要注意的是,中途所组装的关于example02的CDS中,CodePackage为nil(这个很好理解,install已经将example02的源码放入指定目录了,部署的时候自然就不必再携带example02的源码数据)。chainID值为testchain,不再为空,即要把example02安装在testchain上。CIS.CS.Input.Args的值依旧来自于protos/utils/proputils.go的createProposalFromCDS()中的ccinp,不过不同于install,这次进入的是case "upgrade":分支(case "deploy"中执行了fallthrough),这点将直接影响lscc的Invoke所进入的分支,后文还将提到。

  2. cf.EndorserClient.ProcessProposal(...)将组装好的SignedProposal,连同一个之后一路在用的Context上下文ctxt,一起发给Endorser服务端。

  3. Endorser服务端在core/endorser/endorser.go中的ProcessProposal(...)接收到来自客户端的ctxt和SignedProposal。ProcessProposal(...)所做的事情依旧如install时所描述的那样,但不同与install之处在于,chainID := chdr.ChannelId获取的值为testchain,以至于之后三个if chainID != ""分支都会进入:(1)lgr := peer.GetLedger(chainID),获取testchain的账本对象lgr,并调用lgr.GetTransactionByID(txid)对txid(交易ID)的唯一性进行检查(在这里是为了避免重复部署)。(2)调用txsim, err = e.getTxSimulator(chainID)historyQueryExecutor, err = e.getHistoryQueryExecutor(chainID);,分别获取testchain的交易模拟工具和历史查询工具并赋值给txsim和historyQueryExecutor,同时将两个工具先后放入了ctxt中。(3)e.endorseProposal()将会执行,对example02的部署进行背书。在此罗列一下进入simulateProposal(...)的参数:ctxt被更新,此刻暂时只加入了历史查询工具;chainID/txid/signedProp/prop均未变;hdrExt.ChaincodeId对应图中的CIS.CS.ChaincodeId,只包含一个值为lscc的Name字段;txsim为testchain的交易模拟工具。

  4. e.simulateProposal(...)中,不同于install所述之处在于,执行e.callChaincode(...)之后,会进入if txsim != nil分支,也会进入最后的if cid.Name == "lscc" && ...分支。在此罗列一下进入callChaincode(...)的参数:除了新抽取出来的cis,其余均未变。

  5. callChaincode(...)中,会进入if txsim != nil分支,将第3步(2)中获取的交易模拟工具加入了ctxt。在此罗列一下进入chaincode.ExecuteChaincode(...)的参数:ctxt新加入了交易模拟工具;cccid对应图中的CCContext;cis.ChaincodeSpec.Input.Args对应图中的CIS.CS.Input.Args。

  6. chaincode.ExecuteChaincode(...)中,依旧生成相当于图中CIS的对象,同ctxt,CCContext一同传入同文件中的Execute(...)函数,在这个函数中按部就班的开始Launch和Excute。

  7. 在此省略与install的Launch/Excute章节相似的过程,一直到调用lscc的Invoke(core/lscc/lscc.go中),args := stub.GetArgs()获取得到的是第1步所提到的ccinp,function := string(args[0])得到的值是deploy,因而之后的switch-case会进入case DEPLOY:分支。在case DEPLOY:分支中,依次对args的每个值进行了检查,对一下参数进行了修补,如escc/vscc若为空,则给默认值,最后调用lscc.executeDeploy(...)开始部署example02。在此罗列一下进入executeDeploy(...)的参数:stub为图中的ChaincodeStub;chainname值为testchain;depSpec为图中的CDS,但是此刻仍是被Marshal过的数据;policy为args的第3个参数,值为"OR ('Org1MSP.member','Org2MSP.member')";escc/vscc由于命令行未指定,为空值。

  8. executeDeploy(...)中,做了如下事情:(1)先检查了example02的名字,版本,是否可通过ACL(账户控制列表)。(2)调用lscc.getCCInstance(...),查看example02是否已经存在于链上。(3)调用ccpack, err := ccprovider.GetChaincodeFromFS(...)将example02的源码读进一个CDSPackage对象ccpack,对应图中的CDSPackage。然后cd := ccpack.GetChaincodeData(),再由ccpack生成一个ChaincodeData数据对象,对应图中的ChaincodeData。(4)调用lscc.getInstantiationPolicy(chainname, ccpack)lscc.checkInstantiationPolicy(...)分别获取并检查一个部署策略(不展开详述)。(5)调用lscc.createChaincode(stub, cd),进而直接调用lscc.putChaincodeData(stub, cd),传入图中的ChaincodeStub和ChaincodeData,执行部署任务

  9. putChaincodeData(stub, cd)中,简单的检查之后,就调用图中ChaincodeStub的函数stub.PutState(cd.Name, cdbytes),以example02的名字为key,Marshal过的ChaincodeData数据为value,把这一对key-value放到账本中去。之后,图中又有没有地方了,数据组装就没有了。同时,从这里可以看出,链,其实就是peer中的账本。

  10. stub.PutState(cd.Name, cdbytes)(core/chaincode/shim/chaincode.go中定义),调用了stub.handler.handlePutState(...)触发了lscc的ShimHandler的handlePutState()函数(stub的handler是在第7步省略的过程中,在core/chaincode/shim/handler.go中handleTransaction中生成ChaincodeStub时传入的lscc的ShimHandler实例)。在此罗列一下传入handlePutState()函数的参数:key,example02的名字;value,被Marshal过的图中的ChaincodeData;txid,交易ID。

  11. handlePutState()中,所做的事情:(1)proto.Marshal(&pb.PutStateInfo{...}),首先将key和value封装,作为一个ChaincodeMessage_PUT_STATE类型的ChaincodeMessage消息的Payload,Txid依旧是txid。(2)handler.sendReceive(msg, respChan),调用ShimHandler将ChaincodeMessage_PUT_STATE类型的消息异步发送给lscc的ServerHandler,然后进入select-case等待ServerHandler的回信。注意这里等待的respChan,是createChannel生成的,这个函数类似于ServerHandler的createTxContext,都是以txid为key在map中存储通知频道,防止交易重复,且随用随删。

  12. lscc的ServerHandler收到ChaincodeMessage_PUT_STATE类型的消息,将只触发状态机的enterBusyState事件函数。该事件函数整个都是异步执行的。

  13. enterBusyState函数一眼看上去很长很麻烦,所以先讲一下函数的布局(1)使用defer作为最后发送消息的地方,发送的消息是triggerNextStateMsg(2)如ShimHandler的handleInit函数一样,定义了一个errHandler函数,一旦检查有错误,即将triggerNextStateMsg赋值后返回,触发defer发送。若中途没有错误,则顺利到达函数的最后,给triggerNextStateMsg赋值一个正常的应答消息,然后随着函数的结束触发defer发送应答(3)中部的if大分支是函数的处理主体,分别处理ChaincodeMessage_PUT_STATE,ChaincodeMessage_DEL_STATE,ChaincodeMessage_INVOKE_CHAINCODE三类消息,对应执行不同动作,从名字基本就可以判断各是做什么的:放一个状态,删一个状态,调用chaincode(改一个状态),就是熟悉的增删改。接着,函数的具体执行(1)handler.createTXIDEntry(msg.Txid),这也是防止同一个交易重复执行的一招。(2)handler.isValidTxSim(msg.Txid...),根据txid获取txContext,这个交易上下文transactionContext是在第7步省略的过程中,Excute(...)最初执行的sendExecuteMessage中的handler.createTxContext(...)创建的(对看《fabric源码分析18》章节Excute第3步),创建的同时,也将第3步,第5步更新到ctxt中的两个工具取出来后赋值给了txContext的txsimulator,historyQueryExecutor两个字段。这其中交易模拟工具在此之后将用到。(3)chaincodeID := handler.getCCRootName()取出来的是ServerHandler关于lscc的信息lscc:1.0.0。(4)只进入if msg.Type.String() == pb.ChaincodeMessage_PUT_STATE.String()分支(其他分支这里不讨论),txContext.txsimulator.SetState(...),利用txContext中的交易模拟工具(回顾一下,这两个工具是在core/endorser/endorser.go中的ProcessProposal(...)中创建的,工具的原型是在core/ledger/kvledger/txmgmt/txmgr/lockbasedtxmgr/lockbased_tx_simulator.go中定义的lockBasedTxSimulator,SetState(...)函数也定义在同文件中),最终将key和value,连同处理example02的lscc:1.0.0一同写入到写集中。这里说的写集追踪一下就可以知道,其实只是个map,这个map的线路是:在core/ledger/kvledger/txmgmt/rwsetutil/rwset_builder.go中的RWSetBuilder中的rwMap映射,这个map以lscc:1.0.0为键,映射一个nsRWs,而SetState(...)最终就是将key和value存储在这个nsRWs中的writeMap(写集)中。也就是说,对example02的部署目前并没有真正提交到账本(数据库)中,部署也是一个交易,自然也需要最终提交到账本中,只是目前还没到最终提交的时候。至此,example02的chaincode完成了真正的部署(5)将交易的结果放入triggerNextStateMsg,然后触发defer,调用ServerHandler的handler.triggerNextState向ShimHandler发送携带部署结果res的ChaincodeMessage_RESPONSE类型的消息(ServerHandler对这个消息会无动于衷)。

  14. lscc的ShimHandler收到ChaincodeMessage_RESPONSE类型的消息,触发状态机的afterResponse事件函数。该事件函数调用handler.sendChannel(msg)向第11步提到的respChan发送消息,第11步(2)的sendReceive(...)的等待结束。重新定位到core/chaincode/shim/handler.go中的handlePutStatesendReceive(...)结束返回后,自此开始一路返回。

  15. 一路返回至core/lscc/lscc.go中的putChaincodeData(...),对应第9步。继续返回,一直返回到同文件中的lscc的Invoke(stub)函数中的case DEPLOY:lscc.executeDeploy(...)结束,整个Invoke(stub)函数的本次部署也执行完毕。返回的是return shim.Success(cdbytes),cdbytes指的是图中Marshal过的ChaincodeData。

  16. 继续返回,定位到core/chaincode/shim/handler.go中的handleTransaction函数,handler.cc.Invoke(stub)执行完毕。触发defer,向lscc的ServerHandler发送ChaincodeMessage_COMPLETED类型的ChaincodeMessage消息,消息的Payload是第15步所返回的结果。

  17. lscc的ServerHandler收到完成的消息,通知之后,core/chaincode/chaincode_support.go中的Execute(...)等待结束,函数返回。继续一路返回,直至返回到core/endorser/endorser.go中的callChaincode中,chaincode.ExecuteChaincode(...)执行完毕(对应第5步),继续向下执行。

  18. 进入if cid.Name == "lscc" && ...分支,对应第4步。cds, err = putils.GetChaincodeDeploymentSpec(...)从图中example02的CIS解压出图中的CDS,cccid = ccprovider.NewCCContext(...)并重新生成一个CCContext,最后将example02的CDS,CCContext和ctxt一同传入chaincode.Execute(...)第二次进入Launch-Execute过程(这个过程图中没有体现)。这次进入没有通过core/chaincode/chaincodeexec.go中的ExecuteChaincode,而是直接调用了exectransaction.go中的Execute(...)。不同于第一次的是:(1)第一次传入的是lscc的CCContext和CIS,这次传入的是example02的CCContext和CDS。(2)第一次时Launch的lscc已经Launch过,中途就返回了,但example02没有Launch过,因此这次Launch将一直执行下去,建立example02的容器。(3)第一次传入的是lscc的CIS,因此生成的是ChaincodeMessage_TRANSACTION消息,第二次传入的是example02的CDS,因此生成的是ChaincodeMessage_INIT类型的消息。不同类型的消息将传入Execute。

  19. Launch(...)中,这里提几个字段的值:(1)userRunsCC是在初始化ChaincodeSupport时写入的,引用的是core.yaml中chaincode的mode配置项是否是dev模式,这里默认是net,即userRunsCC的值为false。(2)example02也没有Launch过,因而chaincodeHasBeenLaunched(canName)获取的chrte为空。(3)example02的CDS的ExecEnv的值为默认的ChaincodeDeploymentSpec_DOCKER。据此三点,可知整个Launch中只会进入if (!chaincodeSupport.userRunsCC ||...分支,接着进入if !(chaincodeSupport.userRunsCC ||...从文件目录中读取处install命令放入的example02的源码包数据,进而获取CDS,该CDS的CodePackage包含example02的源码数据,将供后文example02的docker容器的建立使用。builder = func() (io.Reader, error) { return platforms.GenerateDockerBuild(cds) }创建了一个builder函数。最后调用launchAndWaitForRegister(...),开始对example的Launch。在此罗列一下传入的参数:context为ctxt;cccid是第18步生成的example02的CCContext;cds是图中的CDS;cLang是GO语言;builder是具体启动example02的容器的函数。

  20. 对看《fabric源码分析18》Launch章节第4步,从此步起,开始了对example02的Launch过程,类似的步骤将省略。不同于SCC的Launch之处在于,builder将在部署中使用到,example02所启动的是docker容器DockerVM。一直追溯,将定位到core/container/dockercontroller/dockercontroller.go中的Start(...)函数,在此罗列一下传入的参数:ctxt,添加了ChaincodeSupport实例;其余参数都来自StartImageReq实例(在launchAndWaitForRegister中进行组装)的成员。

  21. Start(...)函数中,启动了example02的docker容器:(1)imageID, err := vm.GetVMName(ccid),根据ccid中保存的三个ID,组装一个example02的docker镜像ID,这个ID的规则是小写,字符范围在只有字母数字,-,.,_之内,否则会用-替换,形式为%s-%s-%s。(2)client, err := vm.getClientFnc(),创建一个go-dockerclient的客户端对象,这个对象是实际进行docker容器的基础。(3)containerID := strings.Replace(imageID...),根据镜像ID生成一个容器ID。(4)attachStdout := viper.GetBool("vm.docker.attachStdout"),获取一个配置项,这个配置项默认值是false,用于为调试目的而使能docker容器的标准输出和标准错误输出,这里使用默认值,即后边的if attachStdout分支(该分支起了两个goroutine分别接收容器的标准输出和标准错误输出)不会进入。(5)vm.stopInternal(ctxt,...),根据example02的镜像ID,容器ID,尝试删除可能已经存在的同ID的镜像和容器,为之后的创建扫清障碍。(6)err = vm.createContainer(ctxt,...),创建容器,罗列一下传入这个函数的参数:ctxt;example02的镜像ID,容器ID;供容器使用的参数args;要应用到容器里的环境变量env;不开启标准输出和错误输出的attachStdout。这个函数中主要做的就是首先根据现有数据的指向生成一个client认可且可以使用的docker容器配置对象copts,这个配置对象除了基本容器的基本信息外,还指定了一个配置函数,即getDockerHostConfig,然后调用client.CreateContainer(copts)创建容器。(7)进入if err != nil分支,事实上,(6)将执行失败,因为创建容器的基础是容器使用的镜像存在,而当第一次部署的时候,example02的镜像并不存在,因此将产生err == docker.ErrNoSuchImage的错误,在这个分支中,使用了builder先创建了example02的镜像,然后重新执行(6)创建容器(8)prelaunchFunc()预Launch一下example02,对看《fabric源码分析18》中Launch章节第7步。(9)client.StartContainer(containerID, nil),启动example02的容器。这里还可以说一句,根据注释,通过配置对象创建容器的方式将在未来的版本中改变。这是docker自身的相关接口将在未来发生变化而产生的连锁反应。

  22. 详解第21步创建example02的镜像和启动容器的过程首先概述一下(1)使用的是第三方库github.com/fsouza/go-dockerclient,读者可以自行对该库进行学习,这是能理解这一步内容的基础。(2)对看core/chaincode/platforms/util/utils.go中的DockerBuild注释,创建ACC的docker容器并不是简单的使用标准的docker build+Dockerfile的机制(因为这样产生的镜像有体积过大,有额外安全漏洞,运行笨拙等缺点),而是先积攒关于example02的镜像数据(Dockerfile文件,peer结点的tls证书,编译后的可执行程序),然后创建一个相对轻量级的ACC容器(由此可以看出,对ACC的容器进行减负,主要是减去要为编译ACC而存在的部分,这部分通常使用较少,但占用的空间和资源又相对多)。(3)编译example02源码用到一个容器,这个容器将core.yaml中chaincode.builder项指定的fabric-ccenv作为启动镜像,该镜像由fabric项目提供,在Getting Started中下载镜像时会下载(这个容器有1G+,所以说上述的机制还是有必要的,不能为了最多M级别的源码一时的编译而一直运行一个G级别的容器),其实应该就是一个能编译example02的linux系统容器,ccenv,就是chaincode environment的缩写。粗略的过程就是先创建这个容器,然后把example02的源码上传到容器中,然后启动容器时执行go build…,然后再把编译好的可执行程序下载出来。(4)在core/chaincode/platforms下是平台相关的代码,用于生成支持的语言的ACC的镜像所需的数据包,platforms.go是总控文件,car、golang、java是平台相关的代码,这里只关注golang语言。然后详述过程(1)**builder执行的是core/chaincode/platforms/platforms.go中的GenerateDockerBuild(...),在这个函数中,先把example02容器通过tls连接peer结点的证书peer.crt放入inputFiles中,然后调用generateDockerfile来生成可用的Dockerfile文件(使用golang平台的GenerateDockerfile来创建了文件头,FROM命令指定example02最终使用**fabric-baseos镜像,由core.yaml中的chaincode.golang.runtime项指定,ADD将编译生成的example02的可执行程序压缩包binpackage.tar复制并解压到/usr/local/bin目录下,还定义了一些LABEL和环境变量等,该Dockerfile文件的范本也上传至网盘中),也将它放入了inputFiles中。(2)input, output := io.Pipe()生成了一个管道,连同go func(){...}中的gw,tw压缩对象,形成了input<—>output<—gw<—tw的数据流向管道,即向tw中写数据,最终会形成压缩包并流向input(3)在新启的goroutine中,generateDockerBuild(...)汇总了example02镜像数据。先把证书和Dockerfile文件写入tw,然后调用golang的GenerateDockerBuild(cds, tw),进而调用core/chaincode/platforms/util/utils.go中的DockerBuild,依据fabric-ccenv镜像创建了一个容器,创建该容器的选项DockerBuildOptions指定了三个值:Cmd指定了编译命令,将example02编译成名为chaincode的可执行程序并放入/chaincode/output;InputStream指定了输入流,该流为example02的CDS.CodePackage;OutputStream指定了容器的输出流,该流最后也通过调用cutil.WriteBytesToPackage写入tw,随后流向input。在DockerBuild中,所做的就是根据选项先检查fabric-ccenv是否存在,若不存在则尝试下载,然后创建、启动fabric-ccenv容器,然后等待编译完成,最后将编译好的chaincode从/chaincode/output/中下载到输出流OutputStream并删除fabric-ccenv容器。(4)异步执行(3)后直接将input返回。返回到第21步(7)处builder执行完毕将input返回给reader,对接上文,reader就是接收example镜像数据。然后通过调用vm.deployImage,把reader作为镜像的输入流(即上下文,可以理解为以此镜像使用Dockerfile运行容器时Dockerfile能使用的哪个范围下的数据),将example02的镜像部署。继续第21步的(7)向后执行。这里需说明的是,这里启动的容器是example02镜像的Dockerfile指定的fabric-baseos,且ADD命令会将binpackage.tar(即名为chaincode的example02的可执行程序的压缩包)复制并解压到/usr/local/bin目录下,再者createContainer创建该容器的时候,配置Config中Cmd的值(相当于Dockerfile中的CMD)是最初在core/chaincode/chaincode_support.go中getArgsAndEnv(...)生成的args = []string{"chaincode", fmt.Sprintf("...},所以当client.StartContainer(containerID, nil)启动fabric-baseos时(准确的说是exmaple02镜像,fabric-baseos只是其基础镜像)会执行example02的程序chaincode -peer.address=0.0.0.0:7051。这里要清晰的区分,执行client.StartContainer(containerID, nil)的是peer结点(这个结点可以宿存在主机中,也可以宿存在一个docker容器中),执行chaincode -peer.address=0.0.0.0:7051的是example02容器。

  23. 创建example02的两个Handler。在新运行的example02容器中执行example02的程序chaincode,参看源码examples/chaincode/go/chaincode_example02/chaincode_example02.go,执行的func main中直接调用了shim.Start(new(SimpleChaincode)),该函数在core/chaincode/shim/chaincode.go中定义,相当于部署SCC时调用的StartInProc(参看《fabric源码分析18》章节Launch第9步),旨在启动一个example02的ShimHandler并通过grpc主动发送一个ChaincodeMessage_REGISTER类型消息给peer结点中example02的ServerHandler。传入的SimpleChaincode即为example02链码对象,相当于lscc的LifeCycleSysCC。具体的过程如下:(1)SetupChaincodeLogging(),设置viper在本容器内获取环境变量值的一些方法,如把前缀设置为CORE,把_替换为.,这样viper.GetString("chaincode.id.name")就可以获取第22步最后启动example02容器时设置的Env中CORE_CHAINCODE_ID_NAME=example02:antherversion的值,其次是获取其他的环境变量以设置日志输出级别等,这些环境变量均是最初在core/chaincode/chaincode_support.go中getArgsAndEnv(...)生成的,一路被传至example02的容器配置中,对看第22步中的createContainer(2)stream, err := streamGetter(chaincodename),获取一个grpc流,这个流是连接peer结点的ChaincodeSupport客户端流。其中peer的地址是通过flag.StringVar(&peerAddress,"peer.address"...)获取的,对应第22步最后启动example02容器时执行的程序是chaincode -peer.address=0.0.0.0:7051,即通过flag给定了peer结点的地址,在此则通过flag获取这个地址。这一步执行过后,由于streamGetter中执行了chaincodeSupportClient.Register(...)
    ,因此peer结点在core/chaincode/chaincode_support.go中的gprc服务端的Register(...)函数将被调用,进而调用HandleChaincodeStreamHandleChaincodeStream新创建了属于example02的ServerHandler并调用handler.processStream()启动了循环接收ShimHandler消息的for循环。(3)chatWithPeer(chaincodename, stream, cc),罗列一下传入该函数的参数:chaincodename值为example02:anotherversion;stream为连接peer结点的grpc客户端流;cc为example02链码对象SimpleChaincode自身。如同SCC的部署一样,通过chatWithPeer,先创建了属于example02的ShimHandler对象,将stream和cc赋值给ShimHandler相应成员,然后利用ShimeHandler向在peer结点中的ServerHandler发送了一条ChaincodeMessage_REGISTER类型消息,最后启动了循环接收ServerHandler消息的进程。(4)(2)中的ServerHandler收到(3)中ShimHandler发送的ChaincodeMessage_REGISTER消息,开始了注册的过程。这里的注册指的是用(2)新建的属于example02的ServerHandler把第21步(8)中prelaunchFunc()预Launch的Handler替换掉,这个过程省略,可对看《fabric源码解析18》章节Launch第12,13,14步。直到example02的ServerHandler和ShimHandler均达到ready状态。至此,第二次进行的Launch-Execute,Launch部分执行完毕,开始返回。中间过程省略,直接返回定位到对应第18步,core/chaincode/exectransaction.go的Execute(...)中,theChaincodeSupport.Launch(...)执行结束。

  24. 继续执行theChaincodeSupport.Execute(...),罗列一下传入的参数:ctxt,依旧包含这两个工具,交易模拟工具和历史查询工具;cccid,存放example02的数据和最初的部署申请数据,即图中的SignedProposal和Proposal;ccMsg,一个ChaincodeMessage_INIT类型消息,Payload存放的是example02的CDS.CS.Input,即命令行-c指定的{"Args":["init","a", "100", "b","200"],txid依旧是最初申请部署时的txid,Proposal在之后将被赋值为图中的SignedProposal;executetimeout超时时间。依旧对看《fabric源码解析18》章节Execute第1-5步,在此省略,直接定位到example02容器中运行的ShimHandler端core/chaincode/shim/handler.go的handleInit(msg)(ShimHandler接收到ServerHandler发来的ChaincodeMessage_INIT消息,状态机触发beforeInit事件函数,进而调用handleInit(msg)),在这个函数中:(1)stub := new(ChaincodeStub)stub.init(...)创建并根据收到的ChaincodeMessage_INIT消息初始化了一个ChaincodeStub。(2)handler.cc.Init(stub),调用了ShimHandler的cc(即example02的SimpleChaincode对象)的Init接口,可以定位到examples/chaincode/go/chaincode_example02/chaincode_example02.go中的Init(stub)(3)Init(stub)中,首先stub.GetFunctionAndParameters()获取了stub中的args中包含的函数和函数所用参数,即-c指定的函数init,参数a,100,b,200,分别看作a账户余额100,b账户余额200。然后stub.PutState(A, []byte(strconv.Itoa(Aval)))stub.PutState(B, []byte(strconv.Itoa(Bval)))将两个账户的初始状态提交。下文只以A账户状态为例。

  25. stub.PutState将触发ShimHandler的handler.handlePutState,之后的过程类似于第9-14步,只不过这时使用的ServerHandler和ShimHandler都是example02的,所提交的key是A的账户名,value是A的余额,最终也是将这一对key-value通过交易模拟工具在core/chaincode/handler.go的enterBusyState中提交到example02:anotherversion的写集(同第13步中的写集)中。然后返回到core/chaincode/shim/handler.go的handleInit(msg)中,随着handler.cc.Init(stub)的结束,触发defer发送ChaincodeMessage_COMPLETED消息。ServerHandler收到后通知core/chaincode/chaincode_support.go中的Execute(...)结束等待并返回,exectransaction.go中的Execute(...)也随之返回。至此,第二次进行的Launch-Execute,Execute部分执行完毕,开始返回。返回至core/endorser/endorser.go的callChaincode(...),对应第18步。继续返回至simulateProposal()中,接着进入if txsim != nil分支执行了txsim.GetTxSimulationResults(),获取了交易的读写集(这里主要是写集中的数据,是由第13步中lscc的写集写入的example02的链码数据ChaincodeData,此步中example02的写集写入的A/B两个账户的状态数据,当前操作的读集里面没有数据),然后返回,至ProcessProposal(...)中,这里罗列一下e.simulateProposal(...)返回的数据:cd为空;res为lscc部署example02时的返回结果Response,成功的结果中包含example02的ChaincodeData;simulationResult,交易的读写集(即目前所进行的交易的结果);ccevent为lscc部署example02时最终返回ChaincodeMessage_COMPLETED消息时所携带的ChaincodeStub中定义的事件,这里为空(core/chaincode/shim/handler.go的handleTransaction中)。继续,由于chainID不为空,将继续执行e.endorseProposal(...),在此罗列一下传入该函数的参数:ctxt;chainID,值为testchain;txid,交易ID;signedProp/prop对应图中的SignedProposal和Proposal;cd/res/res/simulationResult/ccevent均为上文返回的数据;hdrExt.PayloadVisibility为空;hdrExt.ChaincodeId只包含一个值为lscc的Name字段。

  26. endorseProposal(...)中,主要做的就是生成一个供escc使用的CIS,然后通过再次调用callChaincode来执行背书。具体过程如下:(1)确定要使用的进行背书的SCC的名字escc和版本号。(2)根据参数和准备的数据,生成一个ecccis,这个作用类似于ExecuteChaincode中的createCIS和图中的CIS,罗列一下Args的值:【0】函数名,为空;【1】图中Proposal的Header;【2】图中Proposal的Payload;【3】[]byte格式的ccid,这个ccid的Name值为lscc,version值为1.0.0;【4】[]byte格式的包含example02的ChaincodeData的成功返回结果;【5】交易读写集数据;【6】事件,为空;【7】payload的权限控制,为空,当前版本对这个字段并没有使用。后边的背书过程中所用到的数据均来自于此。(3)调用callChaincode

  27. callChaincode中,这次只会执行一次Launch-Execute过程,且Launch会中途返回,因为escc已经被Launch过。中途的过程对看《fabric源码解析19》执行申请章节第5步之后,一直对看到到Execute章节的第6步,只不过这期间一直使用的是escc的两个Handler,最后调用到的是escc的Invoke()方法对example02的交易结果进行背书。直接定位到core/scc/escc/endorser_onevalidsignature.go的Invoke,传入Invoke的stub是在core/chaincode/shim/handler.go的handleTransaction中生成的。

  28. Invoke中,只做了两件事:(1)分别将携带的Args中的每个值都解压出来,在此可以与第26步(2)处的对看,看看都是哪些数据。(2)根据解压出来的Args中的值,签名并整理应答数据,最后返回这个数据,这里不再详述。自此一路返回,过程省略,直接定位到endorseProposal中的e.callChaincode(...)执行完毕,对应第26步的(3)。接着返回至ProcessProposal(),至此,ProcessProposal()全部执行完毕,返回的数据是protos/utils/txutils.go中CreateProposalResponse生成的ProposalResponse,且该ProposalResponse的Response.Payload被赋值为example02的ChaincodeData。这里详解一下ProposalResponse的构成:(1)成员Version,明确版本固定为1。(2)成员Endorsement,包含了结点的签名者Endorser和签名者的签名Signature。(3)成员Payload,[]byte格式的ProposalResponsePayload,包含[]byte格式的ChaincodeAction,哈希过的图中的Proposal。ChaincodeAction相当于在描述一个chaincode动作,即谁干了什么事儿,产生了什么后果,包含的有:ChaincodeId里的数据是lscc和1.0.0,Response数据是lscc部署example02是返回的res,其中Payload包含了example02的ChaincodeData,Results则包含了lscc部署完example02后两条链码的读写集,Events为空。(4)一个Success的Response,Payload为example02的ChaincodeData。这样的一个ProposalResponse被返回给peer/chaincode/instantiate.go的instantiate中,cf.EndorserClient.ProcessProposal(...)执行完毕。

  29. instantiate继续执行,utils.CreateSignedTx,结合图中的Proposal,返回的ProposalResponse,生成“一封信”,即可供cf.BroadcastClient.Send(env)使用的common.Envelope。Envelope数据的封装过程是一个具体数据到被一层层包装进一个通用数据的过程,也需要耐心。

广播

  1. 在peer/chaincode/instantiate.go的chaincodeDeploy中,env, err := instantiate(cmd, cf)返回的Envelope紧接着被cf.BroadcastClient.Send(env)发送进行广播。
  2. peer命令的广播客户端为在peer/common/ordererclient.go中的broadcastClient,封装了一个grpc连接和一个AtomicBroadcast_BroadcastClient客户端对象(这个对象其实是AtomicBroadcastClient客户端的一部分,即Broadcast流客户端,在protos/orderer/ab.pb.go中定义)。从所在的文件名就可知,这是连接orderer服务的客户端。ordering服务客户端的地址是命令行中-o指定的,若未指定,则是在peer/common/common.go中的GetOrdererEndpointOfChain,通过向cscc发送请求获取的(这里又是一大堆文字)。这个连接在instantiate命令执行之初就已经被建立。
  3. cf.BroadcastClient.Send(env)将env发送到了orderer/server.go的Broadcast(...)处接收,自此数据交给了orderer服务中进行处理。由于涉及到orderer服务,这里只延续讲大概过程:即orderer服务将env数据排序后发送给peer结点的gossip服务开始散播,散播后,最终提交到网络中的每个结点的链(账本)上。这点在讲述orderer服务的时候会再次提及。

部署后的状态

  • example02的对应图中的ChancodeData数据被放入交易模拟器的写集中。
  • example02的镜像被创建,镜像的上下文包含Dockerfile文件,通过tls连接peer结点的证书,可执行程序压缩包。
  • example02的容器被创建,单独运行ShimHandler,并通过grpc与peer结点中ServerHandler通信。
  • example02的ServerHandler在peer结点的theChaincodeSupport.runningChaincodes.chaincodeMap中进行了注册。
  • example02的ShimHandler和ServerHandler均处于ready状态。
  • example02指定的{"Args":["init","a", "100", "b","200"],即A账户余额100,B账户余额200,这两个状态被放入交易模拟器的写集中。
  • example02部署的数据通过orderer服务排序后散播到网络的各个有效结点中并最终提交到各自的链(账本)上(这一点本不是peer部署example02本身做的事情,只是会促成的orderer服务和gossip服务要做的事情)。

后记闲言

ACC的部署至此结束,相当复杂,复杂处主要在于的是各层数据之间的包装、传递、解包验证和各自链码的两个状态机之间的缠斗,需要对照图中数据一点一点的磨。文中如有纰漏错误之处,可以留言指明。

猜你喜欢

转载自blog.csdn.net/idsuf698987/article/details/78353481
acc