高 云 嚴 悍
(南京理工大學 南京 210095)
Hyperledger Fabric 是由IBM 主導的面向企業客戶的區塊鏈開源項目[1]。Fabric區塊鏈軟件結構復雜,在應用項目開發時,使用了分布式系統、Docker 容器、Go 語言、Node.js 語言、Nosql 等多種計算機技術[2]。在軟件設計階段既需考慮前端的用戶接入、非區塊鏈信息處理等問題,又要保證區塊鏈網絡邏輯的正確性。同時,Fabric 區塊鏈軟件前后端的連接問題也需要復雜的編碼來實現。
基于以上問題,本文對傳統的Fabric 軟件開發架構進行了優化,設計開發了Fabric 中間件作為連接前后端的介質。中間件實現了一般場景下區塊鏈軟件所需的多種通用功能,具有可復用性。在簡化軟件架構的同時避免了重復編程,降低了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]。
Fabric 區塊鏈網絡的數據結構在總體上看是由一個個區塊串連而成的鏈條[8]。信息存儲的結構分為三層:每一個區塊中存儲了多筆交易,每一筆交易又保存了一次或多次鍵值狀態(KVS)的變更操作。總體存儲結構如圖2所示。

圖2 Fabric區塊鏈信息存儲結構示意圖
區塊信息的查詢借助了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表示交易無效,由此可統計全網的有效交易數量)。
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(),顯示出該交易的信息。
該部分實現了區塊鏈網絡啟動過程中,用戶同時加入網絡的功能。方法設計的準備階段,根據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網絡的連接。
鏈碼函數分為三類,即初始化類(init)、查詢類(query)以及交易類(invoke)[10]。初始化類函數在Fabric 區塊鏈網絡啟動時自動執行,因此中間件主要調用鏈碼的查詢與交易函數。實現query類鏈碼函數基本調用功能的中間件函數命名為query;實現invoke 類鏈碼函數基本調用功能的中間件函數命名為invoke。
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 調用查詢類鏈碼函數的組件通信結構圖
中間件函數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所示。
針對Fabric 區塊鏈軟件應用開發架構的復雜性問題,本文設計研究了Fabric 中間件結構。中間件中各個模塊的函數分擔了傳統服務器的部分職能。其中的區塊查詢功能模塊實現了初始區塊信息獲取、網絡中所有區塊遍歷查詢。交易查詢模塊實現了按多種屬性查找指定交易的功能。用戶連接模塊允許用戶通過簡單的方式與區塊鏈網絡進行連接,從而執行各種區塊鏈操作。鏈碼函數調用模塊實現了中間件與底層智能合約的連接,通過調用查詢類與交易類鏈碼函數,完成區塊鏈中鍵值狀態的獲取與更新。
中間件的引入使區塊鏈軟件應用各層職能更加明確,前后端開發任務更加具體,有效降低了區塊鏈軟件的開發難度。同時,中間件具有高度的可復用性,一般生產場景下的區塊鏈應用均可引入該結構,在開發中避免了重復編程,降低了Fabric 區塊鏈應用的研發成本。