Hyperledger Fabric技术内幕:架构设计与实现原理
上QQ阅读APP看书,第一时间看更新

1.2 Hyperledger Fabric基本概念与架构

本节将首先介绍Hyperledger Fabric的基本概念与架构,接着运行e2e_cli示例演示实验环境的部署步骤,最后分析Fabric系统的初始化启动流程与交易处理流程。

1.2.1 基本概念

本节介绍Hyperledger Fabric的基本概念,包括Peer节点、Orderer排序节点、Client客户端等。

1. Peer节点

在Hyperledger Fabric网络中,Peer节点指提供交易背书、交易验证、提交账本等服务功能的逻辑节点,包括Endorser背书节点、Committer记账节点等,通常采用进程实例(或线程、goroutine等)与功能模块的实现方式,运行在物理服务器、Docker容器等环境中提供服务。因此,不同功能角色的Peer节点可以同时运行在同一个物理节点、虚拟机或容器中。其中,推荐对性能有严格要求的生产环境将Peer节点部署在物理节点上,开发测试环境可以考虑虚拟机或容器环境以方便开发调试与测试。

类似于P2P网络,Fabric中每个Peer节点的功能地位都是对等的,它们之间通过服务分工协作以响应来自Fabric客户端(包括CLI命令行客户端和多种语言SDK客户端)的交易请求消息,并共同维护Fabric分布式账本的数据一致性。目前,Fabric提供了如下两种功能角色的Peer节点,其中:

□ Endorser背书节点:负责接收来自客户端的签名提案消息请求,检查消息后模拟执行交易提案,并对模拟执行结果签名背书,即使用私钥对请求提案、状态变更(读写集)等签名,表示Endorser背书节点认为此次交易是合法有效的,然后将签名背书信息等打包成提案响应消息回复给客户端;

□ Committer记账节点:负责检查交易消息结构的完整性与合法性、调用VSCC验证交易背书策略、执行MVCC检查读写集冲突等,标记交易的有效性并提交账本,更新本地账本数据库与文件,包括区块数据文件、隐私数据库、区块索引数据库、状态数据库、历史数据库等。

2. Orderer排序节点

Orderer排序节点同样属于逻辑节点,负责管理系统通道与应用通道,维护通道账本与配置,提供Broadcast交易广播服务、Orderer共识排序服务、Deliver区块分发服务等。

通常,Orderer节点通过Broadcast()服务接口接收与处理交易消息请求(普通交易消息与配置交易消息),过滤检查后提交共识组件进行排序,并添加到本地待处理的缓存交易消息列表,按照约定的交易出块规则(如配置出块时间、配置出块字节数限制、通道配置消息单独出块等)切割打包成新区块,再保存到本地账本的区块数据文件中。对于配置交易消息,Orderer节点还负责创建新的应用通道或更新通道配置。同时,Orderer节点通过Deliver()服务接口接收与处理区块请求消息,从本地账本中获取请求范围内的区块数据回复给请求节点。因此,Orderer节点在整个Fabric系统中属于核心功能模块,其处理交易排序达成共识的性能将直接影响到整个Fabric系统的出块效率。

另外,Orderer节点支持独立的多通道(Channel)管理,包括系统通道与应用通道,可以保持各通道内节点的账本数据彼此隔离,账本数据只会同步给加入通道的合法Peer节点,从而确保数据的隐私性与安全性。

3. Client(客户端)

客户端(Client)是用户与Fabric网络组件发送请求进行交互的接口,包括如下两种客户端。

□ Fabric-CA客户端:负责节点注册登记,包括登记注册用户信息、获取注册证书与私钥信息等;

□ Fabric客户端:负责网络配置与节点管理,包括初始化与更新配置、启动和停止节点等。同时,还负责通道管理(创建、更新、查询等)与链码生命周期管理(安装、实例化、调用、升级等),能够通过Peer节点服务客户端发送消息给Endorser节点与Orderer节点请求处理,包括交易背书、创建通道、更新通道配置、交易排序、请求区块数据等。

目前,Fabric客户端包括CLI(Command Line Interface)命令行客户端和多种语言SDK客户端,如Node.js、Go、Java、Python等,负责与其他服务节点进行交互,提供配置操作、通道操作、链码操作、节点操作、日志操作等相关API接口,支持开发丰富的应用程序。

4. CA节点

CA(Certificate Authority)节点类似于证书机构,提供用户身份注册服务,基于数字证书与标准的PKI服务管理Fabric网络中的成员身份信息,管理证书生命周期如创建、撤销、认证等操作,以及身份鉴别与权限控制功能,包括Ecerts(身份证书)、Tcerts(交易证书)等。目前,Fabric中的身份证书符合X.509标准规范 [17],并且基于ECDSA算法生成公钥与私钥。

通常情况下,任何合法的成员实体都需要在接入网络前获取认证签署的身份证书等,而不需要在运行过程中一直访问CA节点。因此,Fabric-CA节点是相对独立的组件,不会在网络运行时影响到其他流程和节点的状态。

5. Gossip消息协议

Gossip消息协议(流言算法)[22]自20世纪70年代提出到现在已经有40多年的历史,该算法简单高效,具有良好的可扩展性和鲁棒性,被广泛应用于分布式定位、数据库复制等领域,如分布式系统Cassandra用于实现集群失败检测与负载均衡。Gossip消息协议规定,节点采用随机选择近邻节点的方式进行路由并交换信息,近邻节点重复这一过程将其传播给没有数据的节点,直到所有节点收到数据为止。这种方式可以有效避免拥塞和路径失效问题,同时拥有节点数对数量级的较低时间复杂度,常见的数据传输模式包括push模式(主动推方式)、pull模式(主动拉方式)和push/pull模式(推拉结合方式)。

Fabric中的Gossip消息模块可以提供Gossip消息协议服务,负责在应用通道中的组织(通常对应于一个MSP对象)内探测节点成员、在节点间分发区块数据以及同步状态等,用于管理新加入的通道节点,发送成员关系请求消息获知其他节点信息,在组织内分发数据(区块数据与隐私数据)与同步状态,同时使用反熵算法周期性地从其他节点拉取本地缺失的数据(区块数据与隐私数据),以确保组织内所有节点上账本数据的一致性。

6.共识(Consensus)

共识(Consensus)算法通常是指参识节点对一段时间内发生的一批交易状态达成一致观点的计算方法,并按照规则将这些交易打包成区块,以保证同一个通道上所有的节点账本最终具有相同的状态,至少要保证参识节点在以下几个方面的一致性观点。

□ 区块的基本属性,如区块号。

□ 区块内交易对象的排列顺序,如按照接收交易的先后时间排序。

□ 区块内交易的数量,受限于出块时间配置、区块字节数限制、通道配置交易单独出块等规则。

□ 交易合法性与有效性规则,如VSCC验证交易背书策略、MVCC检查交易数据读写集版本冲突等。

□ 容忍恶意节点数据或故障节点数据的规则,如PBFT(实用拜占庭容错)算法。

因此,Hyperledger Fabric达成共识的过程包含在交易背书阶段、交易排序阶段、交易验证阶段等,其中:

□ 背书阶段:Endorser背书节点负责检查签名提案消息并模拟执行,对模拟结果读写集等添加签名,表示予以背书支持,客户端只有收集到满足条件数量的背书信息之后,才允许构造交易请求提交给Orderer节点,同时会在交易验证阶段检查这些背书信息是否满足指定的背书策略,以确保交易背书的合法性;

□ 排序阶段:目前,Orderer节点支持两类共识组件,包括Solo类型(用于单节点测试)与Kafka类型(基于Kafka集群)。这两类共识组件都是先利用Golang通道、Kafka集群等对接收到的合法消息(符合通道处理要求)进行排序,对交易顺序等达成一致再添加到缓存交易消息列表中,并按照约定的交易出块规则切割打包构造新区块,以保证全局一致的区块顺序、区块内交易顺序与交易数量等。另外,Fabric 0.6中测试的sbft共识组件则是参识节点先将交易分割打包到区块中,经过sbft共识算法(简外的BFT算法)确定区块顺序的一致性以达成共识,最后写入账本;

□ 验证阶段:Committer记账节点负责验证排序后的交易数据,包括检查交易结构格式的正确性、调用VSCC链码验证交易背书签名是否满足预设指定的背书策略、执行MVCC检查交易数据读写集的版本冲突等,标记交易的有效性,并提交到区块数据文件中,建立索引更新到区块索引数据库。

7.成员关系服务提供者(MSP)

MSP(Membership Service Provider,成员关系服务提供者或成员关系服务模块)是Fabric中提供身份验证的实体抽象概念,基于X.509标准的身份证书实现对不同资源实体(成员、节点、组织、联盟等)进行认证等权限管理操作,同时提供数字签名算法与身份验证算法。

Fabric中属于同一个MSP组件内的成员都拥有共同信任的根证书,支持共享敏感数据。一个组织或联盟都可以对应一个层级化的MSP实例,通常一个MSP对象负责一个组织或联盟对象。MSP对象包括MSP名称ID、信任的根证书、中间证书列表、管理员身份证书、组织单元列表、CRL(证书撤销列表)等。

8.组织(Organization)与联盟(Consortium)

组织(Organization)表示多个成员的集合,通常拥有共同信任的根证书(根CA证书或中间CA证书)。组织下的所有成员被认为拥有同一个组织身份。目前,存在普通成员角色(Member)和管理员角色(Admin)两类组织成员,后者具有修改组织配置的权限。组织对象通常包括组织名称、组织所属MSP名称(MSP ID)、MSP对象、锚节点列表等信息。

联盟(Consortium)表示相互合作的多个组织集合,使用相同的Orderer服务,拥有相同的通道创建策略(ALL、MAJORITY或ANY),其中,MAJORITY策略要求联盟内必须要超过一半的成员都同意才能创建新通道。

9.交易(Transaction)和区块(Block)

交易(Transaction,或称为事务)是Fabric的核心概念,通常是指通过调用链码(智能合约)改变账本状态数据的一次操作。对账本状态的变更是用交易结果读写集来描述的,将交易集合经过Orderer节点排序后按规则打包到区块中。目前,Hyperledger Fabric包括普通交易消息、配置交易消息等,其中,普通交易消息封装了变更账本状态的执行交易结果,需要经过排序后打包成区块,配置交易消息则用于创建新的应用通道或更新通道配置,通常在排序后单独打包成区块,同时将最新配置区块号更新到最新的区块元数据中以便于索引查找。

区块(Block)是指一段时间内发生的交易集合,经排序后按规则打包后并添加签名、哈希值、时间戳与其他元数据所构成的数据结构,而区块链就是以区块为基础按照时间顺序连接构成的链状数据结构。Fabric中的区块结构(Block类型)包括区块头Header、交易数据集合Data以及区块元数据Metadata三个部分,其中,区块头Header封装了区块号、前一个区块的哈希值、当前区块的哈希值,交易数据集合Data封装了打包的交易集合,区块元数据Metadata封装了如下4个元数据索引项,其中:

□ BlockMetadataIndex_SIGNATURES:区块签名;

□ BlockMetadataIndex_LAST_CONFIG:最新配置区块的区块号;

□ BlockMetadataIndex_TRANSACTIONS_FILTER:最新交易过滤器,封装了交易数据集合Data中所有交易对应的交易验证码,标识其交易的有效性。

□ BlockMetadataIndex_ORDERER:Orderer配置信息,如Kafka共识组件的初始化参数。

通常情况下,Orderer节点根据交易出块规则(出块时间限制、区块字节数限制、配置交易单独出块等)来确定是否将收到的一批交易消息排序切割打包成区块。

10.链码(Chaincode)

链码(Chaincode)或链上代码就是Hyperledger Fabric中的智能合约,分为系统链码和用户链码。通常情况下,链码要经过安装和实例化(部署)步骤之后才能正常调用,同时必须实现Chaincode类型接口的Init()方法与Invoke()方法。

系统链码在节点启动或初始化新链结构(节点加入通道、节点启动恢复等)时完成部署,用于支持配置管理、背书签名、链码生命周期管理等系统功能,并运行在goroutine中,目前支持如下5类系统链码。

□ CSCC(Configuration System Chaincode):配置系统链码,负责管理系统配置,支持的命令包括JoinChain节点加入应用通道、GetConfigBlock获取通道配置区块、UpdateConfigBlock更新通道配置区块、GetChannels获取节点加入的通道列表等;

□ ESCC(Endorsement System Chaincode):背书管理系统链码,负责对模拟执行结果背书签名,并创建提案响应消息,同时管理背书策略;

□ LSCC(Lifecycle System Chaincode):生命周期系统链码,负责管理用户链码的生命周期,如打包、安装、实例化(部署)、升级、调用、查询等链码操作;

□ QSCC(Query System Chaincode):查询系统链码,负责查询账本和区块链信息,支持的命令包括GetChainInfo获取区块链信息、GetBlockByNumber获取指定区块号的区块数据、GetBlockByHash获取指定区块头哈希值的区块数据、GetTransactionByID获取指定交易ID的交易数据、GetBlockByTxID获取指定交易TxID的区块数据等;

□ VSCC(Verification System Chaincode):验证系统链码,负责对交易数据进行验证,并检查签名背书信息是否满足预定的背书策略。

用户链码是用户编写的智能合约代码,通常运行在Docker容器中,支持打包、安装、实例化(部署)、升级、调用等链码操作。

11.通道(Channel)与链(Chain)

