APP下载

Hyperledger Fabric区块链软件架构中的中间件设计∗

2020-11-02

计算机与数字工程 2020年9期
关键词:中间件调用背书

高 云 严 悍

(南京理工大学 南京 210095)

1 引言

Hyperledger Fabric 是由IBM 主导的面向企业客户的区块链开源项目[1]。Fabric区块链软件结构复杂,在应用项目开发时,使用了分布式系统、Docker 容器、Go 语言、Node.js 语言、Nosql 等多种计算机技术[2]。在软件设计阶段既需考虑前端的用户接入、非区块链信息处理等问题,又要保证区块链网络逻辑的正确性。同时,Fabric 区块链软件前后端的连接问题也需要复杂的编码来实现。

基于以上问题,本文对传统的Fabric 软件开发架构进行了优化,设计开发了Fabric 中间件作为连接前后端的介质。中间件实现了一般场景下区块链软件所需的多种通用功能,具有可复用性。在简化软件架构的同时避免了重复编程,降低了Fabric区块链软件应用的开发成本和难度。

2 包含中间件的Fabric 区块链软件架构

从广义上讲,中间件是在操作系统功能范围外为应用提供服务的多用途软件[3]。Fabric系统的中间件位于前端客户服务器与底层智能合约(chain⁃code)[4]之间,它接收前端服务器发送的请求,然后将请求进行处理后传递给链代码进行区块链操作。Fabric 中间件起到了重要的连接作用,中间件的加入,使前端开发可以更多地注重用户需求而无需过多考虑复杂的区块链问题,后台区块链底层开发者也只需保证链代码的稳定运行。有效减少了系统各层的交互难度,提高了软件开发的灵活性。Fabric 中间件在区块链系统中的位置如图1 所示。Fabric 中间件使用Node.js 语言进行开发,匹配Fabric 原生的Node SDK,主要调用“fabirc-cli⁃ent”和“fabric-ca-client”SDK 包,并进一步扩展和优化了官方SDK 的功能。主要实现的功能包括:Fabric 区块链中区块交易的查询、用户连接以及链代码函数的调用。

图1 Fabric中间件在区块链系统中的位置示意图

包含中间件的区块链软件开发架构分为四个层次,由上至下分别为客户端层、应用服务器层、中间件层、链代码层。

客户端层与应用服务器层的交互原理与非区块链的应用程序相同,并不涉及Fabric 的相关技术原理,需根据实际的生产应用场景运用相应的计算机技术进行开发[5]。为了适配中间件和Fabric 网络,建议服务器层编程使用Node.js 语言,对于区块链的Web 应用软件开发,使用基于Node 的Express框架进行编程[6]。应用服务器层将用户指令数据解析为相应中间件函数的参数,从而调用中间件函数连接区块链系统。中间件层(Node.js 语言编程)通过其中的query 和invoke 函数进行基本链码调用连接到链代码。链代码层依据Fabric 区块链交易原理编写各种链码函数(Go 语言编程),处理区块链上的区块、交易以及键值状态事务[7]。

3 区块交易查询中间件模块

Fabric 区块链网络的数据结构在总体上看是由一个个区块串连而成的链条[8]。信息存储的结构分为三层:每一个区块中存储了多笔交易,每一笔交易又保存了一次或多次键值状态(KVS)的变更操作。总体存储结构如图2所示。

图2 Fabric区块链信息存储结构示意图

3.1 区块信息查询

区块信息的查询借助了channel包中区块查询类的方法。相关中间件函数实现的功能包括创世区块详细信息查询和当前网络所有区块的遍历。

1)创世区块配置信息查询

Channel.query Block 方法提供了按指定编号查询区块信息的功能。该函数的语法为<async>que⁃ry Block(block Number,target,use Admin,skip De⁃code)。针对创世区块的查找,将“blockNumber”置为0,其他可选参数使用默认值,即query Block(0)。函数执行后返回一个Block对象,存储了创世区块的所有信息。查询创世区块配置主要是查询order 的配置信息,order 配置可以反映区块链网络主要配置信息。通过对Block 对象的结构分析,得出order配置信息的存储位置:

