超級賬本實踐——應用開發?

www.liankuai.tech

本節分享開發一個區塊鏈票據系統,從鏈碼到前端的這個過程。

前言

本節分享開發一個區塊鏈票據系統,從鏈碼到前端的這個過程。

目錄

1. 票據知識梳理

2. 項目背景

3. 票據系統演示

4. 鏈碼講解

5. SDK代碼分析

6. 前端代碼梳理

1. 票據知識

票據交易的轉讓方式:

1.1 貼現

貼現是指持票人在需要資金時,將其持有的未到期的商業匯票,經過背書轉讓給銀行,並貼付利息,銀行從票面金額中扣除貼現利息後,將餘款支付給匯票持有人的票據行為。

貼現既是一種票據轉讓行為,也是一種銀行授信行為,可以視作銀行通過接受匯票給持票人短期貸款,如果匯票付款期滿,銀行能收回匯票資金的,該貸款就自動沖銷,如果銀行不能得到匯票付款,則可以向匯票付款人和所有的債務人追索。

1.2 背書轉讓

本質是兌換權利的轉讓, 票據法規定,持票人將票據權利轉讓給他人,應當背書並交付票據。所以,當持票人為了轉讓票據權利,而在票據背面或者粘單上記載有關事項並簽章,就是在進行背書轉讓。

如下圖所示,把蓋章之後的粘單貼在票據背面的行為就叫背書轉讓。紙質的粘單有很多弊端,比如易丟失和偽造而且流轉環節繁瑣效率不高。所以把紙質的電子化,變成電子票據會有很多優勢,那麼電子票據如果部署在中心化系統上也可以被黑客侵入篡改甚至銷毀,而利用區塊鏈的分散式存儲和不可篡改特性,把電子票據存儲在這種系統上就大大降低了風險。本文就是基於這個痛點開發一套票據背書轉讓系統。

1.3 匯票質押

匯票質押是指以設定質權、提供債務擔保為目的而進行的背書。它是由背書人通過背書的方式,將票據轉移給質權人,以票據金額的給付作為對被背書人債務清償保證的一種方式。

2.票據背書轉讓案例

2.1 【案情介紹】

4月28日,傢具銷售公司向光華沙發廠簽發了一張金額為50萬元、到期日為10月28日的匯票。光華沙發廠根據合同約定發送了沙發,接受了匯票。4月30日,光華沙發廠將匯票背書轉讓給某皮革廠,該皮革廠又將匯票於6月21日背書轉讓給畜牧場,以抵銷所欠貨款。在匯票到期日,畜牧場向傢具銷售公司提示付款,因傢具公司在開戶行存款不足而遭退票。畜牧場向皮革廠追索票款,皮革廠拒付。畜牧場遂以票據債務人即出票人傢具銷售公司、背書人光華沙發廠、皮革廠為被告向法院起訴,要求被告承擔連帶責任,清償票款。

原告:某畜牧廠

被告:B市某傢具銷售公司(以下簡稱傢具銷售公司)

被告:A市光華沙發廠(以下簡稱光華沙發廠)

被告:某皮革廠

以上就暴露了紙質票據的弊端,如果換做區塊鏈票據,畜牧場向皮革廠索要票款的時候,不管皮革廠同意不同意,區塊鏈系統上的智能合約會根據日期判斷,只要到期會強制把皮革廠的款項支付給畜牧場。

3.系統架構設計

在這套系統中,共分四層。最底層是基於超級賬本的區塊鏈網路,在這層網路上部署智能合約,就是上節所講的鏈碼(電子合同,強制執行)。再往上就是傳統的部署架構,應用層就是俗稱的前端,業務層就是後端服務。用戶通過前端界面輸入內容並提交,通過restful協議與後端通信,後端再通過sdk與智能合約交互,最終把區塊鏈數據逐層返回給前端界面,表現為用戶看到提交後反饋的結果。

而節點的組織關係則如圖所示,共有兩個組織,每個組織裏有兩個peer節點,這四個peer節點都與同一個orderer節點通信,他們處在同一個通道內。

3.1 搭建網路

網路搭建可以參考之前發布的搭建聯盟鏈的文章,或者參考官方提供的fabric-sample裏的first-network案例,啟動網路。並分別創建通道,然後把兩個組織(org1和org2)加入通道,並且更新錨節點。

4.鏈碼分析

首先分析一下代碼結構組成:

其中shim是鏈碼開發所用到的主要模塊,和其它模塊一樣都是通過import關鍵詞導入。

4.1 模塊注入