通道(Channel)是Fabric的核心概念,通常是指Orderer排序节点管理的彼此隔离的原子广播渠道,提供隔离Peer节点信息的重要机制。链或链结构(Chain)包含关联通道上的账本区块及其交易数据、通道配置、链码信息等,并将账本上的区块链接起来构成线性数据结构。

Peer节点在加入应用通道时会主动创建关联通道的链结构对象,以管理本地节点上该通道的账本、配置、链码信息等,接收保存来自Orderer节点或其他节点的通道账本数据。通常,由通道组织Leader主节点负责从Orderer节点请求获取通道账本的区块数据,并分发到组织内的其他节点。另外,隐私数据(明文)也会在通道上组织内授权的节点间传播。因此,通道上的数据只会发送给加入通道的合法组织成员,从而隔离未经授权的数据访问,保护数据隐私性。

目前,Fabric上的通道分为应用通道(Application Channel)和系统通道(System Channel),其中:

□ 应用通道:保存Application配置(组织信息等)等,为上层应用程序处理交易提供隔离机制,在指定通道的组织成员间共享账本数据。客户端向Orderer节点发送通道配置交易消息创建应用通道,并生成该通道的创世区块(Genesis Block),同时还可以提交新的通道配置交易消息以更新通道配置。另外,应用通道账本上还保存了应用通道的创世区块、配置区块(更新通道配置)与普通交易区块。

□ 系统通道:保存Orderer配置(共识组件类型、服务地址、出块规则、通道数量等)等,基于系统通道配置与应用通道配置交易消息创建新的应用通道,并将其注册到Orderer节点的多通道注册管理器Registrar对象上,同时启动通道共识组件链对象,从而能够正常处理应用通道上的交易消息请求。另外,系统通道账本上还保存了系统通道的创世区块、所有应用通道的创世区块及其更新的配置区块。

注意,Fabric创建或更新应用通道时必须指定通道配置交易文件。

12.账本(Ledger)

Fabric账本(Ledger)提供了多个数据库与文件用于存储账本数据,且每个通道都拥有物理或逻辑上独立的账本对象,具体如下。

□ idStore数据库:保存账本ID信息,将账本ID与创世区块字节数组构成的键值对保存到LevelDB数据库中;

□ 区块数据文件:保存所有区块数据,用于存储所有交易状态发生变更的历史记录;

□ 隐私数据库:保存隐私数据(明文),支持LevelDB数据库;

□ 状态数据库:记录最新的世界状态(World State),即状态变更结果,保存有效交易的公共数据、隐私数据哈希值与隐私数据,支持LevelDB与CouchDB数据库;

□ 历史数据库:记录交易(经过Endorser背书的有效交易)中每个状态数据的历史变化信息,支持LevelDB数据库;

□ 区块索引数据库:存放区块索引信息,支持按照区块头哈希值、区块号、区块交易ID检查区块的文件位置,支持按照交易ID、区块号与交易序号检查交易的文件位置等,支持LevelDB数据库;

□ transient隐私数据库:Fabric 1.1.0开始支持隐私数据集合的实验版本新特征(1.2.0及后续版本支持),用户可以在智能合约的交易执行中获取与保存隐私数据。Peer节点将通过Gossip协议传播的隐私数据交由transient隐私存储对象暂时保存到本地的transient隐私数据库(LevelDB),并在将区块更新提交到账本的隐私数据库时,再自动清理相关的隐私数据记录,以保持数据的时效性。

13.策略管理(Policy)

Fabric采用策略这种权限管理方法对系统资源的访问权限进行控制,包括交易背书策略、链码实例化策略、通道管理策略等。策略(Policy类型,含策略类型Type与策略内容Value)定义了指定类型的验证规范,以检查请求访问的节点签名数据是否符合规范要求,具体如下。

□ SignaturePolicy:支持单个角色的签名策略SignaturePolicy_SignedBy类型与多个角色的签名组合策略SignaturePolicy_NOutOf_类型,后者是在前者的基础上支持多个签名实体的AND、OR、NOutOf等签名组合策略,NOutOf是指满足m个条件中的n个即可满足策略(mn);

□ ImplicitMetaPolicy:隐式元策略,适用于通道管理策略,是在SignaturePolicy签名策略的基础上定义子策略时所应该满足的规则,通常采用递归方式进行定义,支持ImplicitMetaPolicy_ANY(任意子规则满足)、ImplicitMetaPolicy_ALL(全部子规则满足)、ImplicitMetaPolicy_MAJORITY(超过一半的子规则满足)。

常见的策略具体如下。

□ 交易背书策略:用于在实例化链码时指定交易的背书规则,客户端需要收集到足够多的有效签名背书,并通过背书策略的验证(VSCC)才能确定该交易是合法有效的。交易背书策略可以在命令行中通过-P选项指定,并使用背书主体角色的布尔表达式(AND、OR等组合)进行描述,默认背书策略要求指定MSP(默认名称“DEFAULT”)成员签名;

□ 链码实例化策略:指定实例化链码或升级链码的权限,通常是在打包链码package时通过-i选项指定的,默认是通道的组织管理员身份(ADMIN)。链码实例化或升级时将链码数据对象(ChaincodeData类型,含有链码实例化策略)保存到指定通道的账本中(lscc名字空间);