ordervalues=block.data.data [0]. payload.data.config.channel_group.groups.Orderer.values。

由ordervalues.max_message_count 可以查看每个区块的规定存储的最大交易数。

2)区块的遍历

对Fabric 网络中当前所有的区块进行遍历,首先需要确认网络中区块的数量。channel.query Info方法可以实现区块链网络整体状态信息的查看。该 函 数 语 法 为<async>query Info(target,useAd⁃min)。使target 等于当前peer,use Admin 等于true,执行该函数。返回值是一个Blockchain Info 对象,该对象存放了当前区块的一些信息,具有三个属性,包括height(当前区块高度)、current Block Hash(当前区块哈希值)、previous Block Hash(前驱区块哈希值)。经验得知,height 对象下的low 属性表示当前区块的序号,由于当前区块就是最新的区块,因此height.low也可表示网络中区块的数量。

获取到区块数量后,用for 语句循环调用chan⁃nel.query Block 函数即可实现区块信息的遍历。根据实际需要,总结出每个Block 对象需要显示输出的重要区块数据,包括:区块编号、前驱区块哈希值、该区块交易数量(可以统计出整个区块链网络的交易数量)、交易验证码(数组类型,长度为交易数量,其中的元素为0则表示交易有效,不为0表示交易无效,由此可统计全网的有效交易数量)。

3.2 交易信息查询

Fabric 区块链中几乎所有针对交易的查询都需要调用channel.query Transaction 方法,该方法的功能为返回指定交易id 对应的交易主体。该函数的返回值为一个Processed Transaction 对象,包含以下两个属性:validation Code(number类型)用编号表示交易的有效性;transaction Envelope(对象类型)存储了该交易的所有信息。

基于query Transaction方法,设计编写出通过交易id 查询交易信息的中间件函数showtx(tx_id),参数”tx_id”为待查询的交易id,功能是在Linux 终端显示出交易的详细信息。该函数为中间件交易查询功能的基本函数。

query Transaction 函数返回的Processed Transac⁃tion对象中,交易信息主要存放在其子对象transac⁃tion Envelope 内。该对象的payload 属性(payload 也是对象结构)存储了交易的头部数据(payload.head⁃er,类型为Header 对象)和交易体数据(payload.da⁃ta,类型为Transaction 对象)。Transaction 对象保存了该交易的主体部分,读写集是其中最主要的部分。读写集保存了该交易完成后键值的更新情况。其中读集包含键名和提交版本号信息,写集包含了键名与键值。因此,对区块链资产键值信息修改后,其值保存在该交易的写集中。代码清单1位获取读写集的核心Node.js代码。

代码清单1获取读写集如下。

var ns_rwset=

processed Transaction.transaction Envelope.

payload.data.actions[0].payload.action.

proposal_response_payload.extension.

results.ns_rwset;

ns_rwset.forEach((elem)=>{

var rwset=elem.rwset;

var rset=rwset.reads;

基于上述的基础交易查询功能以及交易实体内部存储结构的分析,可以在中间件中对交易的查询功能进行扩展。一方面,以交易版本为输入参数查询当前交易版本所属的交易;另一方面,通过区块遍历与用户实体证书,查询当前用户创建的所有交易。

1)根据交易版本查询交易信息

实现此功能的中间件函数showtx ByVer设计为两个形参,分别表示交易版本的两个属性。第1 个参数为区块编号(设为blocknum);第2个参数是为交易编号(设为txnum),交易编号为0,代表区块中第一个交易。由于配置交易的交易id为空值,该函数只能查询背书交易。函数实现原理:首先进行参数合法性检验,确保blocknum 为正值且在全网区块的范围之内,在此前提下调用queryBlock 函数定位到指定区块,再检验交易编号的合法性,保证待查询的交易编号不会溢出;第二步,根据Block对象的结构,获取交易编号为txnum 的交易的id;最后调用上述中间件函数showtx(),成功显示交易信息。

2)查询当前用户创建的所有交易。

