1.3 Hyperledger Fabric源码分析说明
1.3.1 源码分析思路
本书以Hyperledger Fabric初始化启动流程、交易处理流程等流程为主线,分析Fabric核心模块,介绍其核心设计思想,如表1-5所示。
表1-5 Hyperledger Fabric源代码主要目录文件说明表
□ 核心模块:担任功能节点角色的模块,提供核心功能服务,包括Orderer排序节点与Peer节点(包含Endorser背书节点与Committer记账节点),将在第2章至第5章中分析各个模块的初始化启动流程、处理交易流程以及与其他模块间的交互过程,并介绍其核心功能的设计与实现细节。
□ 公共模块:为核心模块和其他模块提供基础支持服务,包括bccsp、common、core、events、gossip、msp、protos等目录代码。其中,第6章介绍了Gossip消息模块,为Peer节点提供安全、可靠、可扩展的P2P数据分发协议。第7章介绍了公共功能模块,包括账本数据存储模块、安全服务模块、事件模块等,为其他模块提供底层存储机制、安全机制、异步通信机制等。
□ 辅助模块:为其他模块提供辅助性工具、运行环境、测试用例、文档等,不作为分析重点,视情况分析以帮助理解其他模块。
同时,分析代码时应注意Hyperledger Fabric代码的常用惯例,这样做将有助于理解数据结构与代码功能,比较典型的情况包括:
□ 同一个模块的相关代码可能存在于其他多个模块相同模块名称的目录下,需要结合相关目录下的代码共同研究,如分析Peer节点需要结合fabric/peer、core/peer、core/chaincode、protos/peer等目录。
□ mocks目录下通常包含模拟功能模块源码以用于测试环境或数据,test名称目录通常包含测试代码与数据,sample名称目录通常包含示例代码。
□ Fabric中经常采用提供者模式(Provider Pattern)来分离接口定义与具体实现。通常,核心数据结构都存在XXX接口类型、xxxImpl/YXXX接口实现类型、XXXProvider提供者接口类型、xxxProviderImpl/YXXXProvider提供者接口实现类型等。注意,接口实现类型名称大多数是将接口类型名称XXX的第一个有效单词变为小写字母xxxImpl或直接在前面添加实现功能名称(可能会对XXX略作修改)为YXXX,具体说明如下。
○ XXX接口类型通常用于定义该模块提供的功能方法接口,大多数是Interface接口并且没有定义属性字段,xxxImpl/YXXX实现类型会具体实现对应的接口方法和定义属性字段。例如,deliverServiceImpl类型实现了DeliverService接口类型,LedgerCommitter类型实现了Committer接口类型。
○ XXXProvider提供者类型本身通常不会直接实现核心数据结构,而只是提供生成或获取XXX接口类型或实现类型的方法描述,由xxxProviderImpl/YXXXProvider提供者类型实现对应的方法,可以直接获取实际功能的接口或实现对象,如blocksProviderImpl类型实现了BlocksProvider接口类型。
□ XXXSupport类型通常都是对XXX类型进行操作的支持对象,一般会包含XXX类型对象、相关辅助工具以及资源。
□ 代码中defaultXXX类型的数据结构通常都是XXX实现类型,其通常作为核心数据结构的默认初始对象。
□ 本文在分析时使用“→”符号表示程序执行路径上的方法或函数调用关系。
□ 以大写字母开头的结构字段与方法、数据结构类型与函数通常是公共的,可以提供给外部程序公开调用。以小写字母开头的结构字段与方法、数据结构类型与函数通常都是私有的,一般提供给内部模块调用。
本书为防止分析函数调用过深,从而影响到主线流程的分析,考虑到篇幅有限,并确保清晰简洁,在分析时将遵循如下原则。
□ 所有代码清单首行标注的源码文件位置均默认处于fabric源码目录下。
□ 尽可能在一个章节内分析属于同一个功能模块的代码,有可能属于同一个功能模块的相关代码分布在不同的包或模块中。对不复杂的跨功能模块函数调用操作,文中摘选核心代码片段直接解释说明,但将其相对独立完整的模块执行流程仍保留在独立章节中进行分析,如Endorser背书流程等,适当添加引用章节号以便于索引。
□ 与逻辑主线相关度不高的繁琐底层细节操作,或者较少代码的操作,如简单的参数合法性检查、错误检查、打印处理信息等,限于篇幅,都不提供代码清单或直接省略。
□ 文中有时会将过长的源代码分割开来在同一个章节中进行解析,这些代码在源代码中可能属于同一个函数中连续执行的程序。
□ 文中标注了重要变量的原始数据结构类型,以方便读者在源码中进行检索查询。
Hyperledger Fabric 1.1.0实现了很多实验新特性,在正式发布版本中,这些新特性都处于关闭状态或者将参数默认设置为空,其目的是不影响后续版本代码的功能升级,同时又能兼容当前发布版本的稳定性。Fabric源码采用了Makefile管理源码构建工作,很多实验新特性的源码都存在正式代码与实验代码两个版本。用户可以在使用make工具编译源代码时添加打开实验版本标志位metadata.Experimental为true,那么在编译Docker镜像时就会自动添加gotags,即打开实验版本“experimental”选项,从而支持测试很多新特性功能,如链码API支持隐私数据、Java链码实例化与调用等。本书不会专门对所有实验版本新特性进行深入分析,但会解析重大的版本新特性设计,如支持隐私数据集合(Private Data Collection, PDC)新特性,即通道内隐私数据的细粒度隐私保护机制。Hyperledger Fabric 1.2.0及以后版本已经开始支持新特性,读者可以对照学习。
事实上,Fabric社区一直以来就在积极讨论增强通道内隐私保护的新特性,如FAB-1151、FAB-2961、FAB-4976、FAB-8718等。Fabric 1.0.0中引入了多通道(Multiple-channel)机制实现了隔离数据特性,防止通道外的节点非法获取这些受保护的数据,起到了保护通道内公共数据隐私性的目的。但是,这种设计机制仍然存在以下不足之处。
□ Orderer排序节点保存了所有通道的账本数据,实际上该节点不读取使用任何账本数据,仅仅为所有通道保存账本数据和提供访问服务。如果存在隐私数据,则同样会暴露给Orderer节点。
□ 通道内的Peer节点拥有该通道上所有组织的账本数据,包括与本地节点无关的交易数据。
□ 通道隔离机制提供的数据粒度较粗。如果所有组织之间都需要共享彼此的隐私数据,又不希望其他组织看见自己的所有数据,则每个组织之间都需要构建通道,大约需要构建O(n2)量级的通道数量。同时,通道暂时还不支持删除操作,缺乏灵活性。
□ 缺乏动态访问控制机制,即在运行过程中还不支持动态控制节点对数据的访问权限。
目前,Fabric社区正在努力推进工作以增强Endorser背书环节、Orderer排序环节以及Committer提交账本环节中的数据隐私性,存在采用加密机制或Hash机制(可以结合加盐即随机字符串或HMAC等机制提高安全强度)两种较为成熟和灵活的方案。但前者需要解决密钥的分发与管理问题,可能带来设计上新的复杂性,这也是Fabric 1.1.0在实验版本中开始支持隐私数据集合及Side DB数据库的基本动机。
新特性采用了Hash机制,即背书节点对交易提案执行结果中的私有数据(即隐私数据)计算哈希值,签名后返回客户端以验证背书节点结果的一致性。同时,将隐私数据明文通过Gossip消息协议传播给授权组(以静态或动态方式指定)的Peer节点集合,其他节点只能接收到隐私数据哈希值作为交易证据,且不需要创建独立的通道,从而实现比通道更细粒度与低开销的数据隐私保护机制。另外,新特性包括隐私数据库(Side DB数据库)、transient隐私数据存储对象(封装了transient隐私数据库)等。当节点提交账本时,将隐私数据读写集更新到隐私数据状态数据库中,将区块数据即公有数据(公共数据与隐私数据哈希值)读写集更新到区块数据文件中。
Fabric 1.2.0及其后续版本(推荐使用隐私数据的生产环境采用)开始在正式版中支持通道隐私数据集合新特性,在1.1.0实验版本的基础上对隐私数据添加了隐私数据集合配置信息,并保存到transient隐私数据库,同时在账本的隐私数据库中添加了区块有效范围信息,另外,还支持服务发现(Service Discovery,以简化客户端应用程序)、可插拔的背书及验证系统链码(Pluggable E/V Syscc,将原先的ESCC与VSCC系统链码实现为灵活的插件对象)等新特性。Fabric 1.3.0支持CouchDB分页机制、Java语言链码支持、基于通道的事件服务等新特性。有兴趣的读者可以通过Jira(https://jira.hyperledger.org/)查看项目更多的开发计划与任务进展情况。
1.3.2 配置机制
Fabric配置机制包括配置文件、配置环境变量与配置命令行选项参数,如表1-6所示。Fabric通过Viper组件管理配置项的解析、设置与获取等操作,Viper组件可以读取系统环境变量、yaml/json配置文件(orderer.yaml和core.yaml)等与绑定命令行选项参数,解析并转换为配置项键值对再存储到Viper组件中,从而灵活支持多种配置方式。通常,Fabric调用InitViper()函数初始化Viper组件的配置文件路径以及指定配置文件名称,默认在$FABRIC_CFG_PATH(如/etc/hyperledger/fabric)路径下查找配置文件,在找不到文件时依次查找当前目录、默认开发配置目录($GOPATH/src/github.com/hyperledger/fabric/sampleconfig)和系统默认配置路径(/etc/hyperledger/fabric)下的配置文件,接着调用viper.SetConfigName()方法,指定配置文件名称。然后,调用Viper组件的ReadInConfig()方法,从上述指定配置路径上加载指定名称的配置文件,解析成配置项键值对再存储到Viper组件中。Viper组件的常用基本用法如下。
viper.SetConfigName("core") // 设置配置文件名称为core viper.AddConfigPath("/etc/appname/") // 设置搜索的配置文件路径,可以设置多个 viper.AddConfigPath("$HOME/.appname") viper.AddConfigPath(".") viper.ReadInConfig() // 查找并读取配置文件 viper.Get("key") // 获取配置项key对应的属性值 viper.Set("key", "value") // 设置配置项key的值为value
表1-6 Hyperledger Fabric配置机制列表
用户可以通过设置环境变量来改变配置项的属性值,环境变量与配置文件中的配置项名称是严格对应的。例如,orderer.yaml配置文件中General配置下的LogLevel默认为info级别,若设置环境变量ORDERER_GENERAL_LOGLEVEL=NOTICE,则将日志级别更改为NOTICE级别。注意,Peer节点和Orderer节点相关环境变量分别是以CORE_和ORDERER_前缀开头的大写字符串。
另外,用户可以通过设置命令行选项参数来改变配置项的属性值,Fabric采用Cobra组件处理命令行请求,通过重置命令行选项参数来绑定Viper组件指定的配置项,以改变指定配置项的属性值。如代码清单1-20所示,在Peer节点的启动阶段,Cobra组件创建主命令选项mainFlags及其logging-level选项,接着Viper组件将其绑定到自身的logging_level配置项,这样,用户就可以在peer命令行中设置当前模块的日志级别。
因此,设置命令行选项参数、环境变量、配置文件在设置配置项时的优先级是依次降低的。
代码清单1-20 main()主程序的源码示例
peer/main.go文件 func main() { …… // 定义命令行选项集合 mainFlags := mainCmd.PersistentFlags() …… // 设置logging-level选项 mainFlags.String(“logging-level”, “”, “Default logging level and overrides, see core.yaml for full syntax”) viper.BindPFlag(“logging_level”, mainFlags.Lookup(“logging-level”)) // Viper组件配置绑定的命令行选项 …… }