package main
import (
"encoding/json"
"fmt"
"time"
"github.com/hyperledger/fabric/core/chaincode/shim"
pb "github.com/hyperledger/fabric/protos/peer"
)

4.2 狀態變數的定義

4.2.1 票據狀態分為四種:

新出票:NewPublish

等待背書:EndrWaitSign

已簽名背書:EndrSigned

背書拒絕:EndrReject

4.2.2 另外定義一組狀態資料庫中表的前綴,方便查詢:

Bill_、holderName~billNo、holderId~dayTime-billType-billNo

4.2.3 定義結構體

票據:Bill

歷史背書:HistoryItem

鏈碼響應結構:chaincodeRet

以及最主要的記錄票據背書情況的結構:BillChaincode

4.2.4 定義函數

根據票號取出票據:getBill

保存票據:putBill

鏈碼初始化:Init

票據發布:issue

背書請求:endorse

背書人接受背書:accept

背書人拒絕背書:reject

根據持票人編號批量查詢票據:queryMyBill

查詢等待背書的票據: queryMyWaitBill

根據票號查詢票據以及該票據背書歷史:queryByBillNo

鏈碼invoke介面:Invoke

主函數:main

雖然函數比較多,但是函數之間有很多相似點,因此限於篇幅,只分析三個主要的函數:Invoke、issue、queryMyBill

4.2.5 Invoke函數

鏈碼在實例化的時候會調用main和init函數,這兩個函數僅僅初始化代碼並沒有實際對數據操作。而當調用函數的時候,比如客戶端輸入peer chaincode invoke...的時候就會調用invoke函數。

這個函數通過function, args := stub.GetFunctionAndParameters()獲取應用層傳過來的參數。然後根據function的值來調用對用的函數。

switch function {
case "issue":
return a.issue(stub, args)
case "endorse":
return a.endorse(stub, args)
case "accept":
return a.accept(stub, args)
case "reject":
return a.reject(stub, args)
case "queryMyBill":
return a.queryMyBill(stub, args)
case "queryByBillNo":
return a.queryByBillNo(stub, args)
case "queryMyWaitBill":
return a.queryMyWaitBill(stub, args)
}

4.2.6 issue函數

票據發布的主函數, 這個是記錄在區塊鏈數據裏,因此需要至少兩個背書節點。

func (a *BillChaincode) issue(stub shim.ChaincodeStubInterface, args []string) pb.Response {
//判斷參數是否等於一,阻止多參數攻擊。
if len(args) != 1 {
res := getRetString(1, "LiankuaiChaincode Invoke issue 參數不等於一")
return shim.Error(res)
}
//定義Bill類型的變數,存儲票據信息
var bill Bill

//把傳過來的json數據反編列與bill合併
err := json.Unmarshal([]byte(args[0]), &bill)

//及時拋出異常中斷程序也為了方便調試
if err != nil {
res := getRetString(1, "LiankuaiChaincode Invoke issue 反編列異常")
return shim.Error(res)
}

// 查詢是否有重複的票據
_, existbl := a.getBill(stub, bill.BillInfoID)
//如果有相同的也拋出異常
if existbl {
res := getRetString(1, "LiankuaiChaincode Invoke issue failed : 票號已經存在 ")
return shim.Error(res)
}

//獲取當前時間戳
var dayTime = time.Now().Format("2018-10-10")

//根據混合key獲取狀態變數,返回的是一個迭代器,執行.Next()就可以逐條遍曆數據
resultIterator, err := stub.GetStateByPartialCompositeKey(HolderIdDayTimeBillTypeBillNoIndexName, []string{bill.HodrCmID, dayTime, bill.BillInfoType})
if err != nil {
res := getRetString(1, "LiankuaiChaincode Invoke issue 獲取列表錯誤")
return shim.Error(res)
}
defer resultIterator.Close()

var count = 0
for resultIterator.HasNext() {
_, _ = resultIterator.Next()

count++

if count >= 5 {
res := getRetString(1, "LiankuaiChaincode Invoke issue 每天不能出大於5個相同類型的票據")
return shim.Error(res)
}
}

//創建一個混合的key方便查詢
holderIdDayTimeBillNoIndexKey, err := stub.CreateCompositeKey(HolderIdDayTimeBillTypeBillNoIndexName, []string{bill.HodrCmID, dayTime, bill.BillInfoType, bill.BillInfoID})
if err != nil {
res := getRetString(1, "LiankuaiChaincode Invoke issue 更新查詢表錯誤")
return shim.Error(res)
}
//在更新狀態資料庫
stub.PutState(holderIdDayTimeBillNoIndexKey, []byte(time.Now().Format("2018-11-20 12:56:56")))

// 更改票據信息和狀態並保存票據:票據狀態設為新發布
bill.State = BillInfo_State_NewPublish
// 保存票據
_, bl := a.putBill(stub, bill)
if !bl {
res := getRetString(1, "LiankuaiChaincode Invoke issue 更新票據錯誤")
return shim.Error(res)
}
// 以持票人ID和票號構造複合key 向search表中保存 value為空即可 以便持票人批量查詢
holderNameBillNoIndexKey, err := stub.CreateCompositeKey(IndexName, []string{bill.HodrCmID, bill.BillInfoID})
if err != nil {
res := getRetString(1, "LiankuaiChaincode Invoke issue 更新索引表錯誤")
return shim.Error(res)
}
//更新狀態資料庫
stub.PutState(holderNameBillNoIndexKey, []byte{0x00})

res := getRetByte(0, "invoke issue success")
return shim.Success(res)
}