设计中间件函数mytx()实现当前用户所有交易的查询。函数无参,功能为显示当前连接区块链网络的用户创建的交易信息。函数实现原理:根据User 对象的结构获取到用户证书ucert(存储位置:user。_identity。_certificate);然后依次遍历区块链网络中的每个区块,对于每个区块,再遍历所有的交易,根据Block 对象结构获取到交易中标记的用户证书信息bucert;如果ucert等与bucert,表明该交易的创建者是当前用户,则取得该交易的id 后,调用中间件函数showtx(),显示出该交易的信息。

4 用户连接中间件模块

该部分实现了区块链网络启动过程中,用户同时加入网络的功能。方法设计的准备阶段,根据Fabic 网络架构分析,总结出待配置的网络及用户参数。参数名称及其含义描述如表1所示。

用户必须在Fabric-CA 完成注册与登记,在key-value store 中保存了自身的用户语境和密钥后,才允许加入网络。中间件用户连接与网络启动功能实现流程分为三个阶段,分别为用户状态加密库的配置阶段、自定义参数赋值阶段、用户网络连接阶段。

1)用户状态加密库的配置阶段

通过用户语境证书创建并配置状态库(State S⁃tore)、加密键库(Crypto Key Store)以及用户加密套件(Crypto Suite)[9]。State Store用于持久保存用户状态信息,用户连接到网络后的证书密钥会存储到State Store中,从而避免了每次创建交易都重复传递状态信息;Crypto Key Store 在系统使用非默认key-value-store 时创建新的用户密钥库;Crypto Su⁃ite 包含了用于执行数字签名、加密、解密、安全散列等操作的一套密码学算法[8]。具体步骤:根据应用程序用户密钥库的路径配置hks;使用hks 作为参数调用client.new Default Key Value Store 函数创建KeyValue Store 的实例,返回值(赋值给变量kvs)用于配置State Store;使用hks 作为参数调用client.new Crypto Key Store 函 数 创 建Crypto Key Store 的 实例;使用kvs 作为参数调用client.set State Store,完成State Store 的创建和配置;调用client.new Crypto Suite函数,创建一个用户加密套件的实例,返回值(赋值给变量crysu)用于配置Crypto Key Store;调用crysu.set Crypto Key Store 函 数,完 成Crypto Key Store 的 配置;调用client.set Crypto Suite函数,完成用户加密套件的配置。该阶段各函数创建配置用户库的原理如图3所示。

表1 网络及用户参数配置信息

图3 各函数创建配置用户库原理图

2)自定义参数赋值阶段

需要配置的参数中部分需要根据用户实际需要进行自定义配置。该类型的参数主要用于设置组件的名称及地值,包括:userid,指定连接网络的用户名,如上所述,用户应已在CA 注册登记,并且密钥库中已产生证书和密钥;chaincode_id,指定安装到区块链网络的链码名称;channel_id,指定加入的通道名称;ca_id,指定Fabric CA 服务器的名称;ca_url,指定CA 服务器所在地址,支持http 协议,用于创建Fabric CA 实例;peer_url,指定与用户直接连接的peer 节点地址,使用grpc 协议,可以是一个地址,也可以是一个地址数组;order_url,指定用户连接的order节点的地址,使用grpc协议。

3)用户网络连接阶段

用户与区块链网络的连接,实际上是用户依次与peer 和order 的连接过程。首先进行peer 节点的连接,对于单个peer,调用client.new Peer 方法通过peer_url 获取到peer 对象实体,然后调用channel.add Peer方法,将完成连接的peer加入通道,对于连接多个peer 的用户,使用for 循环对每个peer 值进行地址获取和通道加入。order 节点的连接同理,调用client.new Order 方法从order_url 湖区到order对象,并调用channel.add Order 方法将order 加入通道。

根据以上三个阶段的实现原理,开发中间件的用户连接函数,已登记的用户可以借助中间件来进行Fabric网络的连接。

5 链码函数调用中间件模块

