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结点要持有该组织所颁发的证书。
在表27-1的释义中,create/join/update三个动作都使用到了配置文件(数据):
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
。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块。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中:
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的不同之处进行讲述。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。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。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(...)
中的一部分。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的函数doOutputChannelCreateTx
。peer 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对象。Process(...)
中,继续,对原始的“配置信”处理之后,support, ok := bh.sm.GetChain(chdr.ChannelId)
,根据频道ID获取chainSupport对象,peer channel create
获取的是system channel的chainSupport,peer channel update
获取的是对应application channel的chainSupport。_, 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(...)
中,我们如愿的看到了NewChannelConfig
,configtx.NewManagerImpl(...)
的身影,即根据system channel的配置填充“配置信”对应配置项的值和新建application channel所使用的configManager。这也同时证明了第5点所说的有关sanity类型检验的说法所言非虚。(2) return filter.Accept, &systemChainCommitter{...}
,返回Accept动作和包含了完整的“配置信”的systemChainCommitter。这里注意,这一步主要目的有两个:验证和填充“配置信”,system channel的过滤器集合Apply()
后返回的执行器集合被_省略,是因为后文会再次生成,在block写入账本之前调用执行器。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端的工作基本结束。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
动作来说,至此整个动作结束。executeCreate(cf)
中:block, err = getGenesisBlock(cf)
,在一定时限内(默认5s,可由peer channel create
命令行的-t指定),利用deliver服务每隔200毫秒向orderer结点的指定的application channel索要一次序号为0的block,即上文第2-8所创建的application channel的genesis块,直到成功获取或超时。这一步不展开细讲。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中:
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(...)
,通过背书客户端,发起背书请求。peer chaincode ...
的背书过程,如《fabric源码解析18-20》。最终直接定位到core/scc/cscc/configure.go。Invoke(stub)
中,根据第1点(1)中所述的“说明书”,将进入case JoinChain:
分支:(1) block, err := utils.GetBlockFromBlockBytes(args[1])
从第二个参数中获取mychannel的genesis块,然后是一系列的检查验证,这里略过不讲。(2) joinChain(cid, block)
,最后执行join的具体动作。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)
,创建事件服务,此为监控服务,在此不述。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的账本。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。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等主题文章之前的,但笔者能力所限,一直没太吃透,以致拖延至此,所以也建议读者读完此篇文章后,可以重看一下前面的文章。