fabric源码解析27——Channel

fabric源码解析27——Channel

概述

Channel在fabric中是一个相当重要的概念,可译作频道。对于channel的理解,不妨想象一下电视节目的频道和“我和你不在一个频道”这句话。Channel本身存在于orderer结点内部,但需要通过peer结点使用peer channel ...命令进行维护。一个peer结点要想与另一个peer结点发生交易,最基本的前提就是两个结点必须同时处在同一个Channel中,block账本与channel也是一对一的关系,即一个channel一个账本。Channel的基本动作如下:

表27-1(本文只讲解create,join,update三个动作)

动作 释义
create 在orderer结点内部创建一个channel。如,peer channel create -o orderer.example.com:7050 -c mychannel -f ./channel.tx
join 加入一个channel。如,peer channel join -b mychannel.block
update 升级channel的某一组织的配置。如,peer channel update -o orderer.example.com:7050 -c mychannel -f ./Org1MSPanchors.tx
list 列出当前系统中已经存在的所有的由peer结点创建的channel。
fetch 获取channel中的newest,oldest块数据或当前最新的配置数据。如,peer channel fetch config config_block.pb -o orderer.example.com:7050 -c mychannel

chaincode分为SCC和ACC,这里Channel也分为system channel和application channel。system channel是随着orderer结点运行之时根据genesis.block创建的,而通过peer channel ...维护的channel,均为application channel。对application channel发起维护命令的peer结点必须是所提交的配置文件(channel.tx,mychannel.block,Org1MSPanchors.tx)中所配置的组织中的一员,如peer0执行create,则peer0必须是channel.tx中所配置的组织中的一员,而channel.tx对应生成的block块数据,就相当于application channel中的genesis.block。这里所说的属于组织的一员,其实质是指该peer结点要持有该组织所颁发的证书。

目录

  • common
    • config - 配置数据处理
    • configtx - 配置交易处理
  • orderer
  • peer
    • channel - peer channel命令源码

配置文件

在表27-1的释义中,create/join/update三个动作都使用到了配置文件(数据):

  • channel.tx - application channel的创建配置文件,供peer channel create使用,由configtxgen工具根据指定的profile从configtx.yaml中读取配置数据。这里的profile指的是configtx.yaml中Profiles项下定义的某一个配置项。该文件主要规定了application channel中包含哪些组织,最终会被用作channel的配置原型,通过填补,作为channel账本的genesis块。例子:configtxgen -profile TwoOrgsChannel -outputCreateChannelTx ./channel.tx -channelID mychannel,指定了要生成的application channel的ID为mychannel,并从configtx.yaml的Profiles中选取TwoOrgsChannel项作为channel的具体配置,并把生成配置数据导入到./channel.tx文件中。channel.tx中存储的是二进制格式的Envelope,这点可以参看common/configtx/tool/configtxgen/main.go中的doOutputChannelCreateTx
  • mychannel.block - application channel的genesis.block,供join使用,由peer channel create动作生成。上文中说channel.tx只是配置原型,在create的过程中,会根据system channel中的配置,对channel.tx中的数据进行详细的填补,如填补orderer的配置(毕竟application channel是存在于orderer结点中的,不能对orderer的“规矩”一无所知),填补application channel所包含的组织的详细配置。填补了这些东西,最终生成一个block,作为application channel的genesis块被保存入账本。要想join一个application channel,就需要先获取这个channel的genesis块。
  • Org1MSPanchors.tx - application channel的某组织升级配置文件,供peer channel update使用,由configtxgen工具根据指定的组织ID从configtx.yaml中指定的profile项中生成。channel的配置中,基本项目之一就是频道所包含的组织了,而update命令就是升级channel配置中的某个组织的配置。例子:configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./Org1MSPanchors.tx -channelID mychannel -asOrg Org1MSP,指定从configtx.yaml的Profiles下的TwoOrgsChannel项中获取组织ID为Org1MSP的组织的配置数据,用于升级频道ID为mychannel的application channel,并把获取生成的配置数据导入到./Org1MSPanchors.tx文件中。同样,Org1MSPanchors.tx存储的是二进制的Envelope,可参看common/configtx/tool/configtxgen/main.go中的doOutputAnchorPeersUpdate