链码函数分为三类,即初始化类(init)、查询类(query)以及交易类(invoke)[10]。初始化类函数在Fabric 区块链网络启动时自动执行,因此中间件主要调用链码的查询与交易函数。实现query类链码函数基本调用功能的中间件函数命名为query;实现invoke 类链码函数基本调用功能的中间件函数命名为invoke。

5.1 查询类链码调用中间件函数query

query 函数所需的形参数量不固定,形参分为两部分:第一部分为一个字符串类型的参数,表示所需调用的链码函数名;第二部分为若干个字符串类型参数,表示被调用的链码函数的形参,实际使用时该部分的参数数量需要与链码函数参数的数量相等。函数语法为query(fcn,...args)函数返回其调用的查询类链码函数的相应查询结果。

中间件实现该功能所使用的核心Node SDK 函数是channel 包的queryBy Chaincode 函数。该函数提供了Node 程序与链代码查询类函数交互的接口[11]。返回值类型是一个字节数组的数组,查询结果保存在第一个字节数组成员中,因此需要先取出第一个数组成员(该成员是一个字节数组),然后对其进行类型转换操作才可得到相应结果。

中间件函数query 的实现流程:验证参数的合法性,确保fcn 不为空;指定peer 对象作为Chain⁃code Query Request对象targets属性的值;调用client.new Transaction ID()函数获取一个交易id(Transac⁃tion ID对象,并非字符串类型),作为transient Map属性的值;定义一个Chaincode Query Request对象类型的静态变量request,将上述属性传入变量中;以re⁃quest为参数调用queryBy Chaincode函数,将返回值赋值给query_res 变量;将query_res[0]转换为字符串类型并返回。

调用查询类链码函数在原理上包含中间件与peer 节点两次通信。第一次通信由中间件提交查询类调用请求到peer节点,peer节点根据对应链码函数进行背书与查询;第二次通信由peer节点将背书与查询结果返回给中间件。通信结构如图4 所示。

图4 调用查询类链码函数的组件通信结构图

5.2 交易类链码调用中间件函数invoke

中间件函数invoke 语法为invoke(fcn,...args),形参“fcn”表示需要调用的交易类链码函数名,“..args”表示若干个与链码函数参数相匹配的参数值。函数功能即调用相应链码函数实现区块链键值数据的修改并上链。

invoke 调用交易类链码函数进行交易时,必须符合Fabric 区块链网络交易规范[12]。除了中间件与peer 节点的通信外,还需要实现中间件与order节点的通信。实现流程可以总结为三个阶段,包括交易和网络合法性检验阶段、交易背书阶段、排序服务阶段。

1)交易和网络合法性检验阶段

除了检验实参合法性之外,由于交易需要同时连接peer 和order 节点,所以在验证阶段需要order对象已经加入网络。

2)交易背书阶段

根据Fabric 交易原理,在此阶段,用户通过客户端操作中间件提交创建交易,然后将提案发送给背书peer 节点[13]。背书peer 会对该提案进行签名等验证,并进行模拟交易和背书(根据链码的背书策略响应链码的背书请求)。最后peer背书后将结果返回到中间件。

发送提案到peer,peer 进行验证背书然后将结果返回的一系列操作,其核心方法为<async>send⁃Transaction Proposal(request,timeout)。形参“time⁃out”用来指定交易超时的等待时限,默认使用peer节点或世界状态设置的超时时限。“request”类型是一个Chaincode Invoke Request 对象,该对象定义了交易操作所需的各类参数,相当于Fabric 交易原理中由客户端创建并发送的提案对象。如果背书完成,peer 会把背书结果的消息返回给中间件,该消息就是send Transaction Proposal 的返回值。返回值是一个Proposal Response Object类型的对象,其中包含所有参与背书节点的背书结果以及交易最初的提案。该对象的属性结构如表2所示。

表2 roposal Response Object对象属性结构

通过实验发现,Proposal Response Object 对象还额外包含一个名为index:2 的属性,该属性的类型为Header对象,保存了交易的头文件信息。