□ 通道管理策略:采用递归结构在通道配置中定义不同层级与类型的权限策略,包括Readers(定义读取通道数据的读权限)、Writers(定义提交通道数据的写权限)与Admins(定义修改通道配置的管理权限)等权限,并在通道配置路径(/Channel、/Channel/Application、/Channel/Orderer、/Channel/*/Org等)上指定对应层级对象(系统、应用通道、系统通道、任意组织等)中读、写、管理等权限的策略名称(如/Channel/Writers)、类型(ImplicitMetaPolicy类型或SignaturePolicy类型)与规则(如ImplicitMetaPolicy_ANY策略),其中,使用configtxgen工具生成创世区块或通道配置交易时默认的通道配置策略如表1-1所示,所有策略都定义在common Hools lconfigtxgem/encoder/encoder.go中,*表示Orderer或Application, Org表示组织名称。Fabric 1.2以后支持用户在Configtx.yaml文件中自定义通道配置的默认访问策略类型及策略规则。

表1-1 通道配置的默认策略列表

1.2.2 Hyperledger Fabric架构

1. Hyperledger Fabric系统逻辑架构

Hyperledger Fabric自1.0.0版以后,其系统逻辑架构发生了很大的变化,如图1-5所示。从应用层视角来看,Hyperledger Fabric为开发人员提供了CLI命令行终端、事件模块、客户端SDK、链码API等接口,为上层应用提供了身份管理、账本管理、交易管理、智能合约管理等区块链服务,具体如下。

图1-5 Hyperledger Fabric逻辑架构示意图 [23]

□ 身份管理:获取用户注册证书及其私钥,用于身份验证、消息签名与验签等;

□ 账本管理:提供多种方式查询与保存账本数据,如查询指定区块号的区块数据;

□ 交易管理:构造并发送签名提案消息请求背书,检查合法后请求交易排序,并打包成区块,验证交易后提交账本;

□ 智能合约管理:基于链码API编写智能合约程序,安装链码并实例化(部署)后,通过调用链码请求执行更改状态的操作。

从底层视角看,Hyperledger Fabric提供了成员关系服务、共识服务、链码服务、安全与密码服务等服务,具体如下。

□ 成员关系服务:Fabric-CA节点提供成员登录注册服务,接收申请并授权新用户证书与私钥等,对身份证书生命周期进行管理。MSP组件基于身份证书实现对成员等资源实体进行认证等权限管理操作,同一个MSP组件对象内的成员拥有共同信任的根证书;

□ 共识服务:通过Endorser背书节点模拟执行提案消息,请求对模拟执行结果等签名进行背书,再提交到Orderer节点共识组件(Solo、Kafka等)对交易进行排序并打包出块,然后交由Committer记账节点验证交易并提交账本。同时,基于Gossip消息协议提供P2P网络通信机制,实现高效数据分发与状态同步,确保节点账本的一致性;

□ 链码服务:基于Docker容器提供隔离运行环境执行链码,支持多种语言开发的链码程序(智能合约),具有良好的可扩展性,同时,提供完善的镜像文件仓库管理机制,支持快速环境部署与测试;

□ 安全与密码服务:将安全与密码服务封装为BCCSP组件,提供生成密钥、消息签名与验签、加密与解密、获取哈希函数等服务功能,具有可插拔组件特性,能够扩展定制的密码安全服务算法(如国密等)。

2. Hyperledger Fabric系统运行时架构

Hyperledger Fabric包括Client客户端节点、CA节点、Endorser背书节点、Committer记账节点、Leader主节点、Orderer排序节点等,如图1-6所示。

图1-6 Hyperledger Fabric系统运行时架构示意图

(1)CA节点

CA节点部署Fabric-CA等可选组件,基于RESTful接口提供用户注册、证书颁发等用户管理与证书服务。用户可以通过客户端登记信息,注册合法用户并登录,申请获取合法的身份证书与私钥,交由MSP组件验证与管理用户实体身份。另外,用户也可以通过其他第三方合法的CA工具实现颁发证书等服务功能。

(2)Client客户端节点

Client客户端节点部署用户应用程序或CLI命令行终端,需要登记注册用户(或其他合法途径)获取合法的证书与私钥,并执行用户程序或命令,首先发送消息到Endorser背书节点请求背书,当收集到足够多的背书结果后,将背书信息、模拟执行结果等封装为普通交易消息,通过Broadcast()服务接口发送给Orderer节点请求排序,生成区块后广播到通道中的所有Peer节点上。对于配置交易消息,还需要创建新的应用通道或者更新通道配置。另外,Client节点还可以通过Deliver()服务接口从Orderer节点请求获取指定通道的区块数据。

(3)Peer节点

Peer节点包括Endorser背书节点、Committer记账节点等,可以运行在同一个物理服务器节点上。

Endorser背书节点负责启动链码容器用于模拟执行签名提案,并对模拟执行结果读写集、交易提案等进行签名背书,表示认可交易提案模拟执行结果。通常,默认静态指定Endorser背书节点为当前操作的Peer节点(如命令行模式),或者设置参数列表动态指定Endorser背书节点。

同一个通道上的所有Peer节点默认都是该通道组织上的Committer记账节点,维护本节点上该通道的账本数据,负责对交易进行验证,调用VSCC系统链码检查背书信息是否满足实例化时指定的背书策略,再执行MVCC检查以标记交易的有效性,并提交本地账本。

另外,Leader主节点代表组织(通常对应于一个MSP组件)通过Deliver()服务接口与Orderer节点建立gRPC通信连接,请求指定通道的账本区块数据。如果接收完毕账本当前已有的区块数据,则阻塞等待直到提交新区块。同时,Leader主节点通过Gossip消息协议将接收的区块数据分发到组织内的其他节点,以达到广播交易的目的。同时,通道组织内的Peer节点基于反熵算法同步缺失的数据(区块数据与隐私数据),及时更新组织内的所有节点账本,以确保数据的一致性。

(4)Orderer排序节点

目前,Hyperledger Fabric中的Orderer排序节点提供了基于单个节点的排序服务(Solo类型,仅用于测试)或基于多个节点(集群)的排序服务(Kafka类型)。其中,Kafka共识组件支持CFT(Crash Fault Tolerence,崩溃故障容错)错误。

Orderer节点通过Broadcast()服务接口接收交易消息请求,包括普通交易消息、配置交易消息等,其将消息提交给共识组件进行排序,再添加到本地缓存交易消息列表,按照出块规则(出块时间配置、区块字节数限制、配置交易消息单独出块等)切割打包成新区块,并提交Orderer节点本地账本。同时,通过Deliver()服务接口处理区块请求消息,从本地账本获取请求的区块数据然后发送给组织Leader主节点,并广播到通道组织内的其他节点上。

1.2.3 安装基础环境与部署Fabric系统

本节以Hyperledger Fabric自带示例e2e_cli为例介绍系统部署的流程,建立实验环境进行研究。

1.安装基础环境

读者可以使用虚拟机或物理服务器安装基础环境以及Fabric系统(1.1.0版)用于测试,推荐安装CentOS、Ubuntu等主流Linux操作系统。本节的实验环境是以CentOS 7.4(发行版本7.4.1708)的x86_64版本操作系统为例,默认内核版本为3.10,选择单机多节点测试环境,即所有功能节点容器运行在同一个单机节点上进行交互,e2e_cli示例默认采用了Kafka共识组件。

为避免出错,生产环境最好是先将内核版本升级到3.10以上的最新版本(按照满足Docker运行环境的配置要求),以修复旧版本可能存在的程序缺陷,同时保证功能稳定与符合系统要求。如果在实例化链码步骤启动Docker容器时发生oci runtime error错误,则必须将系统升级到3.10以上的最新版本。

在虚拟机或物理服务器上安装了CentOS 7.4的x86_64版本的操作系统之后,单机测试环境可以选择关闭SELinux等安全配置,即编辑/etc/selinux/config文件中的SELINUX=disabled,保存后退出然后重启操作系统。

(1)安装与卸载Docker CE

使用root账号登录CentOS 7.4操作系统,在命令行终端中执行如下命令在线安装Docker CE。如果以普通用户账号登录,则在执行特殊命令(如yum)或系统权限命令时需要添加sudo。

        #必装软件包
        [root@localhost ~]# yum install -y yum-utils device-mapper-persistent-data lvm2
        [root@localhost  ~]#  yum-config-manager  --add-repo  https://download.docker.com/
        linux/centos/docker-ce.repo                         # 配置稳定仓库并保存仓库配置
        [root@localhost ~]# yum makecache fast            # 更新yum安装的相关Docker软件包
        [root@localhost ~]# yum install docker-ce         # 安装Docker CE
        接着,选择配置Docker服务跟随操作系统开机启动的方式,正常启动Docker运行。
        [root@localhost ~]# systemctl enable docker.service      # 设置跟随系统开机启动
        [root@localhost ~]# systemctl start docker                # 正常启动Docker

然后,查询Docker版本号并检查是否已成功安装了Docker。

        [root@localhost ~]# docker version
        Client:
          Version:        18.06.0-ce-rc3
          API version:   1.38
          Go version:    go1.10.3
          Git commit:    cbfa3d9
          Built:          Thu Jul 12 05:26:082018
          OS/Arch:        linux/amd64
          Experimental: false

        Server:
          Engine:
          Version:        18.06.0-ce-rc3
          API version:   1.38 (minimum version 1.12)
          Go version:    go1.10.3
          Git commit:    cbfa3d9
          Built:          Thu Jul 12 05:28:342018
          OS/Arch:        linux/amd64
          Experimental: false

如果要卸载已经安装的Docker,则执行如下命令查找已经安装的Docker软件包。

        [root@localhost ~]# yum list installed | grep docker
        docker-ce.x86_64                  18.06.0.ce-2.3.rc3.el7             @docker-ce-test

接着,删除yum中的Docker软件包。

        [root@localhost ~]# yum remove docker-ce.x86_64

最后,删除Docker相关的镜像、容器、自定义配置等文件。

        [root@localhost ~]# rm -rf /var/lib/docker

(2)安装Docker-Compose

Docker Compose项目实现了快速编排Docker容器集群的功能,可以定义和运行多个Docker容器的应用,其前身是开源项目Fig。Compose使用Dockerfile模板文件定义一个单独的应用容器,通过docker-compose.yml模板文件(YAML格式)定义一组相互关联的应用容器为一个项目(project)提供业务,并通过子命令对项目容器进行生命周期管理。因此,Compose极大地简化了Docker容器集群的编排工作,其Github官方地址为https://github.com/docker/compose

首先,安装curl软件依赖包。

        [root@localhost ~]# yum install curl

接着,查看https://github.com/docker/compose/releases选择较新的Docker Compose版本,执行如下命令下载Docker Compose到目录/usr/local/bin/docker-compose,并设置执行权限。

        [root@localhost  ~]#  curl  -L  https://github.com/docker/compose/releases/
            download/1.21.2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-
            compose
        [root@localhost ~]# chmod +x /usr/local/bin/docker-compose

如果出现No such file or directory提示问题,则执行如下操作复制对应的目录。

        [root@localhost ~]# cp /usr/local/bin/docker-compose /usr/bin

最后,打印docker-compose版本信息,检查是否成功安装。

        [root@localhost ~]# docker-compose version
        docker-compose version 1.21.2, build a133471
        docker-py version: 3.3.0
        CPython version: 3.6.5
        OpenSSL version: OpenSSL 1.0.1t   3 May 2016

(3)安装与配置Go语言环境

执行如下命令下载Go语言包,以1.10.3版本为例。在线下载速度可能会比较慢,用户同样能通过https://golang.org/doc/install?download=go1.10.3.linux-amd64.tar.gz下载指定版本,并解压到/usr/local目录。

        [root@localhost  ~]#  curl  -O  https://storage.googleapis.com/golang/go1.10.3.
            linux-amd64.tar.gz
        [root@localhost ~]# tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz

接着,使用vim修改/etc/profile文件,并在文件最后添加Go语言环境变量。

        export PATH=$PATH:/usr/local/go/bin
        export GOROOT=/usr/local/gc
        export GOPATH=/opt/gopath

然后,保存文件后执行命令更新环境变量。

        [root@localhost ~]# source /etc/profile

最后,执行如下命令查看Go版本信息。

        [root@localhost ~]# go version
        go version go1.10.3 linux/amd64

至此,Linux操作系统、Docker、Docker Compose、Go语言环境等基础环境安装准备完毕。

2.下载与安装Fabric系统

首先安装Git工具,使用Git命令下载Fabric代码到指定目录中,然后创建并进入到指定源码目录,通过Git的checkout命令切换到指定v1.1.0版本的源码。

        [root@localhost ~]# yum -y install git
        [root@localhost ~]# mkdir -p ~/go/src/github.com/hyperledger
        [root@localhost ~]# cd ~/go/src/github.com/hyperledger/
        [root@localhost hyperledger]# git clone https://github.com/hyperledger/fabric.git
        [root@localhost hyperledger]# mkdir -p /opt/gopath/src/github.com/hyperledger/fabric/
        [root@localhost hyperledger]# cd /opt/gopath/src/github.com/hyperledger/fabric/
        [root@localhost fabric]# git checkout -b v1.1.0

如果下载速度太慢,则推荐通过https://github.com/hyperledger/fabric/archive/v1.1.0.tar.gz链接下载源码压缩文件,解压后放入上述目录。注意,下载源码指定目录必须与源码import指定文件路径保持一致,当前源码存放目录是/opt/gopath/src/github.com/hyperledger/fabric/。

        [root@localhost  ~]#  tar  -C  /opt/gopath/src/github.com/hyperledger/fabric/  -xzf
            fabric-1.1.0.tar.gz

3.下载Fabric相关的Docker镜像

为了加速下载Fabric相关的Docker镜像,可以创建指定配置文件并添加阿里云加速器地址,保存退出后重新加载Docker服务。该操作需要注册阿里云账号(https://dev.aliyun.com/),进入阿里云镜像加速器页面https://cr.console.aliyun.com/#/accelerator,点击左侧“镜像加速器”目录,查看并复制用户专属的镜像加速器地址,如https://XXX.mirror.aliyuncs.com

        [root@localhost ~]# mkdir -p /etc/docker
        [root@localhost ~]# vim /etc/docker/daemon.json
        {
            "registry-mirrors": ["https://XXX.mirror.aliyuncs.com"]
        }
        [root@localhost ~]# systemctl daemon-reload
        [root@localhost ~]# systemctl restart docker

接着,切换当前目录至fabric/examples/e2e_cli,运行download-dockerimages.sh脚本下载e2e_cli示例必要的Fabric镜像文件,同时,通过参数指定fabric-ca与Fabric版本为x86_64-1.1.0。

        [root@localhost  ~]#  cd  /opt/gopath/src/github.com/hyperledger/fabric/examples  /
            e2e_cli/
        [root@localhost  e2e_cli]#  source  download-dockerimages.sh  -c  x86_64-1.1.0  -f
            x86_64-1.1.0

e2e_cli示例默认使用Kafka共识组件,因此还需要下载fabric-kafka与fabric-zookeeper镜像。

        [root@localhost e2e_cli] docker pull hyperledger/fabric-kafka:latest
        [root@localhost e2e_cli] docker pull hyperledger/fabric-zookeeper:latest

然后,执行docker命令查看已经下载的所有镜像(未启用CouchDB数据库)。

        [root@localhost e2e_cli] docker images
        REPOSITORY                        TAG              IMAGE ID            CREATED         SIZE
        hyperledger/fabric-zookeeper      latest           2b51158f3898        2 weeks ago     1.44GB
        hyperledger/fabric-kafka          latest           936aef6db0e6        2 weeks ago     1.45GB
        hyperledger/fabric-ca             latest           72617b4fa9b4        4 months ago    299MB
        hyperledger/fabric-ca             x86_64-1.1.0     72617b4fa9b4        4 months ago    299MB
        hyperledger/fabric-tools          latest           b7bfddf508bc        4 months ago    1.46GB
        hyperledger/fabric-tools          x86_64-1.1.0     b7bfddf508bc        4 months ago    1.46GB
        hyperledger/fabric-orderer        latest           ce0c810df36a        4 months ago    180MB
        hyperledger/fabric-orderer        x86_64-1.1.0     ce0c810df36a        4 months ago    180MB
        hyperledger/fabric-peer           latest           b023f9be0771        4 months ago    187MB
        hyperledger/fabric-peer           x86_64-1.1.0     b023f9be0771        4 months ago    187MB
        hyperledger/fabric-javaenv        latest           82098abb1a17        4 months ago    1.52GB
        hyperledger/fabric-javaenv        x86_64-1.1.0     82098abb1a17        4 months ago    1.52GB
        hyperledger/fabric-ccenv          latest           c8b4909d8d46        4 months ago    1.39GB
        hyperledger/fabric-ccenv          x86_64-1.1.0     c8b4909d8d46        4 months ago    1.39GB
        hyperledger/fabric-baseos         x86_64-0.4.6     220e5cf3fb7f        4 months ago    151MB

最后,将base/peer-base.yaml配置文件中的默认Docker网络名称e2ecli_default修改为e2e_cli_default,维护默认网络名称与当前目录e2e_cli名称保持一致,启动过程中Docker会基于该目录名称创建网络。

        [root@localhost e2e_cli] vim ./base/peer-base.yaml
        - CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=e2ecli_default   # 原来的配置项
        - CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=e2e_cli_default  # 修改后的配置项

如果不修改该网络名称,则可能会报如下错误。

        Error: Error endorsing chaincode: rpc error: code = Unknown desc = Error starting
            container: API error (404): {"message":"network e2ecli_default not found"}

1.2.4 Fabric初始化启动流程

1. e2e_cli示例概述

e2e_cli示例演示了基于链码查询2个账户余额与转账操作,具体如下。

□ 1个Orderer节点:默认采用Kafka共识组件,即4个Kafka服务节点和3个Zookeeper节点;

□ 4个Peer节点:e2e_cli示例程序将其顺序编号为Peer0-Peer3,并加上组织名称来标识节点;

□ 1个CLI命令行客户端节点(同时作为配置节点):客户端节点使用cryptogen和configtxgen命令行工具生成相关的配置文件、身份证书文件、签名私钥文件、TLS证书、创世区块文件等。

如图1-7所示,e2e_cli示例网络拓扑结构中的4个Peer节点分别属于两个组织,即Org1组织(Peer0/Org1节点与Peer1/Org1节点)和Org2组织(Peer2/Org2节点与Peer3/Org2节点),并加入同一个应用通道(默认名称为mychannel)。各组织的第1个节点即Peer0/Org1节点(主机名peer0.org1.example.com)和Peer2/Org2节点(主机名peer0.org2. example.com)作为锚节点与其他组织通信,默认采用动态选举机制(即core.yaml配置文件默认配置peer.gossip.useLeaderElection为true, peer.gossip.orgLeader为false)选举产生组织上的Leader主节点,并通过Deliver()服务接口负责从Orderer服务节点请求获取本通道账本上的所有区块数据。

图1-7 Hyperledger Fabric初始化启动流程示意图

e2e_cli示例调用执行network_setup.sh(fabric/examples/e2e_cli/network_setup.sh)脚本,启动或停止Hyperledger Fabric系统,指定应用通道名称$channel-name、超时时间$cli_timeout以及CouchDB启用选项couchdb,其标准命令格式如下:

        network_setup <up|down> <\$channel-name> <\$cli_timeout> <couchdb>

实际上,network_setup up命令调用networkUp()函数启动Fabric网络。networkUp()函数首先运行generateArtifacts.sh脚本,在当前e2e_cli/crypto-config目录中生成相关的配置文件,并判断CouchDB启用参数以决定是否启动CouchDB容器(默认不开启该配置项)。接着,调用docker-compose执行$COMPOSE_FILE(即docker-compose-cli.yaml)文件。该文件配置了所有功能节点的容器与镜像名称、依赖配置文件、环境变量、工作目录、服务端口等,并将本地目录作为数据卷挂载到对应节点的镜像目录中。最后,在后台启动所有节点的Docker容器以提供正常的服务。如果没有单独下载Fabric相关镜像,则会自动从官方网站上下载镜像,下载速度可能会较慢,请自行设置镜像加速器。

实际上,Orderer排序节点启动后提供共识排序服务并创建系统通道,Peer节点通过默认命令peer node start启动,CLI客户端节点作为用户操作终端默认执行script.sh脚本(examples/e2e_cli/scripts/script.sh),并且上述节点都运行在Docker容器中。如果想手工尝试测试步骤,则可以先删除docker-compose-cli.yaml中的cli容器启动命令command,再运行network_setup up命令启动网络,并手工执行后续步骤以查看实验结果。

script.sh脚本首先尝试获取系统通道(名称为“testchainid”)的创世区块,以测试Orderer节点是否正常可用,再设置当前CLI客户端容器的4个环境变量为Peer0/Org1节点的相关配置,以切换连接到Peer0/Org1节点上。此时,CLI客户端可以发送peer channel create命令到Peer0/Org1节点,请求Orderer节点建立新的应用通道。

接着,script.sh脚本依次切换到所有4个Peer节点上,通过执行peer channel join命令将切换的节点加入到新通道,并执行peer channel update命令更新Peer0/Org1节点与Peer2/Org2节点上的锚节点配置。

然后,script.sh脚本在Peer0/Org1节点上执行peer chaincode install命令,安装链码chaincode_example02.go。同时,在Peer2/Org2节点上执行peer chaincode instantiate命令实例化链码,以启动链码容器提供服务。该链码初始化账户A和账户B的余额分别为100元与200元,指定背书策略是两个组织中的任意成员签名。

最后,script.sh脚本执行peer chaincode invoke命令调用链码,从账户A转账10元到账户B,然后在Peer3/Org2节点上同样安装chaincode_example02.go链码,并执行peer chaincode query命令检查账户A的余额是否为90元。

如图1-7所示,目前,Hyperledger Fabric网络从初始化启动到正常提供链码服务的流程包括以下步骤。

①生成系统初始化启动的相关配置文件。e2e_cli示例脚本generateArtifacts.sh通过cryptogen工具生成组织成员关系和身份证书、密钥等文件。接着,复制生成配置文件docker-compose-e2e.yaml,并替换其中正确的密钥文件位置。然后,通过configtxgen工具生成节点与通道配置文件,包括Orderer节点上系统通道的创世区块文件genesis.block、新建应用通道(mychannel)的配置交易文件channel.tx、组织锚节点配置更新交易文件Org1MSPanchors.tx与Org2MSPanchors.tx等;

②启动Orderer排序节点。Orderer排序节点根据orderer.yaml配置文件、成员组织结构与身份证书(存放在指定配置路径的msp、tls等目录下)等文件启动节点提供服务,并基于系统通道创世区块文件genesis.block创建系统通道(默认通道名称为testchainid);

③启动Peer节点(Endorser节点、CLI客户端节点等)。Peer节点根据core.yaml配置文件、成员组织结构与身份证书(存放在指定配置路径的msp、tls等目录下)等文件启动节点;

④创建与加入通道、更新锚节点配置。Peer节点首先读取新建应用通道的通道配置交易文件channel.tx,向Orderer节点发送通道配置交易消息,以请求创建新的应用通道(名称为“mychannel”),并依次将两个组织上的所有Peer节点加入新通道,同时,使用Org1MSPanchors.tx与Org2MSPanchors.tx分别更新两个组织的锚节点配置;

⑤安装、实例化与调用链码。Peer节点安装用户链码并进行实例化,在Peer节点上启动Docker容器以提供调用链码服务。此时,Peer节点可以正常发起交易请求处理。

本节将继续介绍Hyperledger Fabric整个初始化启动的详细流程,并作为后续章节分析核心模块的逻辑主线。

2.生成系统初始化启动的相关配置文件

(1)组织成员关系与身份证书等相关文件

用户执行network_setup.sh命令启动Fabric网络,该脚本命令会调用networkUp()函数,如代码清单1-1所示。该函数首先检查指定目录下是否存在./crypto-config目录,用于保存身份证书等文件。如果不存在该目录,则执行source generateArtifacts.sh $CH_NAME命令生成相关目录。其中,应用通道名称$CH_NAME默认为mychannel。

          [root@localhost e2e_cli] bash network_setup.sh up

代码清单1-1 network_setup.sh脚本networkUp()函数的源码示例

        examples/e2e_cli/network_setup.sh文
        function networkUp () {
            if [ -d "./crypto-config" ]; then
                echo "crypto-config directory already exists."
            else
                source generateArtifacts.sh $CH_NAME
            fi
            if [ "${IF_COUCHDB}" == "couchdb" ]; then
                CHANNEL_NAME=$CH_NAME  TIMEOUT=$CLI_TIMEOUT  docker-compose  -f  $COMPOSE_
                    FILE -f $COMPOSE_FILE_COUCH up -d 2>&1
            else
                CHANNEL_NAME=$CH_NAME  TIMEOUT=$CLI_TIMEOUT  docker-compose  -f  $COMPOSE_
                    FILE up -d 2>&1
            fi
            if [ $? -ne 0 ]; then
            echo "ERROR ! ! ! ! Unable to pull the images "
            exit 1
                fi
            docker logs -f cli
        }

generateArtifacts.sh脚本按顺序调用了3个函数,即generateCerts()、replacePrivateKey()与generateChannelArtifacts()。

如代码清单1-2所示,generateCerts()函数将cryptogen工具变量CRYPTOGEN配置为指定目录下的二进制程序($FABRIC_ROOT/release/$OS_ARCH/bin/cryptogen)。其中,$FABRIC_ROOT是e2e_cli示例所在位置的上级目录($PWD/../..),即Fabric源码目录/opt/gopath/src/github.com/hyperledger/fabric/, $OS_ARCH保存当前操作系统的版本与类型,如linux-amd64。因此,generateCerts()函数检查的目录实际上是/opt/gopath/src/github.com/hyperledger/fabric/release/linux-amd64/bin。

代码清单1-2 generateArtifacts.sh脚本中generateCerts()函数的源码示例

        examples/e2e_cli/generateArtifacts.sh文
        ……
        export FABRIC ROOT=$PWD/../..
        ……
        OS_ARCH=$(echo  "$(uname  -s|tr  '[:upper:]'  '[:lower:]'|sed  's/mingw64_nt.*/
        windows/')-$(uname -m | sed 's/x86_64/amd64/g')" | awk '{print tolower($0)}')
        ……
        ## Generates Org certs using cryptogen tool
        function generateCerts (){
            CRYPTOGEN=$FABRIC_ROOT/release/$OS_ARCH/bin/cryptogen
            if [ -f "$CRYPTOGEN" ]; then
                    echo "Using cryptogen -> $CRYPTOGEN"
            else
                echo "Building cryptogen"
                make -C $FABRIC_ROOT release
            fi

            echo
            echo "##########################################################"
            echo "##### Generate certificates using cryptogen tool #########"
            echo "##########################################################"
            $CRYPTOGEN generate --config=./crypto-config.yaml
            echo
        }
        ……

该目录在正常初始状态下不存在任何二进制文件。generateCerts()函数执行make工具命令,编译cryptogen工具到该目录,或者用户可以从指定地址https://nexus.hyperledger.org/content/repositories/releases/org/hyperledger/fabric/hyperledger-fabric/linux-amd64-1.1.0/hyperledger-fabric-linux-amd64-1.1.0.tar.gz下载对应版本的二进制文件压缩包,并解压其中的bin文件夹到上述指定目录,设置合适的文件权限。

cryptogen工具编译成功后,generateCerts()函数执行如下命令,基于当前目录中的配置文件(crypto-config.yaml)生成网络成员组织结构和对应的身份证书、签名私钥等文件,以管理组织成员在网络中的身份信息,并保存到默认的crypto-config目录下,其身份证书等文件生成在对应目录的msp/*中,同时生成TLS证书与密钥等文件,并保存在对应目录的tls/*中。

          $CRYPTOGEN gennerate --config=./crypto-config.yaml

如代码清单1-3所示,e2e_cli示例包含的crypto-config.yaml定义了Orderer排序节点中存在1个组织Orderer(OrdererOrgs类型), Peer节点中存在两个组织Org1和Org2(PeerOrgs类型),且各自包含两个节点(其中1个是普通用户)。

代码清单1-3 crypto-config.yaml的源码示例

          examples/e2e_cli/crypto-config.yaml文件
          # “OrdererOrgs” - 定义Orderer排序节点的组织
          OrdererOrgs:
              - Name: Orderer                 # 组织名称
              Domain: example.com            # 域名称
              CA:   #CA证书相关身份信息
                  Country: US
                  Province: California
                  Locality: San Francisco
              Specs:                           # “Specs” - 组织中的节点
                  - Hostname: orderer       # 主机名称
          # “PeerOrgs” -定义peer节点的组织
          PeerOrgs:
              # 定义Org1组织
              - Name: Org1                    # 组织名称
          Domain: org1.example.com           # 域名称
          EnableNodeOUs: true
          CA:  # 相关身份信息
                    Country: US
                    Province: California
                    Locality: San Francisco
            Template:
                    Count: 2                    # 组织节点总数
            Users:
                    Count: 1                    # 用户节点数量,不包括Admin管理员
            # 定义Org2组织
                - Name: Org2                    # 组织名称
                Domain: org2.example.com      # 域名称
                EnableNodeOUs: true
                CA:                               # 相关身份信息
                    Country: US
                    Province: California
                    Locality: San Francisco
                Template:
                    Count: 2
                Users:
                    Count: 1

crypto-config中的每个目录都代表一种类型组织,其组织的身份证书、签名私钥等文件都存放在各自目录下的msp和tls子目录中,具体如下。

□ ordererOrganizations目录:包含了Orderer类型组织(OrdererOrgs)的身份证书.pem文件、签名私钥*_sk文件、TLS证书(证书.crt文件与密钥.key文件)等,目前e2e_cli示例中存在1个Orderer组织,且包含1个Orderer节点;

□ peerOrganizations目录:包含了Peer类型组织(PeerOrgs)的身份证书.pem文件、签名私钥*_sk文件、TLS证书(证书.crt文件与密钥.key文件)等,目前e2e_cli示例中存在两个组织(Org1和Org2),且包含4个Peer节点。

为了保证Hyperledger Fabric启动时所有节点都可以正常访问到需要的身份证书、签名私钥等文件,必须将这些文件复制或映射部署到对应节点的配置文件路径下。e2e_cli示例通过docker-compose工具,基于docker-compose-cli.yaml、docker-compose-base.yaml等配置文件,将本地crypto-config中相应的目录作为数据卷挂载到容器指定的目录下,如表1-2所示。

表1-2 cryptogen工具基于crypto-config.yaml生成组织关系与身份证书等文件位置列表

读者可以安装tree命令,查看该目录下的组织成员结构及其证书、私钥等文件情况,详细文件列表见附录B。

        [root@localhost e2e_cli]# yum -y install tree
        [root@localhost e2e_cli]# tree -L 5 ./crypto-config
        ./crypto-config
        ├—— ordererOrganizations
        |   └—— example.com
        |        ├—— ca
        |        |    ├—— 07606df0424af9d53de8c3fb5b236cb70bfb04b34cae46bbf0bfbed24b3e51ea_sk
        |        |    └—— ca.example.com-cert.pem
        |        ├—— msp
        |        |    ├—— admincerts
        |        |    |   └—— Admin@example.com-cert.pem
        |        |    ├—— cacerts
        |        |    |   └—— ca.example.com-cert.pem
        |        |    └—— tlscacerts
        |        |        └—— tlsca.example.com-cert.pem
        |        ├—— orderers
        |        |    └—— orderer.example.com
        |        |        ├—— msp
        |        |        └—— tls
        |        ├—— tlsca
        |        |    ├—— 7778350b2e621d374db26e2313057388c41b63773ebce80b64bbdb06807b8a1c_sk
        |        |    └—— tlsca.example.com-cert.pem
        |        └—— users
        |             └—— Admin@example.com
        |                 ├—— msp
        |                 └—— tls
        └—— peerOrganizations
            ├—— org1.example.com
            |    ├—— ca
            |    |    ├—— ca.org1.example.com-cert.pem
            |    |    └—— eb7d1cf0b6114150d7994400af9dfdcf5412b7b11a216ab4ecdcc8a4591346b4_sk
            |    ├—— msp
            |    |    ├—— admincerts
            |    |    |   └—— Admin@org1.example.com-cert.pem
            |    |    ├—— cacerts
            |    |    |   └—— ca.org1.example.com-cert.pem
            |    |    ├—— config.yaml
            |    |    └—— tlscacerts
            |    |        └—— tlsca.org1.example.com-cert.pem
            |    ├—— peers
            |    |    ├—— peer0.org1.example.com
            |    |    |   ├—— msp
            |    |    |   └—— tls
            |    |    └—— peer1.org1.example.com
            |    |        ├—— msp
            |    |        └—— tls
            |    ├—— tlsca
            |    |    ├—— 9bf57185b40e461f8ed81bded116a409a0c457853e23a744571236b7584eff4f_sk
            |    |    └—— tlsca.org1.example.com-cert.pem
            |    └—— users
            |         ├—— Admin@org1.example.com
            |         |   ├—— msp
            |         |   └—— tls
            |         └—— User1@org1.example.com
            |             ├—— msp
            |             └—— tls
            └—— org2.example.com

generateArtifacts.sh脚本接着调用replacePrivateKey()函数,读取并替换正确的私钥文件名称,如代码清单1-4所示。该函数首先在当前目录下基于docker-compose-e2e-template. yaml模板文件创建新的配置文件docker-compose-e2e.yaml。接着,进入Org1组织的CA目录(crypto-config/peerOrganizations/org1.example.com/ca/),获取当前目录下以_sk结尾的私钥文件的文件名作为PRIV_KEY。然后,将docker-compose-e2e.yaml文件中的CA1_PRIVATE_KEY字符串替换为${PRIV_KEY},即以_sk结尾的私钥文件名称。同样,进入Org2组织的CA目录(crypto-config/peerOrganizations/org2.example.com/ca/),将docker-compose-e2e.yaml文件中的CA2_PRIVATE_KEY字符串替换为${PRIV_KEY},即该目录下以_sk结尾的私钥文件名称。

代码清单1-4 generateArtifacts.sh脚本中replacePrivateKey()函数的源码示例

          examples/e2e_cli/generateArtifacts.sh文件
        ……
        ## Using docker-compose template replace private key file names with constants
        function replacePrivateKey () {
            ARCH=`uname -s | grep Darwin`
            if [ "$ARCH" == "Darwin" ]; then
                OPTS="-it"
            else
                OPTS="-i"
            fi
            cp docker-compose-e2e-template.yaml docker-compose-e2e.yaml
                CURRENT_DIR=$PWD
                cd crypto-config/peerOrganizations/org1.example.com/ca/  # 进入org1的CA目录
                PRIV_KEY=$(ls *_sk)
                cd $CURRENT_DIR
                # 将docker-compose-e2e.yaml文件中每一行CA1_PRIVATE_KEY替换为${PRIV_KEY}
                sed $OPTS "s/CA1_PRIVATE_KEY/${PRIV_KEY}/g" docker-compose-e2e.yaml
                cd crypto-config/peerOrganizations/org2.example.com/ca/   #  进入org2的CA目
                    录,执行相同的操作
                PRIV_KEY=$(ls *_sk)
                cd $CURRENT_DIR
                sed $OPTS "s/CA2_PRIVATE_KEY/${PRIV_KEY}/g" docker-compose-e2e.yaml
        }

(2)节点与通道配置文件

generateArtifacts.sh脚本最后执行generateChannelArtifacts()函数,如代码清单1-5与代码清单1-6所示。该函数首先的使用configtxgen工具基于configtx.yaml创建节点与通道配置文件,包括Orderer系统通道的创世区块文件genesis.block、新建应用通道的配置交易文件channel.tx、锚节点配置更新交易文件Org1MSPanchors.tx与Org2MSPanchors.tx等。

代码清单1-5 generateArtifacts.sh脚本中generateChannelArtifacts()函数的源码示例

          examples/e2e_cli/generateArtifacts.sh文件
        ……
        ## Generate orderer genesis block , channel configuration transaction and anchor
            peer update transactions
        function generateChannelArtifacts() {
            CONFIGTXGEN=$FABRIC_ROOT/release/$OS_ARCH/bin/configtxgen
            if [ -f "$CONFIGTXGEN" ]; then
                    echo "Using configtxgen -> $CONFIGTXGEN"
            else
                echo "Building configtxgen"
                make -C $FABRIC_ROOT release
            fi
          ……
          # Orderer系统通道的创世区块文件
            $CONFIGTXGEN -profile TwoOrgsOrdererGenesis -outputBlock ./channel-artifacts/
                genesis.block
          ……
          # 应用通道的配置交易文件
            $CONFIGTXGEN  -profile  TwoOrgsChannel  -outputCreateChannelTx    ./channel-
                artifacts/channel.tx -channelID $CHANNEL_NAME
          ……
          # 锚节点配置更新交易文件
            $CONFIGTXGEN  -profile  TwoOrgsChannel  -outputAnchorPeersUpdate  ./channel-
                artifacts/Org1MSPanchors.tx -channelID $CHANNEL_NAME -asOrg Org1MSP
          ……
            $CONFIGTXGEN  -profile  TwoOrgsChannel  -outputAnchorPeersUpdate  ./channel-
                artifacts/Org2MSPanchors.tx -channelID $CHANNEL_NAME -asOrg Org2MSP
            echo
        }

代码清单1-6 configtx.yaml的源码示例

          examples/e2e_cli/configtx.yaml文件
        Profiles:                                     # 模板定义了Orderer系统通道和应用通道配置信息
        TwoOrgsOrdererGenesis:                        # Orderer系统通道配置,包含1个OrdererOrg组织
                                                    和1个SampleConsortium联盟
            Capabilities:                             # 定义全局Capabilities功能特性
                <<: *ChannelCapabilities
                Orderer:                              # 系统通道配置信息
                    <<: *OrdererDefaults             # 具体配置信息,引用下面OrdererDefaults定位
                                                      的字段信息
                    Organizations:                    # Orderer系统通道组织
                        - *OrdererOrg                # 引用下面定义的OrdererOrg组织
            Capabilities:                             # 定义Orderer系统通道Capabilities功能特性
                    <<: *OrdererCapabilities
                Consortiums:                          # 联盟列表
                    SampleConsortium:                 # 联盟包含Org1和Org2两个组织
                        Organizations:
                            - *Org1
                            - *Org2
            TwoOrgsChannel:                                    # 应用通道配置,包含两个组织Org1和Org2
                Consortium: SampleConsortium                   # 应用通道关联的联盟名称
                Application:                                   # 应用通道信息
                    <<: *ApplicationDefaults                  # 具体配置信息,引用下面ApplicationDefaults
                                                            定位的字段信息
                    Organizations:                             # 定义应用通道组织
                        - *Org1
                        - *Org2
                    Capabilities:                              # 定义应用通道Capabilities功能特性
                        <<: *ApplicationCapabilities
        # 定义具体的组织描述信息,被Profiles模板引用
        Organizations:
        - &OrdererOrg                                          # OrdererOrg组织定义
                Name: OrdererOrg                               # 组织名称
                ID: OrdererMSP                                 # 组织MSP标识ID
                MSPDir: crypto-config/ordererOrganizations/example.com/msp
                                                               # MSP配置文件路径
        - &Org1                                                # Org1组织定义
                Name: Org1MSP                                  # 组织名称
                ID: Org1MSP                                    # 组织MSP ID
                MSPDir: crypto-config/peerOrganizations/org1.example.com/msp
                                                               # MSP配置文件路径
                AnchorPeers:                                   # 定义锚节点信息
                    - Host: peer0.org1.example.com             # 主机名
                    Port: 7051                                 # 端口
        - &Org2                                                # Org2组织定义
                Name: Org2MSP                                  # 组织名称
                ID: Org2MSP                                    # 组织MSP ID
                MSPDir: crypto-config/peerOrganizations/org2.example.com/msp
                                                               # MSP配置文件路径
                AnchorPeers:                                   # 定义锚节点信息
                    - Host: peer0.org2.example.com             # 主机名
                    Port: 7051                                 # 端口
        # 定义具体的Orderer描述信息,被前面的Profiles模板引用
        Orderer: &OrdererDefaults
            OrdererType: kafka                                 # 共识组件类型
            Addresses:
                - orderer.example.com:7050                     # Orderer服务节点地址,可扩展成多个
                                                              Orderer节点
            # 打包交易出块的配置规则,根据实际系统需求测试后定制参数
            BatchTimeout: 2s                                   # 打包交易消息出块的超时时间
            BatchSize:
                MaxMessageCount: 10                            # 打包交易消息出块的最大消息个数
                AbsoluteMaxBytes: 98 MB                        # 打包交易消息出块的最大字节数,可以
                                                            适当调大以防止同步区块数量过多
                PreferredMaxBytes: 512 KB                      # 通常情况下打包交易消息出块的建议字节数
            Kafka:
                Brokers:                                       # 排序服务Kafka Broker服务器地址列表
                    - kafka0:9092
                    - kafka1:9092
                    - kafka2:9092
                    - kafka3:9092
            Organizations:                                   # Orderer组织
        # 定义具体的应用通道相关描述信息,被前面的Profiles模板引用
        Application: &ApplicationDefaults
            Organizations:                                   # 应用通道组织
        Capabilities:
            Global: &ChannelCapabilities                     # 全局通道Capabilities
                V1_1: true
            Orderer: &OrdererCapabilities                    # Orderer系统通道Capabilities
                V1_1: true
            Application: &ApplicationCapabilities            # Application应用通道Capabilities
                V1_1: true

(3)Orderer系统通道的创世区块文件

generateChannelArtifacts()函数执行如下configtxgen命令,创建Orderer系统通道的创世区块文件genesis.block,用于Orderer节点启动时生成系统通道的初始配置信息,具体如下。

□ Orderer节点系统通道的配置信息:定义了Orderer共识组件类型(Solo类型、Kafka类型等)、排序服务地址列表、打包消息出块的超时时间、Kafka配置信息、最大应用通道数目以及参与系统通道的组织等;

□ Consortiums联盟列表:定义了Orderer服务节点所服务的联盟列表,联盟内所有组织拥有相同的应用通道创建策略,且创建新的应用通道必须包含合法的所属联盟名称。

          $CONFIGTXGEN  -profile  TwoOrgsOrdererGenesis  -outputBlock  ./channel-artifacts/
              genesis.block

(4)新建应用通道的配置交易文件

generateChannelArtifacts()函数执行如下configtxgen命令,构造新建应用通道的配置交易文件channel.tx(CHANNEL_NAME默认为mychannel),再执行通道创建命令peer channel create读取该文件,并将通道配置交易消息发送到Orderer节点,请求创建应用通道并构造该通道创世区块mychannel.block。接着,通过Deliver()服务接口获取该创世区块文件,并写入本地文件系统,将其作为后续其他节点加入应用通道的命令参数。

          $CONFIGTXGEN  -profile  TwoOrgsChannel  -outputCreateChannelTx  ./channel-artifacts/
              channel.tx -channelID $CHANNEL_NAME

注意,应用通道名称(或链名称)会作为Kafka消息主题(topic)名称以及CouchDB数据库名称的前缀部分,因此,由于Kafka、CouchDB等命名规则的约定,应用通道名称必须使用小写的字母、数字、点或中划线,长度小于250个字符且首字符必须为字母(common/configtx/validator.go中的validateChannelID()函数负责检查),以防止CouchDB数据库等命名冲突问题。

(5)锚节点配置更新交易文件

generateChannelArtifacts()函数执行如下configtxgen命令,创建指定组织的锚节点配置更新交易文件Org1MSPanchors.tx与Org2MSPanchors.tx。每个组织的锚节点配置更新交易文件都必须单独生成,再执行peer channel update命令进行更新。

        # 创建Org1MSP组织锚节点配置更新交易文件
        $CONFIGTXGEN  -profile  TwoOrgsChannel  -outputAnchorPeersUpdate  ./channel-
            artifacts/Org1MSPanchors.tx -channelID $CHANNEL_NAME -asOrg Org1MSP
        # 创建Org2MSP组织锚节点配置更新交易文件
        $CONFIGTXGEN  -profile  TwoOrgsChannel  -outputAnchorPeersUpdate  ./channel-
            artifacts/Org2MSPanchors.tx -channelID $CHANNEL_NAME -asOrg Org2MSP

至此,generateChannelArtifacts()函数执行完毕。generateArtifacts.sh脚本返回至network_setup.sh,判断是否启用了CouchDB标志位(默认不开启)。接着,network_setup.sh配置CHANNEL_NAME与TIMEOUT变量,使用docker-compose工具执行指定的docker-compose-cli.yaml文件,负责启动Fabric网络提供服务。

        CHANNEL_NAME=$CH_NAME TIMEOUT=$CLI_TIMEOUT docker-compose -f $COMPOSE_FILE up -d
            2>&1

最后,generateArtifacts.sh脚本打印日志信息,并结束Fabric网络启动工作。

3.启动Orderer服务节点

如代码清单1-7与代码清单1-8所示,Orderer节点继承了base/docker-compose-base.yaml中的orderer.example.com配置属性。其中,e2e_cli示例中Orderer节点的配置文件位置如表1-3所示。同时,还可以通过设置环境变量或运行时命令行选项进行重新配置。Orderer节点容器启动时执行如下命令,并基于创世区块文件orderer.genesis.block创建系统通道。

        orderer  # 实际上是默认启动orderer start子命令

表1-3 Orderer排序节点配置文件列表

代码清单1-7 docker-compose-cli.yaml配置文件中Orderer节点与Kafka、Zookeeper集群配置的源码示例

          examples/e2e_cli/docker-compose-cli.yaml文件
        version: '2'
        services:
            zookeeper0:
                container_name: zookeeper0
                extends:
                    file: base/docker-compose-base.yaml
                    service: zookeeper
                environment:
                    - ZOO_MY_ID=1
                    - ZOO_SERVERS=server.1=zookeeper0:2888:3888  server.2=zookeeper1:
                          2888:3888 server.3=zookeeper2:2888:3888
            zookeeper1:
                container_name: zookeeper1
                extends:
                    file: base/docker-compose-base.yaml
                    service: zookeeper
                environment:
                    - ZOO_MY_ID=2
                    - ZOO_SERVERS=server.1=zookeeper0:2888:3888  server.2=zookeeper1:
                          2888:3888 server.3=zookeeper2:2888:3888
            zookeeper2:
                container_name: zookeeper2
                extends:
                    file: base/docker-compose-base.yaml
                    service: zookeeper
                environment:
                    - ZOO_MY_ID=3
                    - ZOO_SERVERS=server.1=zookeeper0:2888:3888  server.2=zookeeper1:
                          2888:3888 server.3=zookeeper2:2888:3888
            kafka0:
                container_name: kafka0
                extends:
                    file: base/docker-compose-base.yaml
                    service: kafka
                environment:
                    - KAFKA_BROKER_ID=0
                    - KAFKA_MIN_INSYNC_REPLICAS=2
                    - KAFKA_DEFAULT_REPLICATION_FACTOR=3
                    - KAFKA_ZOOKEEPER_CONNECT=zookeeper0:2181, zookeeper1:2181, zookeeper2:2181
                depends_on:
                    - zookeeper0
                    - zookeeper1
                    - zookeeper2
            kafka1:
                container_name: kafka1
                extends:
                    file: base/docker-compose-base.yaml
                    service: kafka
                environment:
                    - KAFKA_BROKER_ID=1
                    - KAFKA_MIN_INSYNC_REPLICAS=2
                    - KAFKA_DEFAULT_REPLICATION_FACTOR=3
                    - KAFKA_ZOOKEEPER_CONNECT=zookeeper0:2181, zookeeper1:2181, zookeeper2:2181
                depends_on:
                    - zookeeper0
                    - zookeeper1
                    - zookeeper2
            kafka2:
                container_name: kafka2
                extends:
                    file: base/docker-compose-base.yaml
                    service: kafka
                environment:
                    - KAFKA_BROKER_ID=2
                    - KAFKA_MIN_INSYNC_REPLICAS=2
                    - KAFKA_DEFAULT_REPLICATION_FACTOR=3
                    - KAFKA_ZOOKEEPER_CONNECT=zookeeper0:2181, zookeeper1:2181, zookeeper2:2181
                depends_on:
                    - zookeeper0
                    - zookeeper1
                    - zookeeper2
            kafka3:
                container_name: kafka3
                extends:
                    file: base/docker-compose-base.yaml
                    service: kafka
                environment:
                    - KAFKA_BROKER_ID=3
                    - KAFKA_MIN_INSYNC_REPLICAS=2
                    - KAFKA_DEFAULT_REPLICATION_FACTOR=3
                    - KAFKA_ZOOKEEPER_CONNECT=zookeeper0:2181, zookeeper1:2181, zookeeper2:2181
                depends_on:
                    - zookeeper0
                    - zookeeper1
                    - zookeeper2
            orderer.example.com:
                extends:
                    file:     base/docker-compose-base.yaml
                    service: orderer.example.com
                container_name: orderer.example.com
                depends_on:   # 指定当前容器启动时需要依赖的启动容器对象
                    - zookeeper0
                    - zookeeper1
                    - zookeeper2
                    - kafka0
                    - kafka1
                    - kafka2
                    - kafka3
        ……

代码清单1-8 docker-compose-base.yaml配置文件Orderer节点与Kafka、Zookeeper集群配置的源码示例

          examples/e2e_cli/base/docker-compose-base.yaml文件
        version: '2'
    services:
        zookeeper:
            image: hyperledger/fabric-zookeeper
            restart: always
            ports:
                - '2181'
                - '2888'
                - '3888'
        kafka:
            image: hyperledger/fabric-kafka
            restart: always
            environment:
                - KAFKA_MESSAGE_MAX_BYTES=103809024           # 99* 1024* 1024 B
                - KAFKA_REPLICA_FETCH_MAX_BYTES=103809024     # 99* 1024* 1024 B
                - KAFKA_UNCLEAN_LEADER_ELECTION_ENABLE=false
            ports:
                - '9092'
        orderer.example.com:
            container_name: orderer.example.com               # 容器名称
            image: hyperledger/fabric-orderer                 # 镜像名称
            environment:  # 指定当前配置的环境变量
                - ORDERER_GENERAL_LOGLEVEL=debug
                - ORDERER_GENERAL_LISTENADDRESS=0.0.0.0
                - ORDERER_GENERAL_GENESISMETHOD=file
                - ORDERER_GENERAL_GENESISFILE=/var/hyperledger/orderer/orderer.
                    genesis.block
                - ORDERER_GENERAL_LOCALMSPID=OrdererMSP
                - ORDERER_GENERAL_LOCALMSPDIR=/var/hyperledger/orderer/msp
                # enabled TLS
                - ORDERER_GENERAL_TLS_ENABLED=true
                - ORDERER_GENERAL_TLS_PRIVATEKEY=/var/hyperledger/orderer/tls/server.key
                - ORDERER_GENERAL_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/server.crt
                - ORDERER_GENERAL_TLS_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
                - ORDERER_KAFKA_RETRY_SHORTINTERVAL=1s
                - ORDERER_KAFKA_RETRY_SHORTTOTAL=30s
                - ORDERER_KAFKA_VERBOSE=true
            working_dir:  /opt/gopath/src/github.com/hyperledger/fabric     #  容器启动后
                的工作路径
            command: orderer  # 启动容器后执行的默认命令
            volumes:  # 指定宿主机路径挂载到容器上的路径
            - ../channel-artifacts/genesis.block:/var/hyperledger/orderer/orderer.genesis.block
            - ../crypto-config/ordererOrganizations/example.com/orderers/orderer.
                example.com/msp:/var/hyperledger/orderer/msp
            - ../crypto-config/ordererOrganizations/example.com/orderers/orderer.
                example.com/tls/:/var/hyperledger/orderer/tls
            ports:  # 指定宿主机端口与容器端口之间的映射关系
                -7050:7050
    ……

4.启动Peer节点

如代码清单1-9与代码清单1-10所示,4个Peer节点继承了base/docker-compose-base. yaml中对应容器名称的配置属性。其中,e2e_cli示例中Peer节点配置文件如表1-4所示,可以通过设置环境变量或运行时命令行选项的方式重新进行配置。e2e_cli示例在节点容器启动时默认执行如下命令以启动Peer节点,此时没有加入任何应用通道。由于是在单机上部署多个节点,因此,base/docker-compose-base.yaml中定义的4个Peer节点容器端口映射到宿主机端口都是不同的。如果在生产环境中使用多个物理机分开部署Peer节点,则可以配置成统一的端口提供服务。

        peer node start

表1-4 Peer节点配置文件列表

代码清单1-9 docker-compose-cli.yaml配置文件中Peer节点配置的源码示例

          examples/e2e_cli/docker-compose-cli.yaml文件
        version: '2'

        services:
            ……
        peer0.org1.example.com:
            container_name: peer0.org1.example.com
            extends:
                file:  base/docker-compose-base.yaml
                service: peer0.org1.example.com

        peer1.org1.example.com:
            container_name: peer1.org1.example.com
            extends:
                file:  base/docker-compose-base.yaml
                service: peer1.org1.example.com

        peer0.org2.example.com:
            container_name: peer0.org2.example.com
            extends:
                file:  base/docker-compose-base.yaml
                service: peer0.org2.example.com

        peer1.org2.example.com:
        container_name: peer1.org2.example.com
        extends:
            file:  base/docker-compose-base.yaml
            service: peer1.org2.example.com
    cli:
        container_name: cli
        image: hyperledger/fabric-tools
        tty: true
        environment:
            - GOPATH=/opt/gopath
            - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
            - CORE_LOGGING_LEVEL=DEBUG
            - CORE_PEER_ID=cli
            - CORE_PEER_ADDRESS=peer0.org1.example.com:7051
            - CORE_PEER_LOCALMSPID=Org1MSP
            - CORE_PEER_TLS_ENABLED=true
            - CORE_PEER_TLS_CERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/
                  peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.
                  example.com/tls/server.crt
            - CORE_PEER_TLS_KEY_FILE=/opt/gopath/src/github.com/hyperledger/fabric/
                  peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.
                  example.com/tls/server.key
            - CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/
                  fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.
                  org1.example.com/tls/ca.crt
            - CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/
                  peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.
                  example.com/msp
        working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer
        command: /bin/bash -c './scripts/script.sh ${CHANNEL_NAME}; sleep $TIMEOUT'
        volumes:
            - /var/run/:/host/var/run/
            - ../chaincode/go/:/opt/gopath/src/github.com/hyperledger/fabric/
                examples/chaincode/go
            - ./crypto-config:/opt/gopath/src/github.com/hyperledger/fabric/peer/
                crypto/
            - ./scripts:/opt/gopath/src/github.com/hyperledger/fabric/peer/scripts/
            - ./channel-artifacts:/opt/gopath/src/github.com/hyperledger/fabric/
                peer/channel-artifacts
        depends_on:
            - orderer.example.com
            - peer0.org1.example.com
            - peer1.org1.example.com
            - peer0.org2.example.com
            - peer1.org2.example.com

代码清单1-10 docker-compose-base.yaml配置文件Peer节点配置的源码示例

          examples/e2e_cli/base/docker-compose-base.yaml文件
    version: '2'
    ……
    peer0.org1.example.com:
        container_name: peer0.org1.example.com
        extends:
            file: peer-base.yaml
            service: peer-base
        environment:
            - CORE_PEER_ID=peer0.org1.example.com
            - CORE_PEER_ADDRESS=peer0.org1.example.com:7051
            - CORE_PEER_CHAINCODEADDRESS=peer0.org1.example.com:7052
            - CORE_PEER_CHAINCODELISTENADDRESS=0.0.0.0:7052
            - CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.org1.example.com:7051
            - CORE_PEER_LOCALMSPID=Org1MSP
        volumes:
            - /var/run/:/host/var/run/
            - ../crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.
                example.com/msp:/etc/hyperledger/fabric/msp
            - ../crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.
                example.com/tls:/etc/hyperledger/fabric/tls
        ports: # 指定宿主机端口与容器端口之间的映射关系
            -7051:7051
            -7052:7052
            -7053:7053

    peer1.org1.example.com:
        container_name: peer1.org1.example.com
        extends:
            file: peer-base.yaml
            service: peer-base
        environment:
            - CORE_PEER_ID=peer1.org1.example.com
            - CORE_PEER_ADDRESS=peer1.org1.example.com:7051
            - CORE_PEER_CHAINCODEADDRESS=peer1.org1.example.com:7052
            - CORE_PEER_CHAINCODELISTENADDRESS=0.0.0.0:7052
            - CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer1.org1.example.com:7051
            - CORE_PEER_GOSSIP_BOOTSTRAP=peer0.org1.example.com:7051
            - CORE_PEER_LOCALMSPID=Org1MSP
        volumes:
            - /var/run/:/host/var/run/
            - ../crypto-config/peerOrganizations/org1.example.com/peers/peer1.org1.
                example.com/msp:/etc/hyperledger/fabric/msp
            - ../crypto-config/peerOrganizations/org1.example.com/peers/peer1.org1.
                example.com/tls:/etc/hyperledger/fabric/tls

        ports: # 指定宿主机端口与容器端口之间的映射关系
            -8051:7051
            -8052:7052
            -8053:7053

    peer0.org2.example.com:
        container_name: peer0.org2.example.com
        extends:
            file: peer-base.yaml
            service: peer-base
    environment:
        - CORE_PEER_ID=peer0.org2.example.com
        - CORE_PEER_ADDRESS=peer0.org2.example.com:7051
        - CORE_PEER_CHAINCODEADDRESS=peer0.org2.example.com:7052
        - CORE_PEER_CHAINCODELISTENADDRESS=0.0.0.0:7052
        - CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.org2.example.com:7051
        - CORE_PEER_LOCALMSPID=Org2MSP
    volumes:
        - /var/run/:/host/var/run/
        - ../crypto-config/peerOrganizations/org2.example.com/peers/peer0.org2.
            example.com/msp:/etc/hyperledger/fabric/msp
        - ../crypto-config/peerOrganizations/org2.example.com/peers/peer0.org2.
            example.com/tls:/etc/hyperledger/fabric/tls
    ports: # 指定宿主机端口与容器端口之间的映射关系
        -9051:7051
        -9052:7052
        -9053:7053

    peer1.org2.example.com:
        container_name: peer1.org2.example.com
        extends:
            file: peer-base.yaml
            service: peer-base
        environment:
            - CORE_PEER_ID=peer1.org2.example.com
            - CORE_PEER_ADDRESS=peer1.org2.example.com:7051
            - CORE_PEER_CHAINCODEADDRESS=peer1.org2.example.com:7052
            - CORE_PEER_CHAINCODELISTENADDRESS=0.0.0.0:7052
            - CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer1.org2.example.com:7051
            - CORE_PEER_GOSSIP_BOOTSTRAP=peer0.org2.example.com:7051
            - CORE_PEER_LOCALMSPID=Org2MSP
        volumes:
            - /var/run/:/host/var/run/
            - ../crypto-config/peerOrganizations/org2.example.com/peers/peer1.org2.
                example.com/msp:/etc/hyperledger/fabric/msp
            - ../crypto-config/peerOrganizations/org2.example.com/peers/peer1.org2.
                example.com/tls:/etc/hyperledger/fabric/tls
        ports: # 指定宿主机端口与容器端口之间的映射关系
            -10051:7051
            -10052:7052
            -10053:7053
    ……

实际上,CLI命令行客户端容器cli启动时执行如下script.sh脚本命令,如代码清单1-11所示。script.sh脚本顺序执行默认的测试流程,包括创建新的应用通道、添加节点、更新锚节点配置等操作,接着执行安装链码、实例化(部署)链码、调用链码、查询链码等链码操作。

      /bin/bash -c './scripts/script.sh ${CHANNEL_NAME}; sleep $TIMEOUT'

代码清单1-11 script.sh脚本文件Peer节点启动执行函数的源码示例

          examples/e2e_cli/scripts/script.sh文件
        ……
        CHANNEL_NAME="$1"
        : ${CHANNEL_NAME:="mychannel"}
        : ${TIMEOUT:="60"}
        COUNTER=1
        MAX_RETRY=5
        ORDE RER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
            ordererOrganizations/example.com/orderers/orderer.example.com/msp/
            tlscacerts/tlsca.example.com-cert.pem        # Orderer节点TLS证书文件路径
        ……
        ## 测试Orderer排序节点是否可用
        echo "Check orderering service availability..."
        checkOSNAvailability

        ## 创建应用通道
        echo "Creating channel..."
        createChannel

        ## 添加Peer节点到指定的应用通道
        echo "Having all peers join the channel..."
        joinChannel

        ##  更新通道中每个组织的锚节点配置
        echo "Updating anchor peers for org1..."
        updateAnchorPeers 0
        echo "Updating anchor peers for org2..."
        updateAnchorPeers 2

        ## 在Peer0/Org1与Peer2/Org2节点上安装链码
        echo "Installing chaincode on org1/peer0..."
        installChaincode 0
        echo "Install chaincode on org2/peer0..."
        installChaincode 2

        ## 在Peer2/Org2节点上实例化链码
        echo "Instantiating chaincode on org2/peer2..."
        instantiateChaincode 2

        ## 在Peer0/Org1节点上查询链码,检查账户余额是否为100元
        echo "Querying chaincode on org1/peer0..."
        chaincodeQuery 0100

        ## 在Peer0/Org1节点上调用链码
        echo "Sending invoke transaction on org1/peer0..."
        chaincodeInvoke 0

        ##  在Peer3/Org2节点上安装链码
        echo "Installing chaincode on org2/peer3..."
        installChaincode 3

        ## 在Peer3/Org2节点上查询链码,检查余额结果是否为90元
        echo "Querying chaincode on org2/peer3..."
        chaincodeQuery 3 90
        ……

5.创建、加入通道与更新组织锚节点配置

Hyperledger Fabric要求创建、加入与更新通道的权限必须是具有通道组织(Org1和Org2)的管理员身份。

(1)创建新的应用通道

e2e_cli示例中的CLI客户端容器运行script.sh脚本调用createChannel()函数,创建新的应用通道,如代码清单1-12所示。注意,CLI客户端容器在docker-compose-cli.yaml中通过volumes配置项定义成能访问所有组织用户的身份证书等文件,因此,该函数调用setGlobals()设置全局环境参数,设置如下环境变量,从而在CLI客户端中能灵活切换成指定组织的管理员角色身份,可以直接连接并操作所指定的Peer节点,并且先切换操作到Peer0/Org1节点,如代码清单1-13所示。

□ CORE_PEER_LOCALMSPID

□ CORE_PEER_TLS_ROOTCERT_FILE

□ CORE_PEER_MSPCONFIGPATH

□ CORE_PEER_ADDRESS

代码清单1-12 script.sh脚本文件createChannel()函数创建应用通道的源码示例

          examples/e2e_cli/scripts/script.sh文件
        ORDE RER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
            ordererOrganizations/example.com/orderers/orderer.example.com/msp/
            tlscacerts/tlsca.example.com-cert.pem
        ……
        createChannel() {
            setGlobals 0
            if [ -z "$CORE_PEER_TLS_ENABLED" -o "$CORE_PEER_TLS_ENABLED" = "false" ]; then
                peer  channel  create  -o  orderer.example.com:7050  -c  $CHANNEL_NAME  -f  ./
                    channel-artifacts/channel.tx >&log.txt
            else
                peer  channel  create  -o  orderer.example.com:7050  -c  $CHANNEL_NAME  -f  ./
                    channel-artifacts/channel.tx --tls --cafile $ORDERER_CA >&log.txt
            fi
            res=$?
            cat log.txt
            verifyResult $res "Channel creation failed"
            echo "===================== Channel \"$CHANNEL_NAME\" is created successfully
            ===================== "
            echo
        }

代码清单1-13 script.sh脚本文件setGlobals()函数设置全局环境参数的源码示例

          examples/e2e_cli/scripts/script.sh文件
        setGlobals () {

            if [ $1 -eq 0 -o $1 -eq 1 ] ; then
                CORE_PEER_LOCALMSPID="Org1MSP"

                CORE _PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/
                    fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.
                    org1.example.com/tls/ca.crt

                CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/
                    peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.
                    example.com/msp
            if [ $1 -eq 0 ]; then
                CORE_PEER_ADDRESS=peer0.org1.example.com:7051
            else
                CORE_PEER_ADDRESS=peer1.org1.example.com:7051
                fi
            else
                CORE_PEER_LOCALMSPID="Org2MSP"

                CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/
                    fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.
                    org2.example.com/tls/ca.crt

                CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/
                    peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.
                    example.com/msp
                if [ $1 -eq 2 ]; then
                    CORE_PEER_ADDRESS=peer0.org2.example.com:7051
                else
                    CORE_PEER_ADDRESS=peer1.org2.example.com:7051
                fi
            fi
            env | grep CORE
        }

接着,createChannel()函数采用Org1管理员身份执行如下peer命令,将该通道的配置交易文件channel.tx发送到Orderer排序节点,创建名称为“mychannel”的应用通道,并接收新建应用通道的创世区块,然后写入本地文件mychannel.block。

        peer  channel  create  -o  orderer.example.com:7050  -c  $CHANNEL_NAME  -f  ./channel-
            artifacts/channel.tx

若开启了TLS安全认证,则执行如下peer命令。其中,$ORDERER_CA是TLS认证证书文件路径。

        peer  channel  create  -o  orderer.example.com:7050  -c  $CHANNEL_NAME  -f  ./channel-
            artifacts/channel.tx --tls --cafile $ORDERER_CA

(2)Peer节点加入应用通道

e2e_cli示例的script.sh脚本调用joinChannel()函数,循环遍历所有节点并添加到应用通道中,如代码清单1-14所示。该函数首先调用setGlobals()函数切换操作指定节点,接着执行如下peer命令,使用Org1组织管理员身份将Org1组织包含的Peer0/Org1与Peer1/Org1节点加入新建的mychannel应用通道,同时将应用通道创世区块文件mychannel.block设置为命令行参数。Org2组织上类似上述操作处理Peer2/Org2与Peer3/Org2节点。

        peer channel join -b $CHANNEL_NAME.block  # 将Peer节点加入指定的应用通道

代码清单1-14 script.sh脚本文件joinChannel()函数将Peer节点加入应用通道的源码示例

          examples/e2e_cli/scripts/script.sh文件
        joinChannel () {
            for ch in 0 1 2 3; do
                setGlobals $ch
                joinWithRetry $ch
                echo  "=====================  PEER$ch  joined  on  the  channel  \"$CHANNEL_
                    NAME\" ===================== "
                sleep 2
                echo
            done
        }
        ## Sometimes Join takes time hence RETRY atleast for 5 times
        joinWithRetry () {
            peer channel join -b $CHANNEL_NAME.block   >&log.txt
            res=$?
            cat log.txt
            if [ $res -ne 0 -a $COUNTER -lt $MAX_RETRY ]; then
                COUNTER=` expr $COUNTER + 1`
                echo "PEER$1 failed to join the channel, Retry after 2 seconds"
                sleep 2
                joinWithRetry $1
            else
                COUNTER=1
            fi
                verifyResult $res "After $MAX_RETRY attempts, PEER$ch has failed to Join
                    the Channel"
        }

(3)更新应用通道上组织的锚节点配置

e2e_cli示例的script.sh脚本连续两次调用updateAnchorPeers()函数,更新两个组织的锚节点配置,如代码清单1-15所示。该函数首先调用setGlobals()函数切换操作指定节点,接着执行如下peer命令,用Org1组织管理员身份更新Org1组织上锚节点Peer0/Org1的配置,并指定锚节点配置更新文件Org1MSPanchors.tx。然后以类似操作方法处理Org2组织锚节点Peer2/Org2。其中,变量CORE_PEER_LOCALMSPID根据所属的组织被分别设置为Org1MSP或Org2MSP。

        peer  channel  update  -o  orderer.example.com:7050  -c  $CHANNEL_NAME  -f  ./channel-
            artifacts/${CORE_PEER_LOCALMSPID}anchors.tx

若开启了TLS安全认证,则执行如下peer命令。

        peer  channel  update  -o  orderer.example.com:7050  -c  $CHANNEL_NAME  -f  ./channel-
            artifacts/${CORE_PEER_LOCALMSPID}anchors.tx   --tls --cafile $ORDERER_CA

代码清单1-15 script.sh脚本文件updateAnchorPeers()函数更新锚节点配置的源码示例

          examples/e2e_cli/scripts/script.sh文件
        updateAnchorPeers() {
                PEER=$1
                setGlobals $PEER

                if [ -z "$CORE_PEER_TLS_ENABLED" -o "$CORE_PEER_TLS_ENABLED" = "false" ]; then
                peer  channel  update  -o  orderer.example.com:7050  -c  $CHANNEL_NAME  -f  ./
                    channel-artifacts/${CORE_PEER_LOCALMSPID}anchors.tx >&log.txt
            else
                peer  channel  update  -o  orderer.example.com:7050  -c  $CHANNEL_NAME  -f  ./
                    channel-artifacts/${CORE_PEER_LOCALMSPID}anchors.tx  --tls  --cafile
                    $ORDERER_CA >&log.txt
            fi
            res=$?
            cat log.txt
            verifyResult $res "Anchor peer update failed"
            echo "===================== Anchor peers for org \"$CORE_PEER_LOCALMSPID\" on
                \"$CHANNEL_NAME\" is updated successfully ===================== "
            sleep 5
            echo
        }

至此,所有组织的Peer节点都已经加入了新的应用通道mychannel,并更新完毕两个组织的锚节点配置。

6.安装、实例化与调用链码

e2e_cli示例的script.sh脚本在指定Peer节点上安装(install)和实例化(instantiate)链码,启动链码容器提供服务,即可调用链码转账与查询余额。

(1)安装链码

e2e_cli示例的script.sh脚本连续两次调用installChaincode()函数,如代码清单1-16所示。该函数执行如下peer命令,分别在Peer0/Org1节点与Peer2/Org2节点上安装chaincode_example02链码,并将该链码命名为“mycc”且版本为1.0。

        peer chaincode install -n mycc -v 1.0-p github.com/hyperledger/fabric/examples/
            chaincode/go/chaincode_example02

代码清单1-16 script.sh脚本文件installChaincode()函数安装链码的源码示例

          examples/e2e_cli/scripts/script.sh文件
        installChaincode () {
            PEER=$1
            setGlobals $PEER
            peer  chaincode  install  -n  mycc  -v  1.0  -p  github.com/hyperledger/fabric/
                examples/chaincode/go/chaincode_example02 >&log.txt
            res=$?
            cat log.txt
                verifyResult  $res  "Chaincode  installation  on  remote  peer  PEER$PEER  has
                    Failed"
            echo  "=====================  Chaincode  is  installed  on  remote  peer  PEER$PEER
                ===================== "
            echo
        }

如果链码安装成功,则在指定的安装目录(/var/hyperledger/production/chaincodes/)下保存链码包文件name.version,该文件名称含有链码名称name与链码版本version。先执行docker ps -a命令查看指定cli容器ID(例如7133b3cd32ed),再执行docker exec -it 7133b3cd32ed bash命令进入Peer0/Org1节点容器,切换并查看链码目录下是否存在已安装成功的链码文件mycc.1.0。

        # 使用docker ps -a查看指定cli容器ID(例如7133b3cd32ed)
        [root@localhost ~]# docker exec -it 7133b3cd32ed bash
        root@7133b3cd32ed:/opt/gopath/src/github.com/hyperledger/fabric/peer#  cd  /var/
            hyperledger/production/chaincodes/
        root@7133b3cd32ed:/var/hyperledger/production/chaincodes# ls
        mycc.1.0

(2)实例化(部署)链码

e2e_cli示例的script.sh脚本调用instantiateChaincode()函数实例化(部署)用户链码。Fabric采用“instantiate”命令来实例化链码,而在发送给Endorser背书节点的提案消息中封装的命令是“deploy”部署,表示将实例化数据保存到指定通道账本状态数据库的LSCC系统链码名字空间(lscc)中,包括链码数据对象(键是链码名称,值是ChaincodeData结构链码数据对象)、隐私数据集合配置信息(启用隐私数据功能的情况下)等。同时,启动链码容器以提供链码调用服务。实际上,instantiate(实例化)与deploy(部署)是同一个链码操作在系统中的不同说法。

另外,实例化(部署)链码还需要注意以下两点。

□ 实例化链码操作必须在已经安装链码的节点上执行。同一个通道内所有节点上相同链码(具有相同链码名称与链码版本)的实例化数据在通道账本中是共享的,用户只需要在通道内任意Peer节点上成功执行一次实例化链码操作,并通过排序打包出块后将实例化数据广播到其他节点上,则通道上的所有合法节点都可以通过LSCC系统链码访问到该链码的实例化数据。所以,用户需要在提供链码服务的所有节点上安装链码,但只需要成功实例化一次链码即可,而不需要重复实例化链码。一个通道账本中只会维护唯一的实例化数据,即链码数据对象与隐私数据集合配置信息(若支持),表示执行了该链码实例化(部署)操作,其他Peer节点可以直接执行调用链码、查询链码等操作请求。如果在执行这些链码操作时发现本地Peer节点没有启动对应的链码容器,则默认启动链码容器提供服务。注意,即使是相同的链码对象,不同的通道还是需要执行一次实例化(部署)链码操作,因为不同通道的实例化链码数据保存在不同通道的账本数据库上(以链ID或账本ID实现逻辑隔离)。

□ Peer节点上同一个版本链码的链码容器是共享的,支持不同通道上的链码实例化(部署)、升级、查询、调用等操作,而不会重复启动相同容器名称的链码容器,即本地只会维护一个链码规范名称(ChaincodeName:ChaincodeVersion)的链码运行时环境对象与一个容器名称(系统链码是ChaincodeName-ChaincodeVersion、用户链码是NetworkID-PeerID-ChaincodeName-ChaincodeVersion)的链码容器。事实上,链码容器是一种无状态的链码运行时环境对象,不会保存任何与交易相关的数据,而是由交易相关的交易模拟器暂时保存模拟执行结果读写集,并在验证交易合法后提交账本。因此,如果在执行链码操作(实例化/部署、升级、查询、调用等)时发现本地节点已经正常启动了指定名称版本的链码容器,则可以直接发送消息给容器请求执行,而不会重复启动新的链码容器。

如代码清单1-17所示,instantiateChaincode()函数执行如下peer命令,在Peer2/Org2节点上实例化链码mycc(版本1.0),并指定链码调用参数列表与背书策略OR('Org1MSP. peer', 'Org2MSP.peer'),表示两个组织的任意Peer节点签名都被认为是合法交易,账户A和账户B的余额分别被初始化为100元与200元。

        peer  chaincode  instantiate  -o  orderer.example.com:7050  -C  $CHANNEL_NAME  -n
            mycc  -v  1.0  -c  '{"Args":["init", "a", "100", "b", "200"]}'  -P  "OR     ('Org1MSP.
            peer', 'Org2MSP.peer')"

代码清单1-17 script.sh脚本文件instantiateChaincode()函数实例化链码的源码示例

          examples/e2e_cli/scripts/script.sh文件
        instantiateChaincode () {
            PEER=$1
            setGlobals $PEER
            if [ -z "$CORE_PEER_TLS_ENABLED" -o "$CORE_PEER_TLS_ENABLED" = "false" ]; then
                peer  chaincode  instantiate  -o   orderer.example.com:7050  -C  $CHANNEL_NAME
                    -n  mycc  -v  1.0  -c  '{"Args":["init", "a", "100", "b", "200"]}'  -P  "OR
                    ('Org1MSP.peer', 'Org2MSP.peer')" >&log.txt
            else
                peer  chaincode  instantiate  -o  orderer.example.com:7050  --tls  --cafile
                    $ORDERER_CA -C $CHANNEL_NAME -n mycc -v 1.0-c '{"Args":["init", "a", "
                    100", "b", "200"]}' -P "OR     ('Org1MSP.peer', 'Org2MSP.peer')" >&log.txt
            fi
            res=$?
            cat log.txt
            verifyResult $res "Chaincode instantiation on PEER$PEER on channel '$CHANNEL_
                NAME' failed"
            echo  "=====================   Chaincode  Instantiation  on  PEER$PEER  on  channel
                '$CHANNEL_NAME' is successful ===================== "
            echo
        }

执行实例化链码操作之后,使用docker ps -a命令查看当前节点上的容器运行状态,执行结果显示目前只有Peer2/Org2节点启动了mycc-1.0容器。注意,这里是以多行显示太长的容器信息查询结果,以加粗的容器CONTAINER ID标识每条容器信息,其中,NetworkID为dev(core.yaml文件中peer.networkId配置项), PeerID为peer0.org2. example.com(docker-compose-base.yaml文件中配置的CORE_PEER_ID环境变量), ChaincodeName为mycc, ChaincodeVersion为1.0,容器名称NAMES为dev-peer0.org2. example.com-mycc-1.0。

        [root@localhost ~]# docker ps -a
        CONTAINER ID      IMAGE      COMMAND      CREATED      STATUS      PORTS      NAMES
        0376ec9343af           dev-peer0.org2.example.com-mycc-1.0-15b571b3ce849066b7ec7449
        7da3b27e54e0df1345daff3951b94245ce09c42b    "chaincode -peer.add…"    17 hours ago
        Up 17 hours      dev-peer0.org2.example.com-mycc-1.0      # Peer2/Org2节点

(3)调用链码

至此,e2e_cli示例的script.sh脚本在Peer0/Org1节点与Peer2/Org2节点上都安装了指定链码mycc.1.0,接着在Peer2/Org2节点上实例化链码mycc(1.0版本)并启动链码容器。如代码清单1-18所示,script.sh脚本在Peer0/Org1节点上继续调用链码chaincodeQuery()函数与chaincodeInvoke()函数,启动链码容器并请求查询链码与调用链码mycc,查看账户A的余额,然后从账户A向账户B转账10元。

        peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["query", "a"]}'
        peer  chaincode  invoke  -o  orderer.example.com:7050  -C  $CHANNEL_NAME  -n  mycc  -c
            '{"Args":["invoke", "a", "b", "10"]}'

代码清单1-18 script.sh脚本文件chaincodeInvoke()函数调用链码的源码示例

          examples/e2e_cli/scripts/script.sh文件
        chaincodeInvoke () {
            PEER=$1
            setGlobals $PEER
            if [ -z "$CORE_PEER_TLS_ENABLED" -o "$CORE_PEER_TLS_ENABLED" = "false" ]; then
                peer  chaincode  invoke  -o  orderer.example.com:7050  -C  $CHANNEL_NAME  -n
                    mycc -c '{"Args":["invoke", "a", "b", "10"]}' >&log.txt
            else
                peer  chaincode  invoke   -o  orderer.example.com:7050    --tls  --cafile
                    $ORDERER_CA  -C  $CHANNEL_NAME  -n  mycc  -c  '{"Args":["invoke", "a",
                    "b", "10"]}' >&log.txt
            fi
            res=$?
            cat log.txt
            verifyResult $res "Invoke execution on PEER$PEER failed "
            echo  "=====================  Invoke  transaction  on  PEER$PEER  on  channel
                '$CHANNEL_NAME' is successful ===================== "
            echo
        }

(4)查询链码信息

e2e_cli示例的script.sh脚本调用chaincodeQuery()函数查询链码,如代码清单1-19所示。该函数在Peer3/Org2节点(主机名peer1.org2.example.com)上安装链码,接着执行如下命令查询账户A的余额,在Peer3/Org2节点上请求查询链码mycc,并检查其余额是否还剩90元,若是则说明上述调用链码进行转账的操作执行成功。

        peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["query", "a"]}'
        ------------------------------------
        Query Result: 90   # 账户A余额为90元

代码清单1-19 script.sh脚本文件chaincodeQuery()函数查询链码的源码示例

          examples/e2e_cli/scripts/script.sh文件
        chaincodeQuery () {
            PEER=$1
            echo  "=====================              Querying  on  PEER$PEER  on  channel  '$CHANNEL_
                NAME'... ===================== "
            setGlobals $PEER
            local rc=1
            local starttime=$(date +%s)

            # continue to poll
            # we either get a successful response, or reach TIMEOUT
            while test "$(($(date +%s)-starttime))" -lt "$TIMEOUT" -a $rc -ne 0
            do
                sleep 3
                echo "Attempting to Query PEER$PEER ...$(($(date +%s)-starttime)) secs"
                peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["query", "a"]}'
                    >&log.txt
                test $? -eq 0 && VALUE=$(cat log.txt | awk '/Query Result/ {print $NF}')
                test "$VALUE" = "$2" && let rc=0
            done
            echo
            cat log.txt
            if test $rc -eq 0 ; then
                echo "===================== Query on PEER$PEER on channel '$CHANNEL_NAME'
                    is successful ===================== "
            else
                echo  "! ! ! ! ! ! ! ! ! ! ! ! ! ! !  Query  result  on  PEER$PEER  is  INVALID
                    !! ! ! ! ! ! ! ! ! ! ! ! ! ! ! "
                    echo  "==================  ERROR  ! ! !  FAILED            to  execute  End-2-End
                        Scenario =================="
                echo
                exit 1
            fi
        }

接着,执行docker ps -a命令查看当前节点上的容器运行状态,执行结果显示当前Peer0/Org1节点、Peer2/Org2节点与Peer3/Org2节点都启动了mycc-1.0容器(容器完整名称使用下划线标记)提供链码服务。注意,script.sh脚本只在Peer2/Org2节点上执行过一次实例化链码操作,其余节点都是直接查询链码或者调用链码请求服务,即如果发现本地没有正常运行该链码容器,则默认启动链码容器后再请求执行其他操作。

        [root@localhost ~]# docker ps -a
        CONTAINER ID      IMAGE      COMMAND      CREATED      STATUS      PORTS      NAMES
        3bf72099be1e          dev-peer1.org2.example.com-mycc-1.0-26c2ef32838554aac4f7ad6f1
            00aca865e87959c9a126e86d764c8d01f8346ab    "chaincode -peer.add…"    17 hours
            ago         Up 17 hours      dev-peer1.org2.example.com-mycc-1.0     # Peer3/Org2
        1dfc14776024           dev-peer0.org1.example.com-mycc-1.0-384f11f484b9302df90b4532
            00cfb25174305fce8f53f4e94d45ee3b6cab0ce9   "chaincode -peer.add…"   17 hours ago
            Up 17 hours      dev-peer0.org1.example.com-mycc-1.0   # Peer0/Org1
        0376ec9343af           dev-peer0.org2.example.com-mycc-1.0-15b571b3ce849066b7ec7449
            7da3b27e54e0df1345daff3951b94245ce09c42b   "chaincode -peer.add…"   17 hours ago
            Up 17 hours      dev-peer0.org2.example.com-mycc-1.0   # Peer2/Org2

至此,Hyperledger Fabric网络已经正式启动运行,能够为用户提供正常的链码(智能合约)服务。

1.2.5 Fabric交易处理流程

Hyperledger Fabric正常启动后,用户基于Client节点从Fabric CA节点或通过工具(cryptogen等)生成合法的身份证书、签名私钥等文件,获得合法的节点身份,认证之后进入Fabric网络。此时,用户就能正常发送交易到网络中请求处理,如图1-8所示。

图1-8 Hyperledger Fabric系统中交易处理流程示意图

① 发送签名提案消息到Endorser背书节点请求处理

Client节点构造签名提案消息(SignedProposal类型),通过调用Endorser背书服务客户端的ProcessProposal()接口,提交该消息到Endorser背书节点,请求模拟执行交易提案并签名背书。

② Endorser背书节点模拟执行交易提案并签名背书

Endorser背书节点收到签名提案消息之后,进行如下处理。

□ 检查签名提案消息的格式合法性与签名有效性,包括通道头部、签名头部、签名域、交易ID、消息扩展域的ChaincodeId属性与PayloadVisibility可见性模式等;

□ 检查提案消息的创建者是否满足指定通道上的通道访问权限,即/Channel/Application/Writers写权限;

□ 检查并启动链码容器以模拟执行交易提案,并将模拟执行结果暂时保存在交易模拟器中,等待排序共识与交易验证,而不是直接更新到账本中。其中,交易模拟执行结果采用状态数据读写集(读数据的键和版本、写数据的键值)记录交易造成的状态变更结果;

□ 调用ESCC系统链码对该提案消息的模拟结果读写集等进行签名背书。

③ Endorser背书节点向客户端返回提案响应消息,并分发隐私数据明文

Endorser背书节点基于背书信息、模拟执行结果等构造提案响应消息(ProposalResponse类型),并回复给请求客户端。

目前,模拟执行结果读写集包含公有数据(包括公共数据与隐私数据哈希值)与私有数据(或隐私数据)。其中,公有数据交由Orderer节点进行排序出块,再提交到账本区块数据文件,并广播到该通道上的所有节点。如果模拟执行结果中还存在有效的隐私数据明文,则Endorser背书节点通过Gossip消息协议将隐私数据发送给通道内授权的其他节点(由隐私数据集合配置的签名策略决定),交由transient隐私数据存储对象暂时保存到本地的transient隐私数据库(LevelDB),并在提交账本时存储到隐私数据库(LevelDB),同时清理transient隐私数据库中的过期数据。

④ 处理提案响应消息

Client节点解析Endorser背书节点回复的提案响应消息,获取背书结果并检查提案响应消息状态的合法性,以判断是否收集到了足够多的符合要求的背书签名信息。

⑤ 发送交易数据给Orderer服务节点请求排序

当收集到足够多数量的符合要求的Endorser背书签名之后(由背书策略决定), Client节点基于模拟执行结果、背书签名等构造合法的签名交易消息(Envelope类型),通过Broadcast()服务接口将该消息提交给Orderer节点,请求交易排序处理。其中,配置交易消息不需要经过Endorser节点处理。

⑥ Orderer服务节点对交易进行排序并构造新区块

Orderer排序节点提供Solo类型(用于单节点测试)、Kafka类型(支持CFT容错)等共识组件,对符合通道处理要求的合法交易消息(普通交易消息、配置交易消息等)进行排序并达成一致观点,并对一段时间内接收的一批交易消息按照打包交易的出块规则(出块周期时间、区块字节数限制、配置交易单独出块等)构造新区块,创建应用通道或更新通道配置,同时提交账本。

⑦ Leader主节点请求Orderer服务节点发送通道账本区块

Leader主节点通过Deliver()服务接口代表组织从Orderer节点请求通道账本上所有的区块数据,并通过Gossip消息协议分发到组织内的其他Peer节点。如果请求的区块数据不存在,则Orderer节点默认阻塞等待,直到指定区块创建完成并提交账本,再将该区块发送给Leader主节点。

⑧ Committer记账节点验证交易并提交账本

Committer记账节点对区块与隐私数据(明文)执行如下检查,并提交至本地账本。如果不存在隐私数据明文,则跳过隐私数据的相关检查与提交账本的步骤。

□ 检查交易消息格式的正确性、签名合法性、交易内容是否篡改、消息头部的合法性等。

□ 调用VSCC系统链码,验证收集的签名背书结果是否符合指定的背书策略。

□ 对模拟结果中公有数据(即区块数据,含有公共数据与隐私数据哈希值)的读写集执行MVCC检查,针对单个键查询、键范围查询、隐私数据哈希值三种情况,检查读数据版本与交易时的账本是否一致,即是否存在读写冲突,并将存在冲突的交易标记为无效交易。

□ 验证模拟结果中隐私数据的正确性,遍历区块中有效交易的隐私数据读写集哈希值,取出对应交易的原始隐私数据读写集明文,重新计算其哈希值并对两者进行比较。如果两者完全相同,则说明该交易的隐私数据是真实有效的。

□ 保存所有的区块数据(即公有数据)到区块数据文件中,保存所有的私有数据(即隐私数据)读写集到隐私数据库(LevelDB)中,建立区块索引信息到区块索引数据库,将最新的有效交易数据(包含公共数据读写集、隐私数据读写集、隐私数据读写集哈希值)更新到状态数据库,最后将区块数据中经过Endorser背书的有效交易数据同步到历史数据库。同时,清理transient隐私数据库中的过期数据。

⑨ Leader主节点分发数据与状态同步

Leader主节点基于Gossip消息协议将区块数据分发到组织内的其他节点上。同时,节点之间通过反熵算法等机制主动拉取缺失的数据(区块数据与隐私数据)、节点身份信息等,以确保组织内所有节点上的账本数据等信息保持同步。

⑩ Committer记账节点验证交易并提交账本(同步骤⑧)

至此,Hyperledger Fabric系统上的一次完整交易处理流程即告结束。