命令

create/update

create和update的操作从字面上讲一个是创建,一个是升级,在fabric都被处理成升级操作,create作为从无到有的升级操作,update作为从旧到新的升级操作。在peer/channel/create.go和update.go中:

  1. 如果把create的执行过程分为三步,那么update只是create的第一步而已。在create.go的createCmd -> create -> executeCreate(cf)中,过程可以简单的分为:(1)sendCreateChainTransaction(cf),向orderer发送创建application channel的配置数据。(2)getGenesisBlock(cf),多次尝试从orderer获取生成的application channel的genesis块,即此时application channel已经建立,上一步发送的配置数据经过填补已经作为第一块block被保存到频道的账本中。(3)ioutil.WriteFile(file, b, 0644),将获取的genesis块写入的文件中,供peer channel join命令使用。在update.go的update(...)中,完成的任务与executeCreate中的sendCreateChainTransaction(cf)一致,只不过发送的配置数据是用于更新application channel中的某个组织的配置数据。以下将主要以create的执行过程为例,辅以update的不同之处进行讲述。
  2. 开始第一步。在sendCreateChainTransaction中,(1)根据命令行指定的channelTxFile,也即channel.tx文件路径,chCrtEnv, err = createChannelFromConfigTx(channelTxFile)读取配置数据到chCrtEnv中。(2)sanityCheckAndSignConfigTx(chCrtEnv),对看函数名,理性的检查并对chCrtEnv进行签名,sanity-头脑清晰,心智健全的意思,所以这里的意思就是只要你没疯,就在将配置数据发往orderer之前检查一下。但同时又说明,这样的检查不是非要不可,因为channel.tx本身是configtxgen工具生成的,只要工具规规矩矩不搞鬼,那这里不检查也可以。后边的步骤中还会有多次这样的sanity类型的检查。签名的话就是正常的签名,哪个peer结点发起的peer channel create,在chCrtEnv中留下签名即可。(3)broadcastClient.Send(chCrtEnv),向orderer结点发送chCrtEnv。
  3. 在orderer/server.go的Broadcast(...)中的s.bh.Handle(srv) -> orderer/common/broadcast/broadcast.go的Handle(...)中的msg, err := srv.Recv(),orderer接收到第2点中peer发来的原始的“配置信”msg,接着开始从msg中抽取数据。因为是“配置信”,因此会进入if chdr.Type == int32(cb.HeaderType_CONFIG_UPDATE)分支,在此分支中,msg, err = bh.sm.Process(msg)对原始的“配置信”msg进行了重新整理:过滤重复的配置,若是peer channel create的话,过滤后的配置还会被装进一个以system channel的ID为外衣的“配置信”中,形成新的“配置信”msg。
  4. bh.sm.Process(msg)最终调用的是orderer/configupdate/configupdate.go的Process(...),在此函数中,support, ok := p.manager.GetChain(channelID)即获取了所需要的资源,也验证了“配置信”所对应的频道是否存在,若存在,则说明此“配置信”是用于update已有频道的配置的,则进入if ok分支执行p.existingChannelConfig(...),否则,则说明此“配置信”是用于create新的频道的,则跳过if ok分支去执行p.newChannelConfig(...)。这里p.existingChannelConfig(...)依然算是雷同于p.newChannelConfig(...)中的一部分。
  5. p.newChannelConfig(...)中,(1) ctxm, err := p.manager.NewChannelConfig(envConfigUpdate)这一步比较有意思,在orderer/multichain/manager.go的NewChannelConfig,相当于费了九牛二虎之力验证原配置数据,并填补了一些system channel的配置,又到common下的config和configtx遛了一大圈生成了一个内含application channel新的配置的configManager(这个过程相当复杂费劲),但是这个configManager仅仅停留在当前函数中且并没有返回供调用者继续使用,而且envConfigUpdate虽然是以指针的形式传进去的,但是在NewChannelConfig并没有改变envConfigUpdate中的值。而下文newChannelConfigEnv, err := ctxm.ProposeConfigUpdate(envConfigUpdate)又直接使用原envConfigUpdate来生成application channel的频道“配置信”。如此种种会感觉NewChannelConfig做了很多无用功,但这里就属于上文提及的类似于sanity类型的验证,因为后续步骤在真正创建application channel时也会经历NewChannelConfig和创建频道所使用的configManager,所以这里相当于事前先创建一下,若有问题趁早发现趁早返回。(2) newChannelConfigEnv, err := ctxm.ProposeConfigUpdate(envConfigUpdate),根据原有的配置数据,利用configManager的ProposeConfigUpdate功能,通过对比“配置信”中读集和写集,将已有且版本相同的配置项过滤掉,生成要创建的application channel的频道配置数据newChannelConfigEnv。这里的读集和写集可以对看生成channel.tx的函数doOutputChannelCreateTxpeer channel update所调用的existingChannelConfig(...)中执行的是support.ProposeConfigUpdate(...)与这里的ctxm.ProposeConfigUpdate(...)实际是一样的,都是configManager的ProposeConfigUpdate,只不过existingChannelConfig(...)中使用的是已存在的application channel的configManager(包含在chainSupport->ledgerResources中)的ProposeConfigUpdate。这个已存在的configManager正好也证明了(1)所述的后文在创建频道时还会创建频道所使用的configManager。(3) newChannelEnvConfig, err := utils.CreateSignedEnvelope(...),对新生成的频道的“配置信”进行签名,类型为HeaderType_CONFIG。peer channel update所调用的existingChannelConfig(...)同样执行了这一步,且就此返回该“配置信”。(4) p.proposeNewChannelToSystemChannel(...),将签名过的“配置信”装到一个以system channel为频道ID的“配置信”,且类型变为HeaderType_ORDERER_TRANSACTION,然后返回新的“配置信”。这一点是peer channel create独有的,因为创建application channel要使用system channel的chainSupport对象。
  6. 重回orderer/configupdate/configupdate.go的Process(...)中,继续,对原始的“配置信”处理之后,support, ok := bh.sm.GetChain(chdr.ChannelId),根据频道ID获取chainSupport对象,peer channel create获取的是system channel的chainSupport,peer channel update获取的是对应application channel的chainSupport。
  7. _, filterErr := support.Filters().Apply(msg),由第6点获取的chainSupport获取频道的过滤器集合并对“配置信”进行Apply()过滤。只讲peer channel create,获取的是system channel的过滤器集,具体为orderer启动时,在orderer/multichain/chainsupport.go的createSystemChainFilters(...)生成。依次进行非空、大小、签名的检查后,最后调用systemChainFilter的Apply(),即orderer/multichain/systemchain.go的Apply(env)。在systemChainFilter的Apply(env)中,一系列抽取“配置信”并检查之后,(1) scf.authorizeAndInspect(configTx)对“配置信”整理和检查。这里需要明确一点,从peer发送到orderer接收至此,“配置信”均只有配置项而没有具体的配置值,是不完整的。而直到这一步才对“配置信”进行配置值的填充。在authorizeAndInspect(...)中,我们如愿的看到了NewChannelConfigconfigtx.NewManagerImpl(...)的身影,即根据system channel的配置填充“配置信”对应配置项的值和新建application channel所使用的configManager。这也同时证明了第5点所说的有关sanity类型检验的说法所言非虚。(2) return filter.Accept, &systemChainCommitter{...},返回Accept动作和包含了完整的“配置信”的systemChainCommitter。这里注意,这一步主要目的有两个:验证和填充“配置信”,system channel的过滤器集合Apply()后返回的执行器集合被_省略,是因为后文会再次生成,在block写入账本之前调用执行器。
  8. support.Enqueue(msg),把“配置信”作为一条消息发送给kafka(或solo),具体是由orderer/kafka/chain.go中chainImpl的Enqueue(...)发给kafka的(又由于support是system channel的chainSupport,因此这里的chainImpl是system channel对应的对象,其成员support也是system channel的chainSupport),经过kafka暗盒的排序处理,配置信被包裹在一条KafkaMessage_Regular消息,在orderer/kafka/chain.go的processMessagesToBlocks()中被接收,并进入case *ab.KafkaMessage_Regular:分支,交由processRegular(...)处理。在processRegular(...)中,将“配置信”抽取出来后:(1) batches, committers, ok := support.BlockCutter().Ordered(env),由“配置信”生成block,由于是“配置信”,因此会单独作为一个block,且support是system channel的chainSupport,获取的执行器集合committers也是system channel的过滤器集合Apply(env)之后返回的,即第7点末尾中提及的被_省略掉的过滤器集合,在这里又被重新生成。执行集合中只包含第7点(2)中所述的systemChainCommitter。(2) for i, batch := range batches,在循环中依次处理每批消息,将每批消息打包成block,然后support.WriteBlock(block, committers[i],...)在账本中写入block。(3) support.WriteBlock(...)执行的是orderer/multichain/chainsupport.go的WriteBlock(...),在这个函数中,首先在for _, committer := range committers循环中依次执行committer.Commit(),即将执行器集合兑现,这里因为只有一个systemChainCommitter,因此执行的是orderer/multichain/systemchain.go的Commit() -> scc.filter.cc.newChain(scc.configTx),最终执行的是orderer/multichain/manager.go的multiLedger的newChain(...),在这里,正式的创建了application channel。可以看到,创建application channel的最终效果就是:创建了频道的账本(且写入“配置信”作为genesis块)并在orderer的multiLedger对象成员chains中添加一个chainSupport对象并启动了相应的服务。而上文提及的peer channel update使用到的chainSupport对象,也就是这里创建的。(4)还是在WriteBlock(...)中,兑现执行器集合后,cs.ledger.Append(block)也会把application channel的genesis块写入system channel的账本。执行至此,peer channel create在orderer端的工作基本结束。
  9. 重新返回到orderer/common/broadcast/broadcast.go的Handle(...)中,定位到support.Enqueue(msg)之后,之后的srv.Send(..._SUCCESS)就是orderer结点处理完毕创建新的application channel的工作后,向发起peer channel create动作的peer结点返回成功的应答。这里注意一下peer结点接收这个应答的地方是在broadcastClient的Send(...)接口内部(参看peer/commono/ordererclient.go的Send(...)实现中的getAck()),也即当peer/channel/create.go的sendCreateChainTransaction中执行完broadcastClient.Send(chCrtEnv),在Send(...)内部已经接收到了来自orderer结点的应答信息。至此,第一步结束。对于peer channel update动作来说,至此整个动作结束。
  10. 开始第二步。重新返回peer/channel/create.go的executeCreate(cf)中:block, err = getGenesisBlock(cf),在一定时限内(默认5s,可由peer channel create命令行的-t指定),利用deliver服务每隔200毫秒向orderer结点的指定的application channel索要一次序号为0的block,即上文第2-8所创建的application channel的genesis块,直到成功获取或超时。这一步不展开细讲。
  11. 开始第三步b, err := proto.Marshal(block) -> ioutil.WriteFile(file, b, 0644),将第二步获取的block以application channel ID+.block的格式写入文件,供peer channel join动作使用。至此,整个peer channel create动作执行完毕。