获取到peer 的消息后,还需对消息进行验证,从而检查背书是否成功。主要验证内容为验证Proposal Response Object 对象的index:0 属性是否存在,且其中表示状态的子属性是否标记为成功(sta⁃tus==200 表示背书成功)。index:0 是一个数组,每个数组成员都代表一个背书peer的背书消息,只有所有peer的背书消息属性都存在且被标记为成功,背书才算成功。

invoke 函数该阶段的编码实现流程为:指定Peer 对象实体作为Chaincode Invoke Request 对象targets 属性的值;调用client.new Transaction ID()函数获取一个交易id对象,作为tx_id属性的值;定义一个Chaincode Invoke Request 对象类型的变量re⁃quest,将上述属性传入变量中;以request 为参数调用send Transaction Proposal函数,进行中间件与背书peer 节点的通信,将返回值赋值给results 变量;验证背书交易是否成功,循环遍历所有peer 背书消息,确保所有背书消息都为成功的交易,核心实现原理如代码清单2所示(Node.js语言)。

代码清单2 验证背书交易是否成功

var proposal Responses=results[0];

let endsuccess=true;

for(let ind in targets){

if(!(proposal Responses&&

proposal Responses[ind]。response&&

proposal Responses[ind]。response.status==200)){

end success=false;

break;

} }

if(end success){console.log(“背书成功”)

}else{ console.log(“背书失败”)

3)排序服务阶段

在Fabric 交易原理的排序阶段,客户端将由背书结果与起始提案组成的交易提案发送给order节点[14]。order 对多个交易进行排序服务打包成区块,将结果区块发布给committing peer 节点,这些peer按背书策略进行验证。验证成功后,该交易在区块上被标记为有效(valid)[15]。当一个区块中的所有交易都完成验证之后,无论交易是否有效,区块被广播给全网所有peer节点,由peer更新其托管的分类帐信息。

中间件开发过程中,实现上述流程的核心Node SDK 函 数<async>send Transaction(request,timeout)。函数形参“request”类型是一个Transac⁃tion Request 对象,该对象即是发送给order 的交易提案实体。函数返回一个包含http 响应代码的Promise,用于告知中间件排序服务是否成功,当该响应代码的值为200 时,表示排序服务成功,交易完成且成功上链(但不能判定交易的有效性)。

invoke 函数排序服务阶段的编码实现流程:声明对象类型变量request 作为send Transaction 函数的实参;获取背书阶段返回值的index:0属性,加入request 作为proposal Responses 属性;获取index:1,加入request作为proposal属性;以request为实参调用send Transaction 函数,完成排序服务;验证返回值的status是否为200,显示交易结果。

图5 调用交易类链码函数的组件通信结构图

调用交易类链码函数原理上依次包括了中间件与peer 的两次通信,以及中间件与order 节点一次通信。通信结构如图5所示。

6 结语

针对Fabric 区块链软件应用开发架构的复杂性问题,本文设计研究了Fabric 中间件结构。中间件中各个模块的函数分担了传统服务器的部分职能。其中的区块查询功能模块实现了初始区块信息获取、网络中所有区块遍历查询。交易查询模块实现了按多种属性查找指定交易的功能。用户连接模块允许用户通过简单的方式与区块链网络进行连接,从而执行各种区块链操作。链码函数调用模块实现了中间件与底层智能合约的连接,通过调用查询类与交易类链码函数,完成区块链中键值状态的获取与更新。

中间件的引入使区块链软件应用各层职能更加明确,前后端开发任务更加具体,有效降低了区块链软件的开发难度。同时,中间件具有高度的可复用性,一般生产场景下的区块链应用均可引入该结构,在开发中避免了重复编程,降低了Fabric 区块链应用的研发成本。

猜你喜欢

中间件调用背书
背书是写作的基本功
我国自主可控中间件发展研究
浅谈背书涂销制度
基于Android Broadcast的短信安全监听系统的设计和实现
以实力证明 用事实说话
背书连续性若干问题探析
利用RFC技术实现SAP系统接口通信
中移动集采:东方通中间件脱颖而出
金蝶 引领中间件2.0新时代
C++语言中函数参数传递方式剖析