4.2.7 queryMyBill函數

根據持票人的編號,批量查詢票據

func (a *BillChaincode) queryMyBill(stub shim.ChaincodeStubInterface, args []string) pb.Response {
//判斷參數是否只有一個
if len(args) != 1 {
res := getRetString(1, "LiankuaiChaincode queryMyBill 參數不等於一")
return shim.Error(res)
}
// 以持票人ID從search表中批量查詢所持有的票號
billsIterator, err := stub.GetStateByPartialCompositeKey(IndexName, []string{args[0]})
if err != nil {
res := getRetString(1, "LiankuaiChaincode queryMyBill 獲取票據列表數據錯誤")
return shim.Error(res)
}
defer billsIterator.Close()

var billList = []Bill{}

for billsIterator.HasNext() {
kv, _ := billsIterator.Next()
// 取得持票人名下的票號
_, compositeKeyParts, err := stub.SplitCompositeKey(kv.Key)
if err != nil {
res := getRetString(1, "LiankuaiChaincode queryMyBill 分組獲取混合key錯誤")
return shim.Error(res)
}
// 根據票號取得票據
bill, bl := a.getBill(stub, compositeKeyParts[1])
if !bl {
res := getRetString(1, "LiankuaiChaincode queryMyBill 獲取票據錯誤")
return shim.Error(res)
}
billList = append(billList, bill)
}
// 取得並返回票據數組
b, err := json.Marshal(billList)
if err != nil {
res := getRetString(1, "LiankuaiChaincode Marshal queryMyBill 獲取票據列表錯誤")
return shim.Error(res)
}
return shim.Success(b)
}

鏈碼完成之後,再參考之前配置網路的文章,分別安裝和實例化鏈碼。

5. SDK代碼分析

Fabric為應用開發提供了多種語言的sdk,本案例使用的是基於nodejs的sdk,官方地址是https://github.com/hyperledger/fabric-sdk-node。開發中主要使用其中的兩大模塊:fabric-client和fabric-ca-client。

Fabric-client的使用:

var Fabric_Client = require(fabric-client); //載入sdk模塊var channel = fabric_client.newChannel(mychannel); //創建通道var peer = fabric_client.newPeer(grpc://localhost:7051); //連接peer節點
channel.addPeer(peer);var order = fabric_client.newOrderer(grpc://localhost:7050) //連接排序節點
channel.addOrderer(order);var request = {
chaincodeId: myChaincode, //調用哪個鏈碼
fcn: testFunc, //調用鏈碼裏的哪個函數
args: [arg1, arg2, arg3], //參數
chainId: mychannel, //哪個通道的鏈碼
txId: tx_id
};
channel.sendTransactionProposal(request); //發送交易
以上表示sdk創建通道並鏈接節點的過程,通道和發送數據都先發送一個提交議案調用channel.sendTransactionProposal函數。同時在終端可以檢測到是否發送成功。
Fabric-ca-client的使用:
var Fabric_CA_Client = require(fabric-ca-client);var tlsOptions = {
trustedRoots: [],
verify: false
};//調用加密套件var crypto_suite = Fabric_Client.newCryptoSuite();
//使用相同的地方存放狀態資料庫和用戶的證書以及用戶的私鑰var crypto_store = Fabric_Client.newCryptoKeyStore({path: store_path});
crypto_suite.setCryptoKeyStore(crypto_store);
fabric_client.setCryptoSuite(crypto_suite);//當CA啟用TLS的時候把http改成httpsvar fabric_ca_client = new Fabric_CA_Client(http://localhost:7054, tlsOptions , ca.example.com, crypto_suite);//登錄ca伺服器,拿到證書和私鑰
fabric_ca_client.enroll({
enrollmentID: admin,
enrollmentSecret: adminpw
})

以上這段表示的是ca服務的連接使用,主要為了登錄註冊用戶信息,返回證書和私鑰。sdk代碼是放置在後端,另外需要配合webserver,nodejs裏使用express作為webserver的基礎,在exress中設置好路由配置,監聽前端發送的請求,然後調用sdk裏的功能。類似java開發中的springMVC,如果是Java開發者可以使用對應的java版本的sdk放置在自己原有的系統中。

webserver代碼摘要(偽代碼):

var express = require(express);
...var server = http.createServer(app).listen(8080, function() {});//響應前端請求比如用戶註冊請求
app.post(/users, async function(req, res) {
var username = req.body.username;
var orgName = req.body.orgName;
if (!username) {
res.json(getErrorMessage(username));
return;
}
if (!orgName) {
res.json(getErrorMessage(orgName));
return;
}
//產生前端token
var token = jwt.sign({
exp: Math.floor(Date.now() / 1000) + parseInt(hfc.getConfigSetting(jwt_expiretime)),
username: username,
orgName: orgName
}, app.get(secret));
..........
//拿到CA管理員許可權去註冊新用戶
let adminUserObj = await client.setUserContext({username: admins[0].username, password: admins[0].secret});
let caClient = client.getCertificateAuthority();
//根據前端傳過來用戶名向ca節點註冊
let secret = await caClient.register({
enrollmentID: username,
affiliation: userOrg.toLowerCase() + .department1
}, adminUserObj);

})

//監聽調用鏈碼的前端請求(使用sdk提供的invokeChaincode函數):

app.post(/channels/:channelName/chaincodes/:chaincodeName/invoke, async function(req, res) {
logger.debug(==================== 調用鏈碼 ==================);
//獲取前端傳過來的參數
var peers = req.body.peers;
var chaincodeName = req.params.chaincodeName;
var channelName = req.params.channelName;
var fcn = req.body.fcn;
var args = req.body.args;
let message = await invoke.invokeChaincode(peers, channelName, chaincodeName, fcn, args, req.username, req.orgname);
res.send(message);
});

其它監聽地址比如查詢數據和發布票據的可以寫類似的監聽。並寫好測試用例,跑一遍用例如果沒有錯誤,就可以寫前端代碼了。前端可以使用Vue、React、Angularjs、當然也可以用jQuery甚至原生js去寫。一些不涉及安全的業務邏輯也可以放在前端處理。這樣也減輕了服務端的壓力。

6. 前端代碼梳理

本例使用Angularjs框架編寫前端,關於Angular基礎知識可以自行查詢。

如何發送數據到後端? 需要編寫前端的services,比如:

angular.module(app).factory(HttpService, [ $http,$q,REST_URL,$rootScope, DialogService,$state,toaster, function($http,$q,REST_URL,$rootScope,DialogService,$state,toaster){
return {
//封裝post請求
post : function(url, data) {
if (data != null) {
//前端的發送之前也有一個校驗,如果匹配invoke則需要對原始參數做一個編排
if(url.match(/invoke/)){
//添加兩個背書節點
data.peers = ["peer0.org1.example.com","peer0.org2.example.com"];
//附帶上token讓後端識別用戶身份
data.token = sessionStorage.getItem("token");
}else{
如果只是查詢則僅僅發送給一個節點即可
data.peer = "peer0.org1.example.com"
data.token = sessionStorage.getItem("token");
}
}
//構造請求數據
var req = {
method: POST,
url: url,
headers: {
Content-Type: application/json;charset=utf-8
},
data: data
};

var deferred = $q.defer();
var promise = deferred.promise;
//最終把數據發送給後端
$http(req).then(function(response) {
...

後端接收到前端請求的數據就會根據路由解析對應的參數並經過sdk與智能合約和各個背書節點交互。以上就是完整的開發流程。

小結

本文分享了開發超級賬本應用的基本思路:首先配置網路,然後創建通道、加入通道、更新錨節點。之後使用shim提供的功能編寫鏈碼,之後把鏈碼安裝到節點上並實例化。在後端代碼中引入fabric的sdk模塊,並監聽前端路由,調用sdk對應的函數完成登錄註冊以及與鏈碼調用的功能。

-END-


推薦閱讀:
相關文章