Hyperledger Fabric

引言:Hyperledger Fabric:一个私有的需要许可的区块链

Hyperledger Fabric

Hyperledger是Linux基金会的一个开源项目,负责超级账本项目,而Hyperledger fabric(以下简称fabric)是其中之一。

Fabric特性

  • 支持模块化:项目的各个细节都可以进行不同的实现(包括共识协议、身份认证、秘钥管理协议等)
  • 需要许可:与比特币和以太坊不同,需要认证许可(意味着Fabric需要成员之间有基本的信任)
  • CFT共识协议:不一定要实现BFT,CFT也是可以的
  • 智能合约支持的语言:Java、Go、NodeJS

PKI

基本概念

公钥基础结构PKI:一组互联网技术,或是一种认证标准,用于在网络中进行安全通信;(比如HTTPS的TLS安全传输层服务就是PKI的模式)

PKI的核心思想是:使用数字证书来验证身份和加密

  • 数字证书是一种包含公钥和身份信息的文件,由证书授权中心CA签发和管理。
  • 公钥是用来加密或是解密的密码学工具,与私钥对应(对称加密与非对称加密)

PKI可以类比为身份证系统:

数字证书就像身份证,包含了个人信息和照片(相当于公钥)。

CA就像派出所,负责颁发和管理身份证,并做了防伪措施(相当于数字签名)。

当两个人交流时,他们可以互相出示身份证,以确认对方是谁,并进行加密通话(相当于加密数据,当然这个现实中做不到)。

四个关键要素

PKI 有四个关键要素:

  • 数字证书:最常见的证书类型是符合 X.509标准的证书
  • 公钥和私钥
  • 证书授权中心
  • 证书撤销列表

公钥与私钥的认证流程

对于公钥和私钥:如图所示

私钥加密、公钥解密

bob 发送时:

  1. 将消息 “我爱你” 先进行数字摘要(比如 MD5),生成一个摘要 Digest
  2. 然后使用私钥对 Digest 加密,生成签名 Signature1
  3. 发送时发送:消息 + Signature(两个都要发送)

Alice 接收时:

  1. Alice 接收到的消息是 “我不爱你”,对这个消息也进行摘要,生成 Signature2
  2. Alice 使用公钥对 Signature 进行解密,得到 Signature1
  3. 对比 Signature1 与 Signature2,发现不同,Alice 就知道消息被篡改了

CA机构的运作流程

证书授权中心CA一般是被网站、系统认可的一个机构,它负责颁布数字证书。

CA机构给参与者签名公钥

证书授权中心向不同的参与者颁发证书(证书是一个包含了公钥和身份的文件)

这些证书由 CA 进行签名,并将参与者的公钥绑定在一起

因此:如果一个参与者信任这个CA机构,那么他也可以信任与它绑定在一起的证书内包含的公钥

根CA中间CA:CA有两种,中间CA可以分担一部分根CA的任务,也可以防止根CA的暴露;

信任链:根CA与中间CA有信任链,并且中间CA可以与其他含有信任链的中间CA建立信任链

信任链

在区块链网络Fabric中,提供了Fabric CA(Fabric网络中的根CA),用于管理区块链网络中用户和节点的身份证书

证书撤销列表

证书撤销列表CRL:保存撤销的证书的列表

第三方机构验证另一方的身份,就需要检查CA的CRL确保证书是否被撤销

这个检查不是必须的,但是不检查就会承担无效身份的风险

成员服务提供者MSP

Fabric是一个私有的链:意思是不能像比特币或是以太坊那样允许任何节点加入,fabric需要基本的信任,因此fabric其实是一个私有的区块链(联盟链)

因为需要基本的信任,所以Fabric区块链的参与者需要一种向网络中的其他参与者证实自己身份的机制从而在网络中进行交易(好像与CA的职责很类似)

MSP与CA的区别:

MSP与CA的区别

CA像是颁布信用卡的机构,CA可以颁布各式各样的信用卡;

MSP是本商店承认的允许使用的信用卡列表

Peer节点

Peer

Peer节点是网络中最重要的组成部分,Peer储存了账本和链码(链码和账本将网络中共享的流程和信息对应地封装起来)

Peer与账本与链码的关系:一个Peer节点可以有多个账本与链码

并且Fabric自动给账本与链码做了冗余备份,防止单点失效

账本数量和访问账本的链码的数量之间没有固定的关系

作用:

  • 存储链码与账本
  • 与APP或是管理员交互

APP与Peer

APP与Peer交互过程图

如图A连接到P1想要通过链码S1来查询或更新账本L1:

查询账本需要三步对话:

  1. APP通过Fabric SDK连接到Peer节点上

  2. APP调用安装在Peer上的链码(发送提案 Proposer

    1. Peer使用提案调用链码
    2. 链码通过查询账本生成查询(更新)的响应 Response
  3. Peer返回给A提案响应

如果只是查询操作到此就结束了,更新账本的操作需要额外的两步:

  1. APP从所有的响应中创建一笔交易 Transaction,发送交易给Order节点排序 Order
    1. Order将交易打包为区块,发送给所有Peer节点
    2. Peer对区块进行验证,将交易提交到账本(一个独立的 Peer 节点目前是不能进行账本更新的,因为其他的 Peer 节点必须首先要同意这个变动(即达成共识))
  2. 账本更新后,会生成一个回调事件,返回给App信息

通道与Peer

账本的基本单位是通道(Channel),每个通道内的成员可以共享账本,不同通道内账本则彼此隔离

  • 账本实际上在Peer上存储,但是逻辑上账本属于一个通道
  • 通道内组件可以进行私密交易
  • 通道可以理解为由物理Peer节点组成的逻辑结构(因为Peer提供了对channel的访问和管理的控制)

PS:在某些其他区块链平台中,有群组的概念,其实就是为了实现通道的功能

组织与Peer

区块链网络是由多个组织来管理的,而不是单个组织;

组织越多,这个网络也就越庞大,Peer就是组织的资源

节点与组织

排序节点并没有在图上标识,除了排序节点外,其他一切都是非中心化的

身份与Peer

Peer 节点会有一个身份信息被分给他们,这是通过一个特定的证书认证机构CA颁发的数字证书(包括公钥和身份信息)来实现的

peers节点身份

如图所示,有两个组织、两个CA机构、两个MSP、一个channel、四个节点及他们的身份信息。

  • Peer 节点连接到一个通道的时候,它的数字证书会通过通道MSP来识别它的所属组织
  • 一个 Peer 节点只能被一个组织所有,因此也就只能被关联到一个单独的 MSP(P1 和 P2 具有由CA1颁发的身份信息;P3和P4有CA2颁发的身份信息)
  • MSP负责提供身份信息与组织的映射关系,还决定了一个Peer节点在组织中的角色及对网络中资源的访问权限,一个Peer只能关联一个MSP

PS:

Peer 节点、应用程序、终端用户、管理员以及排序节点如果想同一个区块链网络进行交互的话,必须要有一个身份信息和一个相关联的 MSP。

我们使用身份信息来为每个跟区块链网络进行交互的实体提供一个名字——一个主角(principal

排序节点与Peer

应用程序和 Peer 节点彼此互相交互来确保每个 Peer 节点的账本永远保持一致是通过以排序节点作为中心媒介的一种特殊机制。

在一个账本的更新被应用到 Peer 节点的本地账本之前, Peer 节点会请求网络中的其他 Peer 节点来批准这次更新(即Peer节点需要达成共识)。


想要更新账本的应用程序会被引入到一个三阶段的流程

(其实这里的三阶段流程类似于经典的三阶段提交流程,对比学习可以check这个链接

第一阶段:APP发送提案给Peer

peer提案

  1. APP生成一笔交易的提案,它把提案发送给一系列的被要求的节点(即背书节点)来获得背书。例如:图中的A1生成T1然后传输给背书节点P1与P2

(名词解释:提案就是输入参数,比如执行一个交易所需要的输入;背书的意思就是支持,得到背书就是得到节点的支持)

  1. 背书节点会将提案初步进行执行,但是并不更新刷新到账本,只是简单地为它提供签名然后将它返回给应用程序,即返回提案响应。例如:图中P1与P2使用交易与提案执行链码S1与S2,同意提案(即生成背书),并且返回响应给A1

  2. 应用程序接收到有效数量的被签过名的提案响应之后,交易流程中的第一个阶段就结束。例如:A1收到两个背书响应E1与E2

问题1:APP会选择那些Peer节点呢?

取决于背书策略(背书策略在链码中指定):策略定义了一个交易在能够被接收前,需要哪些节点为这个提案做背书

问题2:得不到背书节点的支持会发生什么呢?

得不到背书的提案,说明此时有不一致的情况发生,APP可以放弃此提案,如果坚持使用没有得到背书的提案在第三阶段会被拒绝

问题3:链码在这一阶段是怎样的?

三个阶段中,其实交易已经在第一节阶段执行了链码,在之后的过程中与链码无关

第二阶段:排序并将交易打包到区块

这一阶段的主人公是Order节点:

  • 接收背书提案并将其排序
  • 打包进区块

排序节点需要做的就是将多个背书提案排序,打包为区块分发给各个Peer节点

排序节点执行过程

如图所示,A1、A2、A3将背书过的交易,发送给排序节点,排序节点将这些交易打包为一个块B2,排序的顺序是T1、T2、T3、T4、T5

注意以下几点:

  • 虽然图中只有一个排序节点,但是排序节点可以不止一个
  • 排序的顺序不一定与排序服务接收的顺序相同
  • 排序节点打包后的区块是最终的,不像以太坊、比特币会产生账本的分叉

第三阶段:验证与提交

在排序节点发送打包后的区块给Peer后(分发),每个 Peer 节点上区块中的每笔交易都会被验证(即检查背书),失败的交易会被统计,成功的交易会被提交

验证与提交

如图所示:O1将打包后的区块B2发送给P1与P2,P1与P2分别check区块内每一项交易的背书,视情况添加到账本中

注意:

  • 排序节点分发区块给各个Peer时,不一定需要连接所有的Peer,Peer之间可以使用Gossip协议传输区块
  • 无效的交易仍然保留在排序节点创建的区块中

智能合约与链码

智能合约与链码的关系

链码(或者智能合约)是对外的API接口,便于APP对账本进行查询或更新

在Hyperledger Fabric中,经常混用智能合约与链码,一般我们认为两个逻辑一样,但是如果非要找一个区别:链码是一个更大的范围,链码包含多个智能合约,并且包含了打包部署的过程

举个例子:比如vehicle chaincode链码包含多个智能合约car contractboat contracttruck contract

背书策略

在以太坊与比特币中没有此概念,这是Fabric独有的。Hyperledger Fabric更真实地模拟了现实世界

每个链码有一个背书策略,策略指明了区块链网络中哪些组织必须对一个给定的智能合约所生成的交易进行签名,以此来宣布该交易有效

有效交易

所谓有效交易:

  • 检查背书策略:是否有要求的组织的背书
  • 交易在背书节点签名时,交易的读集与世界状态的值匹配,并且中间过程中没有被更新

注意:所有交易,不管是有效的还是无效的,都会被添加到区块链历史中,但是仅有效的交易才会更新世界状态。

下面是一个例子:ORG1转让一辆车CAR1给ORG2

有效交易

如图所示,上半部分从左到右一次是APP、链码、背书策略;下半部分是一个交易

所有的交易都有:一个标识符identifier、一个提案proposal、一个被一群组织签名的响应response

标识符表名这是第三个交易t3,并且输入是CAR1、ORG1、ORG2,输出是{CAR1.owner=ORG1,CAR1.owner=ORG2},来表示CAR1之前属于ORG1,现在属于ORG2。可以看到输出被ORG1与ORG2签名表示认可

系统链码

区块链系统定义了一些系统级别的链码:生命周期系统链码、配置系统链码、查询系统链码、背书系统链码、验证系统链码

链码的生命周期

官方文档,在开发完成链码后,链码的声明周期由以下四个步骤组成:

  1. 打包链码:一个或者每一个组织完成。
  2. 安装链码到peer节点:每一个使用链码或是为链码背书的组织
  3. 为你的组织批准链码定义:每一个使用链码的组织
  4. 提交链码定义到链上:一个组织即可

1、打包链码

此过程可以使用第三方工具,但是完全可以使用peer指令完成。

打包的链码需要包括两个部分:

  • 打包的jar,比如mychaincode.jar需要打包为mychaincode.tar.gz
  • metadata.json文件,内容包括了链码的语言、路径、标签

2、安装链码

用peer所在组织的管理员身份Peer Administrator,在每个要执行和背书交易的peer节点上安装链码包。

  • 如果链码有错误,安装过程中会返回错误。
  • 在构建成功后,会返回一个包ID(之后会使用到,没保存也可以查询到)

3、批准定义

链码定义的作用:通过每个组织对链码定义的投票来决定,是否让这个链码运行在通道上。

当通道成员批准一个链码定义,这个批准便作为一个组织在接受链码参数方面的投票,来决定是否允许这个链码运行在通道上

链码定义的内容:

  • 名称:应用调用链码时使用的名称
  • 版本:一个版本号或者和给定链码包关联的值
  • 序列号:链码被定义的次数。这个值是一个整数,并且被用来追踪链码的更新次数。第一次安装并且同意一个链码定义,这个序列号会是1。当你下一次更新链码,序列号会是2。
  • 背书策略
  • 包ID(第二步得到的ID号)

这个批准操作需要提交给排序服务,在此之后会分发给所有的 peer 节点

在批准交易被成功提交后,同意的定义存储在你的组织中所有 peer 节点都可访问到的集合中(有多个 peer 节点,也只需要该组织同意一次)

4、提交链码到链上(通道)

当一个通道上的多数成员同意链码定义,就可以将链码提交到通道

提交时需要以组织管理员的身份来提交Organization Administrator

在链码定义已经提交到通道上后,链码容器会将安装到的 peer 节点上启动,来允许通道成员开始使用链码。

账本

账本

fabric账本子系统包括两个组件:

  • 世界状态:记录给定时间点的账本状态(世界状态就是账本数据库
  • 交易日志(即区块链):记录产生当前世界状态的所有交易(历史记录)

关于账本,需要注意几点:

  1. 世界状态+交易日志才能组成完整的账本;
  2. 世界状态其实是一个键值数据库(可以使用任何想使用的KV数据库);
  3. 交易日志是一个区块链
  4. 区块链总是以文件实现,世界状态以数据库实现
  5. 区块链是属于通道的

世界状态与区块链的图示:

世界状态

世界状态就是一个KV数据库,Value可以是一个复杂的KV串。世界状态的实现可以使用 LevelDB(默认数据库,适合简单的KV对)和 CouchDB(适合JSON文档)

区块链结构

区块链的第一块没有存放交易,称为创世区块,在Fabric中,创世区块包含了网络配置

区块的结构

区块结构

区块由三部分构成:

  • 区块头
    • 区块编号:从0开始,每新增一个区块+1
    • 当前区块hash值
    • 前一个区块的hash值
  • 区块数据:包含一个有序的交易列表
  • 区块元数据:包含了区块被写入的时间,还有区块写入者的证书、公钥以及签名

交易的结构

交易结构

交易的组成有5部分:

  • 头 Header:记录重要的元数据,比如链码名及版本
  • 签名 Signature:APP创建的加密签名,用来检查交易是否被篡改
  • 提案 Proposal:包含了智能合约的输入参数
  • 响应 Response:它是以读写集 (RW-set)的形式记录下世界状态之前和之后的值
  • 背书 Endorsements:一组签名交易响应(背书策略规定的一些组织的签名)

排序服务

Hyperledger Fabric的排序特点

以太坊和比特币是依靠概率共识算法(如PoW、部分PoS),而Hyperledger Fabric是依靠确定性共识算法(如Paxos、Raft、PBFT),这意味着Fabric的账本不会产生分叉

除此外,Fabric的排序节点将排序与背书分离,提高了排序的效率

排序节点

排序节点的职责:

  • 排序与分发:接收背书提案将其排序,以及打包为区块
  • 维护允许创建通道的组织列表(联盟)
  • 基本的访问控制:限制谁可以读写数据,以及谁可以配置数据

排序服务的实现

在2.0开始全面使用Raft,在之前的版本中使用过Kafka、Solo(现在都已经被淘汰)

关于Raft算法可以参考另一篇博客:Raft

Fabric网络模型

官网示例了一个基础的网络,这里介绍一下这个网络模型

network.structure

网络的成员

  • 组织:R1、R2、R3、R4表示不同的组织,比如三家银行与一个政府单位
  • CA机构:CA1、CA2、CA3、CA4每个组织一个,不同组件使用证书表示自己来自于哪一个组织(通过MSP匹配CA与组织)
  • 节点:P1、P2、P3,网络中的实际运行者
  • 通道:C1、C2,需要相互交流的组织就分入同一个通道,通道由配置CC1、CC2进行配置
  • 账本的副本:L1、L2每个通道一个账本,存储在对应节点上
  • 排序服务:O4,很多分布式解决一致性的方案就是收集所有节点各自的操作然后统一进行排序,这里也一样
  • 网络配置:NC4,图中只有一个最初的网络配置
  • 成员服务提供者MSP:Membership Service Provider,匹配CA与组织
  • 智能合约:S5、S6
  • 客户端应用程序:A1、A2、A3

一、初始化

现在有R1、R2、R3、R4想要搭建一个HyperLedger fabric网络N,其中R4确定为网络初始者有权利设置网络的初始版本

network.creation

如图所示,NC4是初始的网络配置,R4负责管理;网络初始时只提供最基础的排序服务O4;

二、添加网络管理员

现在给网络添加管理员R1,帮助R4一起管理网络

network.admins

如图所示,R4添加管理员R1,此时R1和R4都有管理NC4更新网络的权利

三、定义联盟

联盟:具有着共同命运的一个群组,一个联盟内的组织需要进行交流或是交易

一般将拥有共同目标的组织归为一个联盟

network.consortium

管理员可以定义联盟,如图,网络管理员(R1或是R4)将R1与R2归为一个联盟,这个配置存储在NC4中,如图中的X1

四、为联盟创建通道

联盟内通过通道进行交流;通道是一个主要的通信机制,通过它联盟的成员可以彼此通信。

network.channel

如图所示,NC4定义了联盟X1,现在为X1建立通道,R1和R2通过配置文件CC1管理和控制C1

注意排序服务O1也连接到了C1,之后连接到C1的节点都可以与O4通信

通道C1保证了R1与R2之间相互共享信息,但是对R3与R4保密

五、添加节点与账本

给网络中的C1通道添加节点P1,并且在P1上保存账本的副本L1

network.peersledger

节点是保存区块链账本副本的网络组件。账本L1物理在P1节点上,但是逻辑上来说他是C1的,因为通道与账本是一一对应的,而节点可以有很多个

CA1给P1颁发证书,标识其为组织R1的组件

六、安装与运行智能合约

network.appsmartcontract

智能合约定义了对账本的所有通用的访问模式(就是定义了一组方法),比如A1可以通过调用S5的方法来查询账本的内容

安装运行需要如下几步:

  1. 安装:管理员R1许可,将智能合约S5安装到P1节点上
  2. 实例化:在C1通道上实例化S5(因为此时连接到通道C1的组件(除了P1)并不知道S5的存在)实例化后,通道内的所有组件就都知道S5的存在
  3. 调用:此时客户端应用程序A1可以使用智能合约S5通过节点P1访问账本L1

注意以下几点:

  • 不需要在每个节点上都安装智能合约:当一个组织在一个通道中有多个 Peer 节点时,它可以选择哪个节点可以安装智能合约
  • 虽然同样都在C1通道上,但是智能合约S5的逻辑(即代码)只能被P1看到,而O4与A1都只能看到S5的输出与输出(实例化的是智能合约接口,而不是智能合约的实现)这也可以表明智能合约逻辑上在通道上,而物理上在节点上
  • 实例化需要附加信息:背书策略,只有在R1或R2背书的情况下,交易才能被接受并存储到账本L1上。
  • 调用:客户端应用程序A1通过向智能合约背书策略指定的组织R1、R2所属的节点发送交易提案来实现。
  • 智能合约:
    • 输入:交易提案
    • 输出:经过背书的交易响应

在调用完成后,交易响应与交易提案打包在一起,形成一个完整背书的交易,他们会被分发到整个网络

七、完善联盟网络结构

联盟X1中,R2还没添加到网络中,现在为R2添加节点P2,并且在P2中备份账本L1并且安装合约S5,但是不需要再实例化了实例化只需要发生一次(这也表示了合约逻辑上在通道上)

network.grow

现在A1和A2都可以使用节点P1或P2在C1上调用 S5,此时我们就完成了一个基本的网络结构,现在离我们最开始的图就差另外一个通道C2了,过程与此一致

network.structure

节点类型

上面的示例网络中有几种节点类型:

  • 记账节点:每个节点都是记账节点,维护账本

  • 背书节点:每一个带有智能合约的节点都可以作为一个背书节点(没有安装),负责对应用的请求进行背书

  • 排序节点:排序节点负责对网络中所有交易进行排序处理,且整理为区块结构

  • 证书节点:提供标准的PKI服务,负责对网络中的所有证书进行管理(签发+撤销)

  • 领导节点:如果一个组织在一个通道有多个节点,那么会有一个领导节点,负责将交易从排序节点分发到该组织中的其他记账节点

  • 锚节点:如果一个节点需要同另外一个组织的一个节点进行通信的话,那么它可以使用对方组织的通道配置中定义的锚节点

Hyperledeger Fabric实践

贴几个安装运行的文档:

环境安装

最好先换源:换为清华源

更改/etc/apt/sources.list的内容,并更新sudo apt update

前置需求:git、docker、docker-compose、(go、Java、Node)


关于fabric的下载,可以cvhttps://bit.ly/2ysbOFE的脚本在linux下运行:

  1. 用浏览器访问网址:https://bit.ly/2ysbOFE
  2. 将脚本复制粘贴到虚拟机或是服务器上,比如起名叫temp.sh
  3. 将脚本其中的一些内容修改,找到以下位置,修改BINARIES为false,卡的原因就是因为这里去下载了两个文件,十分麻烦,我们自己去下载这两个文件,然后用ftp工具传输到虚拟机或服务器即可
    1
    2
    3
    DOCKER=true
    SAMPLES=true
    BINARIES=false # 改了这里
  4. 修改完成后运行脚本
1
2
sudo chmod +x temp.sh
./temp.sh
  1. 注意这里的版本,要和你的脚本文件中的版本对应,我使用的是2.4.1和1.5.2
  1. 将下载完的软件传输到虚拟机或是服务器上,可以借助xshell等工具
  2. 将两个包解压到fabric-samples文件夹下,比如
1
2
tar -zxvf hyperledger-fabric-linux-amd64-2.4.1.tar.gz -C ./fabric-samples
tar -zxvf hyperledger-fabric-ca-linux-amd64-1.5.2.tar.gz -C ./fabric-samples

这样就可以了

运行测试网络

参考官方文档

进入目录查看命令帮助:

1
2
3
4
# 进入test-network文件目录
cd fabric-samples/test-network
# 查看帮助
./network.sh -h

第一步:直接运行,注意查看你是否有docker的权限,如果没有记得加sudo

1
2
3
4
5
6
7
# 删除先前运行的所有容器或工程
./network.sh down
# 启动网络(不会创建channel)
./network.sh up
# 这个命令会完成两步操作:
# 1、使用cryptogen工具或者Fabric CA来创建Org1、Org2、Orderer组织的身份证书
# 2、使用configtxgen工具来创建联盟,生成系统通道的创世区块genesis.block

(下面是执行流程,可跳过)


此时我们就创建了一个Fabric网络:有两个peer节点 + 一个排序节点orderer

如果你仔细看此时的输出内容:

1
2
3
4
5
6
7
# 测试网络使用 cryptogen帮我们创建认证证书
Generating certificates using cryptogen tool
# 创建 组织1的认证:使用./organizations/cryptogen目录下的配置文件生成证书
Creating Org1 Identities
+ cryptogen generate --config=./organizations/cryptogen/crypto-config-org1.yaml --output=organizations
org1.example.com
+ res=0

这个文件./organizations/cryptogen/crypto-config-org1.yaml的具体内容:

1
2
3
4
5
6
7
8
9
10
11
PeerOrgs:
# Org1
- Name: Org1
Domain: org1.example.com
EnableNodeOUs: true
Template: # 从零开始起名:peer%d,也可以自己定义名称
Count: 1
SANS:
- localhost
Users:
Count: 1 # 节点总数

组织2的相同,这里再贴一个排序节点的:

1
2
3
4
5
6
7
8
9
10
11
OrdererOrgs:
# Orderer
- Name: Orderer
Domain: example.com
EnableNodeOUs: true
# "Specs" spec的每一个条目由两部分组成hostname、commonname
# commonname默认是{{.Hostname}}.{{.Domain}},在这里就是orderer.example.com
Specs:
- Hostname: orderer
SANS:
- localhost

此时如果我们看test-network/organizations/这个目录,会发现多了2个目录,与我们的配置相同,并且目录下就产生了对应的CA证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
├── ordererOrganizations
│ └── example.com
│ ├── ca
│ ├── msp
│ ├── orderers
│ ├── tlsca
│ └── users
└── peerOrganizations
├── org1.example.com
│ ├── ca
│ ├── connection-org1.json
│ ├── connection-org1.yaml
│ ├── msp
│ ├── peers
│ ├── tlsca
│ └── users
└── org2.example.com
├── ca
├── connection-org2.json
├── connection-org2.yaml
├── msp
├── peers
├── tlsca
└── users

然后就是进行第二步,创建联盟以及创建初始块

1
2
3
4
5
6
# 生成CCP文件
Generating CCP files for Org1 and Org2
/home/hynis/hynis/fabric-samples/test-network/../bin/configtxgen
# 生成创世块
Generating Orderer Genesis block
+ configtxgen -profile TwoOrgsOrdererGenesis -channelID system-channel -outputBlock ./system-genesis-block/genesis.block

生成四个文件就是:connection-org1.jsonconnection-org1.yamlconnection-org2.jsonconnection-org2.yaml

connection-org1.json为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{
"name": "test-network-org1",
"version": "1.0.0",
"client": {
"organization": "Org1",
"connection": {
"timeout": {
"peer": {
"endorser": "300"
}
}
}
},
"organizations": {
"Org1": {
"mspid": "Org1MSP",
"peers": [
"peer0.org1.example.com"
],
"certificateAuthorities": [
"ca.org1.example.com"
]
}
},
"peers": {
"peer0.org1.example.com": {
"url": "grpcs://localhost:7051",
"tlsCACerts": {
"pem": "---省去公钥内容\n---\n"
},
"grpcOptions": {
"ssl-target-name-override": "peer0.org1.example.com",
"hostnameOverride": "peer0.org1.example.com"
}
}
},
"certificateAuthorities": {
"ca.org1.example.com": {
"url": "https://localhost:7054",
"caName": "ca-org1",
"tlsCACerts": {
"pem": ["---省去公钥内容\n---\n"]
},
"httpOptions": {
"verify": false
}
}
}
}

此时我们使用docker ps命令就可以看到三个镜像了:

启动了orderer.example.com、peer0.org1.example.com、peer0.org2.example.com三个镜像


(上面是执行流程,可跳过)

第二步:创建一个通道

1
2
# 创建通道
./network.sh createChannel [-c 指定名称 默认为mychannel]

第三步:然后我们在这个网络上启动一个链码(智能合约)

1
2
3
4
5
6
7
# -ccn 指定链码名称
# -ccp 指定文件路径
# -ccl 指定编码的语言:go, java, javascript, typescript
# 部署链码前安装jq
sudo apt install jq
# 部署Java链码
./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-java -ccl java

PS:部署Go链码可能会遇到的问题解决办法

但是直接运行上面的命令会报错,我们需要去对应的链码路径下安装其依赖

1
2
cd ../asset-transfer-basic/chaincode-go/
go mod vendor

运行链码出错,你可能需要改一下测试文件对应位置的go.mod内的go版本1.14为1.13


第四步:进入sudo bash执行以下操作

然后配置几个与网络交互的环境

1
2
3
4
# 1 ${PWD}表示pwd 当前路径位置
export PATH=${PWD}/../bin:$PATH
# 2
export FABRIC_CFG_PATH=$PWD/../config/

添加组织1:

1
2
3
4
5
6
7
8
export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
# ----分割线---
# 其中 CORE_PEER_TLS_ROOTCERT_FILE与CORE_PEER_MSPCONFIGPATH
# 指向组织1 organizations文件夹中的加密材料

初始化账本:(如果此步失败,请进入sudo bash重试

1
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n basic --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"InitLedger","Args":[]}'

查询账本内容:

1
peer chaincode query -C mychannel -n basic -c '{"Args":["GetAllAssets"]}'

输出:

1
2
3
4
5
[{"ID":"asset1","color":"blue","size":5,"owner":"Tomoko","appraisedValue":300},
{"ID":"asset2","color":"red","size":5,"owner":"Brad","appraisedValue":400},{"ID":"asset3","color":"green","size":10,"owner":"Jin Soo","appraisedValue":500},
{"ID":"asset4","color":"yellow","size":10,"owner":"Max","appraisedValue":600},
{"ID":"asset5","color":"black","size":15,"owner":"Adriana","appraisedValue":700},
{"ID":"asset6","color":"white","size":15,"owner":"Christopher","appraisedValue":800}]

调用 asset-transfer (basic) 链码改变账本上的资产所有者:

1
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n basic --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"TransferAsset","Args":["asset6","Christopher"]}'

因为asset-transfer (basic)链码的背书策略需要交易同时被Org1Org2签名,使用 --peerAddresses 标签来指向 peer0.org1.example.compeer0.org2.example.com,因为网络TLS也被开启,所以也需要指定TLS证书

然后我们使用另外一个身份:

1
2
3
4
5
6
7
# Environment variables for Org2

export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_LOCALMSPID="Org2MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=localhost:9051

查询:

1
peer chaincode query -C mychannel -n basic -c '{"Args":["ReadAsset","asset6"]}'

输出,发现"appraisedValue"比原来少了100

1
{"owner":"Christopher","color":"white","size":15,"appraisedValue":700,"assetID":"asset6"}

第五步:关闭网络

1
./network.sh down

链码如何编写

注意时刻参考以下两个文档:

核心注解

  • 类注解Contract注解(声明合约相关信息)、Default注解(标明这个合约是默认使用的合约)、DataType注解(表示该类可以作为返回或传递给事务函数的复杂类型之一)
  • 方法注解Transaction注解(该方法为可调用的事务函数)
  • 属性注解Property注解(可以指定属性的类型及规范,比如字符串可以要求匹配正则,数字要求在一定范围)

关于@Contract注解,下面是详细的描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 标注在合约上,可以使用name给一个别名
public @interface Contract {
Info info() default @Info; // 见下面的Info注解
String name() default ""; // 提供替代的名称,而不是用类名
String transactionSerializer() default "org.hyperledger.fabric.contract.execution.JSONTransactionSerializer";
}
// 标注合约的相关信息
public @interface Info {
String title() default "";
String description() default ""; // 描述
String version() default ""; // 版本号
String termsOfService() default ""; // 使用期限
License license() default @License; // 许可协议
Contact contact() default @Contact; // 联系人:可以描述email、name、url
}

下面是官方的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Contract(
name = "FabCar",
info = @Info(
title = "FabCar contract",
description = "The hyperlegendary car contract",
version = "0.0.1-SNAPSHOT",
license = @License(
name = "Apache 2.0 License",
url = "http://www.apache.org/licenses/LICENSE-2.0.html"),
contact = @Contact(
email = "f.carr@example.com",
name = "F Carr",
url = "https://hyperledger.example.com"))
)

核心接口

核心接口ContractInterface:所有的合约都应该实现这个接口

实现该接口的类中,标有Transaction注释的每个方法都被视为事务函数,事务函数可以被调用,每个事务函数的第一个参数是Context。其他参数由开发人员自行决定。

这个接口有三个比较重要的方法:

  • createContext():为context创建ChaincodeStub

  • beforeTransaction():事务调用前执行一次

  • afterTransaction():事务调用后执行一次

事务被调用的顺序是:createContext() -> beforeTransaction() -> the transaction function -> afterTransaction()

核心类

  • Context:在事务函数中使用,提供了上下文、使用世界状态的api
    • getStub():获取ChaincodeStub对象
    • getClientIdentity():获取ClientIdentity对象
  • ChaincodeStub:管理事务上下文,提供对状态变量的访问,并支持调用其他链码实现,这个类的方法很多,此处只列几个
    • putStringState(key, value):添加世界状态,不存在就添加,存在就更新
    • getStringState(key):根据key获取value
    • delState(key):删除key及对应的value
    • getStateByRange(startKey, endKey):范围get,参数是两个string
    • getQueryResult():富查询

链码如何安装在链上

整个过程为:1、启动网络。2、打包智能合约。3、安装链码。4、为链码提供定义,用于Fabric进行管理。5、将链码定义提交到通道。6、调用链码进行使用

Caliper安装与使用

Caliper:Fabric联盟链的性能测试工具

Caliper基本架构与概念解释

参考文档

Caliper架构图

可以看到,Caliper需要三个配置文件,然后通过不断的对被测系统SUT进行测试,最后生成测试报告:

  • SUT:特定被测系统(Caliper 是一种针对特定被测系统 (SUT) 生成工作负载并持续监控其响应的服务)
  • benchmark configuration:基准配置文件:告诉 Caliper 它应该执行多少轮,应该以什么速率提交 TX,以及哪个模块将生成 TX 内容
  • network configuration:网络配置文件:特定于 SUT 的。该文件通常描述 SUT 的拓扑结构、其节点所在的位置(它们的端点地址)、网络中存在的身份/客户端以及 Caliper 应部署或与之交互的智能合约
  • workload module:工作负载模块,基准测试的大脑。由于 Caliper 是一个通用基准框架,因此它不包含任何具体的基准实现(对于不同的链码实现不同的测试,是Nodejs文件)
  • benchmark artifacts:运行基准测试可能需要额外的工件,这些工件在不同的基准测试和运行之间可能会有所不同

安装

先启动测试网络,部署一个js的链码

1
2
3
cd test-network/
./network.sh up createChannel
./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-javascript -ccl javascript

需要npm环境、Node环境

1
2
3
4
5
sudo apt install npm
# n是node管理工具
sudo npm install n -g
# 安装node版本16.19.1,这是个包管理工具 类似于nvm
sudo n 16.19.1

fabric-samples/目录下创建文件夹caliper-workspace及对应文件,文件结构如下所示

1
2
3
4
5
6
7
./caliper-workspace/
├── benchmarks
│ └── myAssetBenchmark.yaml
├── networks
│ └── networkConfig.yaml
└── workload
└── readAsset.js

进入caliper-workspace目录:

1
2
3
4
5
cd ./caliper-workspace
# 安装caliper
npm install --only=prod @hyperledger/caliper-cli@0.4.2
# 绑定SDK
npx caliper bind --caliper-bind-sut fabric:2.2

网络配置文件

由五部分内容构成:

  • name:配置的名称
  • version:使用的配置文件的版本
  • caliper:向 Caliper 指示目标 SUT
  • channels: Fabric 通道和部署在这些通道上的智能合约
  • organizations:Fabric 组织的列表

填充networkConfig.yaml文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
name: Calier test
version: "2.0.0"
caliper:
blockchain: fabric
channels:
- channelName: mychannel
contracts:
- id: basic
organizations:
- mspid: Org1MSP
identities:
certificates:
- name: 'User1'
clientPrivateKey:
path: '../../fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore/priv_sk'
clientSignedCert:
path: '../../fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/signcerts/User1@org1.example.com-cert.pem'
connectionProfile:
path: '../../fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/connection-org1.yaml'
discover: true

文件中定义的第一个组织称为默认组织。在工作负载模块中,如果您未指定调用组织,则会使用默认组织。由于无论如何只定义了 1 个组织

而且只需要指定至少一个组织,无需提供有关作为测试网络一部分的 Org2 的详细信息。只需提供一个组织是一种非常常见的模式。

测试工作负载

工作负载模块在基准测试回合中与部署的智能合约进行交互,对于不同的链码,此处的逻辑也有所区别,对于示例所用的链码来说,就是此处的文件readAsset.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
'use strict';

const { WorkloadModuleBase } = require('@hyperledger/caliper-core');

class MyWorkload extends WorkloadModuleBase {
constructor() {
super();
}

async initializeWorkloadModule(workerIndex, totalWorkers, roundIndex, roundArguments, sutAdapter, sutContext) {
await super.initializeWorkloadModule(workerIndex, totalWorkers, roundIndex, roundArguments, sutAdapter, sutContext);

for (let i=0; i<this.roundArguments.assets; i++) {
const assetID = `${this.workerIndex}_${i}`;
console.log(`Worker ${this.workerIndex}: Creating asset ${assetID}`);
const request = {
contractId: this.roundArguments.contractId,
contractFunction: 'CreateAsset',
invokerIdentity: 'User1',
contractArguments: [assetID,'blue','20','penguin','500'],
readOnly: false
};

await this.sutAdapter.sendRequests(request);
}
}

async submitTransaction() {
const randomId = Math.floor(Math.random()*this.roundArguments.assets);
const myArgs = {
contractId: this.roundArguments.contractId,
contractFunction: 'ReadAsset',
invokerIdentity: 'User1',
contractArguments: [`${this.workerIndex}_${randomId}`],
readOnly: true
};

await this.sutAdapter.sendRequests(myArgs);
}

async cleanupWorkloadModule() {
for (let i=0; i<this.roundArguments.assets; i++) {
const assetID = `${this.workerIndex}_${i}`;
console.log(`Worker ${this.workerIndex}: Deleting asset ${assetID}`);
const request = {
contractId: this.roundArguments.contractId,
contractFunction: 'DeleteAsset',
invokerIdentity: 'User1',
contractArguments: [assetID],
readOnly: false
};

await this.sutAdapter.sendRequests(request);
}
}
}

function createWorkloadModule() {
return new MyWorkload();
}

module.exports.createWorkloadModule = createWorkloadModule;

基准配置文件

基准配置文件定义基准测试轮次并引用定义的工作负载模块。它将指定生成负载时要使用的测试工作人员的数量、测试轮次的数量、每轮的持续时间、每轮期间应用于事务负载的速率控制以及与监视器相关的选项。

myAssetBenchmark.yaml文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
test:
name: basic-contract-benchmark
description: test benchmark
workers:
number: 2
rounds:
- label: readAsset
description: Read asset benchmark
txDuration: 30
rateControl:
type: fixed-load
opts:
transactionLoad: 2
workload:
module: workload/readAsset.js
arguments:
assets: 10
contractId: basic

其中:比较关键的配置参数有,参考官方配置

属性 描述
test.rounds[i].txNumber 每一个round,caliper需要提交的事务数量
test.rounds[i].txDuration 每个round的执行时间(以s为单位)
test.rounds[i].rateControl 速率控制器一般使用两种:固定速率fixed-rate与固定负载fixed-load

例如这样的组合:txNumber + fixed-rate

1
2
3
4
5
6
txNumber : 500
rateControl:
type: fixed-rate
opts:
tps: 25
# 表示这一个round会以每秒25个的速度发送500个事务给系统

再如这样的组合:txDuration+ fixed-rate

1
2
3
4
5
6
txDuration: 60
rateControl:
type: fixed-rate
opts:
tps: 5
# 表示这一个round会以每秒5个的速度发送60s,也就是总共发送3000个事务

运行基准测试

caliper-workspace目录下,运行:

1
npx caliper launch manager --caliper-workspace ./ --caliper-networkconfig networks/networkConfig.yaml --caliper-benchconfig benchmarks/myAssetBenchmark.yaml --caliper-flow-only-test --caliper-fabric-gateway-enabled

生成的报告将详细说明每轮基准测试的以下项目:

  • 名称 - 来自基准配置文件的回合名称
  • Succ/Fail - 成功/失败交易的数量
  • 发送速率:caliper 发出交易的速率
  • 延迟(最大/最小/平均):与发出交易和接收响应之间所用时间(以秒为单位)相关的统计数据
  • 吞吐量:平均每秒处理的事务数

例如下表

Name Succ Fail Send Rate (TPS) Max Latency (s) Min Latency (s) Avg Latency (s) Throughput (TPS)
readAsset 2266 0 76.5 1.19 0.01 0.03 76.5