本節分享開發一個區塊鏈票據系統,從鏈碼到前端的這個過程。
1. 票據知識梳理
2. 項目背景
3. 票據系統演示
4. 鏈碼講解
5. SDK代碼分析
6. 前端代碼梳理
票據交易的轉讓方式:
貼現是指持票人在需要資金時,將其持有的未到期的商業匯票,經過背書轉讓給銀行,並貼付利息,銀行從票面金額中扣除貼現利息後,將餘款支付給匯票持有人的票據行為。
本質是兌換權利的轉讓, 票據法規定,持票人將票據權利轉讓給他人,應當背書並交付票據。所以,當持票人為了轉讓票據權利,而在票據背面或者粘單上記載有關事項並簽章,就是在進行背書轉讓。
匯票質押是指以設定質權、提供債務擔保為目的而進行的背書。它是由背書人通過背書的方式,將票據轉移給質權人,以票據金額的給付作為對被背書人債務清償保證的一種方式。
4月28日,傢具銷售公司向光華沙發廠簽發了一張金額為50萬元、到期日為10月28日的匯票。光華沙發廠根據合同約定發送了沙發,接受了匯票。4月30日,光華沙發廠將匯票背書轉讓給某皮革廠,該皮革廠又將匯票於6月21日背書轉讓給畜牧場,以抵銷所欠貨款。在匯票到期日,畜牧場向傢具銷售公司提示付款,因傢具公司在開戶行存款不足而遭退票。畜牧場向皮革廠追索票款,皮革廠拒付。畜牧場遂以票據債務人即出票人傢具銷售公司、背書人光華沙發廠、皮革廠為被告向法院起訴,要求被告承擔連帶責任,清償票款。
原告:某畜牧廠
被告:B市某傢具銷售公司(以下簡稱傢具銷售公司)
被告:A市光華沙發廠(以下簡稱光華沙發廠)
被告:某皮革廠
以上就暴露了紙質票據的弊端,如果換做區塊鏈票據,畜牧場向皮革廠索要票款的時候,不管皮革廠同意不同意,區塊鏈系統上的智能合約會根據日期判斷,只要到期會強制把皮革廠的款項支付給畜牧場。
在這套系統中,共分四層。最底層是基於超級賬本的區塊鏈網路,在這層網路上部署智能合約,就是上節所講的鏈碼(電子合同,強制執行)。再往上就是傳統的部署架構,應用層就是俗稱的前端,業務層就是後端服務。用戶通過前端界面輸入內容並提交,通過restful協議與後端通信,後端再通過sdk與智能合約交互,最終把區塊鏈數據逐層返回給前端界面,表現為用戶看到提交後反饋的結果。
而節點的組織關係則如圖所示,共有兩個組織,每個組織裏有兩個peer節點,這四個peer節點都與同一個orderer節點通信,他們處在同一個通道內。
網路搭建可以參考之前發布的搭建聯盟鏈的文章,或者參考官方提供的fabric-sample裏的first-network案例,啟動網路。並分別創建通道,然後把兩個組織(org1和org2)加入通道,並且更新錨節點。
首先分析一下代碼結構組成:
package main import ( "encoding/json" "fmt" "time" "github.com/hyperledger/fabric/core/chaincode/shim" pb "github.com/hyperledger/fabric/protos/peer" )
新出票:NewPublish
等待背書:EndrWaitSign
已簽名背書:EndrSigned
背書拒絕:EndrReject
Bill_、holderName~billNo、holderId~dayTime-billType-billNo
票據:Bill
歷史背書:HistoryItem
鏈碼響應結構:chaincodeRet
以及最主要的記錄票據背書情況的結構:BillChaincode
根據票號取出票據:getBill
保存票據:putBill
鏈碼初始化:Init
票據發布:issue
背書請求:endorse
背書人接受背書:accept
背書人拒絕背書:reject
根據持票人編號批量查詢票據:queryMyBill
查詢等待背書的票據: queryMyWaitBill
根據票號查詢票據以及該票據背書歷史:queryByBillNo
鏈碼invoke介面:Invoke
主函數:main
雖然函數比較多,但是函數之間有很多相似點,因此限於篇幅,只分析三個主要的函數:Invoke、issue、queryMyBill
鏈碼在實例化的時候會調用main和init函數,這兩個函數僅僅初始化代碼並沒有實際對數據操作。而當調用函數的時候,比如客戶端輸入peer chaincode invoke...的時候就會調用invoke函數。
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) }
票據發布的主函數, 這個是記錄在區塊鏈數據裏,因此需要至少兩個背書節點。
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) }
根據持票人的編號,批量查詢票據
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) }
鏈碼完成之後,再參考之前配置網路的文章,分別安裝和實例化鏈碼。
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去寫。一些不涉及安全的業務邏輯也可以放在前端處理。這樣也減輕了服務端的壓力。
本例使用Angularjs框架編寫前端,關於Angular基礎知識可以自行查詢。
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-