join

peer channel join动作看上去很像是peer要加入channel,而channel又在orderer结点中,所以顺气自然的就会认为peer需要发送一些自己的数据到channel,然后channel接收这些数据后添加到自身的对象中。其实join的动作完全是peer结点本地化自身数据和服务,以达到和对应存在于orderer结点的application channel的数据和服务相配套的一个过程。数据主要指账本,服务主要指gossip等模块的服务。这里假设peer channel create创建的是一个ID是mychannel的application channel,对应生成的即为mychannel.block文件。在peer/channel/join.go中:

  1. joinCmd(cf) -> join(...) -> executeJoin(cf)中,(1) spec, err := getJoinCCSpec()创建了一个关于cscc的ChaincodeSpec格式的“说明书”,其中的ChaincodeInput作为scc执行的输入参数,指定了两项:动作cscc.JoinChain、从mychannel.block中读取的mychannel的genesis块数据。(2) invocation := &pb.ChaincodeInvocationSpec{...} -> prop, _, err = putils.CreateProposalFromCIS(...) -> signedProp, err = putils.GetSignedProposal(...),包装+签名,形成一个背书申请。(3) cf.EndorserClient.ProcessProposal(...),通过背书客户端,发起背书请求。
  2. 背书过程省略。可参看peer chaincode ...的背书过程,如《fabric源码解析18-20》。最终直接定位到core/scc/cscc/configure.go。
  3. 在configure.go的Invoke(stub)中,根据第1点(1)中所述的“说明书”,将进入case JoinChain:分支:(1) block, err := utils.GetBlockFromBlockBytes(args[1])从第二个参数中获取mychannel的genesis块,然后是一系列的检查验证,这里略过不讲。(2) joinChain(cid, block),最后执行join的具体动作。
  4. joinChain(cid, block)中:(1) peer.CreateChainFromBlock(block)(core/peer/peer.go),创建peer结点本地的针对mychannel的链(账本)和gossip服务。这个系列之前关于chaincode,gossip主题的文章中涉及使用的账本,gossip服务,均是在此生成的。(2) peer.InitChain(chainID)(core/peer/peer.go),初始化peer结点本地的链。(3) producer.SendProducerBlockEvent(block),创建事件服务,此为监控服务,在此不述。
  5. 详解第4点的(1),在CreateChainFromBlock(block)中:(1) l, err = ledgermgmt.CreateLedger(cb),根据mychannel的genesis块,创建peer本地专供mychannel使用的账本。(2) createChain(cid, l, cb),创建针对mychannel使用的链对象,在这个函数中,着重看一下service.GetGossipService().InitializeChannel(...),该句初始化了peer结点本地gossip模块中专供mychannel使用的gossip服务,并在此gossip服务与orderer结点之间建立了服务连接,由此peer结点就可以源源不断的从orderer结点中的mychannel频道索要block数据块并添加到本地的用于mychannel的账本。
  6. 详解第4点的(2),在InitChain(chainID)中:只执行了chainInitializer(cid),而chainInitializer这个函数变量,是在peer结点启动起来的时候被赋值的,即在peer/node/start.go的serve中执行peer.Initialize(...)时被赋值的,这里给chainInitializer所赋的值是func(cid string){ scc.DeploySysCCs(cid) },即部署了指定频道ID的system chaincode,也即初始化peer结点的mychannel的链,其实就是在mychannel链上部署的scc。如此,peer chaincode ...动作在mychannel上执行的过程中所使用到的scc也就绪了。这里所部署的针对mychannel的scc要与peer结点启动时部署的scc(peer/node/start.go的serve中执行的initSysCCs())做区分,即如peer chaincode ...命令若是没有指定频道ID,默认使用的是peer结点启动时部署的scc,否则使用的是具体的针对某一频道ID的链上的scc。
  7. 重回core/scc/cscc/configure.go的joinChain(cid, block)中,当第4点执行完毕,则背书过程基本结束,开始一路返回,过程省略,可以直接定位回到peer/channel/join.go的executeJoin(cf)中,在接收到背书的返回结果proposalResp后,整个join动作也宣告完成。

结语

以上即为Channel关于create/update,join的执行过程。所述过程线条较粗,尤其是create执行过程中涉及到比较多且复杂的对配置数据的各种变形和处理(主要集中在common/config,common/configtx两个目录中)都进行了省略(若细讲会扰乱主线),读者可以自行深研。其实关于Channel的文章,应该放在类似chaincode、gossip、orderer等主题文章之前的,但笔者能力所限,一直没太吃透,以致拖延至此,所以也建议读者读完此篇文章后,可以重看一下前面的文章。

你可能感兴趣的:(Fabric)