Hyperledger Fabric啟用CouchDB為狀態數據庫
一.概述
- 數據請求流
超級賬本采用背書/共識模型,模擬執行和區塊驗證是在不同角色的節點中分開執行的。模擬執行是並發的,這樣可以提高擴展性和吞吐量:
- 背書節點:模擬執行鏈碼
- Peer節點:驗證交易並提交
2.超級賬本存儲元素
超級賬本包含以下元素:
- 賬本編號:快速查詢存在哪些賬本
- 賬本數據: 實際的區塊數據存儲
- 區塊索引: 快速查詢區塊/交易
- 狀態數據: 最新的世界狀態數據
- 歷史數據: 跟蹤鍵的歷史
每個Peer節點會維護四個DB,分別為:
- 賬本索引庫(IdStore):存儲ChainID
- 狀態數據庫(StateDB): 存儲world state
- 歷史數據庫(HistoryDB): 存儲Key的版本變化
- 區塊索引庫(BlockIndex):存儲Block索引
3.狀態數據庫
狀態數據庫可選類型包括LevelDB和CouchDB。LevelDB是嵌入在peer進程中的默認鍵/值狀態數據庫,CouchDB是一個可選的外部狀態數據庫。與LevelDB鍵/值存儲一樣,CouchDB可以存儲任何以chaincode建模的二進制數據(CouchDB附件函數在內部用於非json二進制數據)。但是,當chaincode值(例如,資產)被建模為JSON數據時,作為JSON文檔存儲,CouchDB支持對chaincode數據進行豐富的查詢。
LevelDB和CouchDB都支持核心chaincode操作,例如獲取和設置一個鍵(資產),並根據鍵進行查詢。鍵可以通過範圍查詢,可以對組合鍵進行建模,以支持針對多個參數的等價查詢。例如,作為所有者的組合鍵,資產id可以用於查詢某個實體擁有的所有資產。這些基於key的查詢可以用於針對賬本的只讀查詢,以及更新總賬的事務。
如果將資產建模為JSON並使用CouchDB,那麽就可以使用chaincode中的CouchDB JSON查詢語言對chaincode數據值執行復雜的富查詢,這些類型的查詢對於理解賬本上的內容很有幫助。對於這些類型的查詢,事務協議響應通常對客戶端應用程序有用,但通常不會作為事務提交到排序服務。事實上,也無法保證結果集在chaincode執行與富查詢提交時間之間的穩定性,因此使用富查詢的結果去執行最終的事務更新操作是不合適的,除非可以保證結果集在chaincode執行時間與提交時間之間的穩定性,或者可以處理在後續交易中的潛在變化。例如,如果對Alice所擁有的所有資產執行一個富查詢並將其傳輸給Bob,那麽一個新的資產可能會被另一個事務分配給Alice,這是在chaincode執行時間和提交時間之間的另一個事務,可能此過程中會錯過這個“虛值”。
CouchDB作為一個獨立的數據庫進程與peer一起運行,因此在設置、管理和操作方面有額外的考慮。我們可以考慮從默認的嵌入式LevelDB開始,如果需要額外的復雜的富查詢,可以轉移到CouchDB。將chaincode資產數據建模為JSON是一種很好的做法,這樣我們就可以在將來執行需要的復雜的富查詢。
二. 啟用CouchDB
本文均采用Hyperledger Fabric1.2中fabric-samples中相關組件與資源,在測試環境(fabric-samples/chaincode-docker-devmode)通過Docker啟動CouchDB服務
1.配置CouchDB啟動信息
參考:fabric-samples/first-network/docker-compose-couch.yaml
couchdb0:
container_name: couchdb0
image: hyperledger/fabric-couchdb
# Populate the COUCHDB_USER and COUCHDB_PASSWORD to set an admin user and password
# for CouchDB. This will prevent CouchDB from operating in an "Admin Party" mode.
environment:
- COUCHDB_USER=
- COUCHDB_PASSWORD=
# Comment/Uncomment the port mapping if you want to hide/expose the CouchDB service,
# for example map it to utilize Fauxton User Interface in dev environments.
ports:
- "5984:5984"
networks:
- byfn
修改:fabric-samples/chaincode-docker-devmode/docker-compose-simple.yaml 末尾添加並修改
couchdb:
container_name: couchdb
image: hyperledger/fabric-couchdb
# Populate the COUCHDB_USER and COUCHDB_PASSWORD to set an admin user and password
# for CouchDB. This will prevent CouchDB from operating in an "Admin Party" mode.
environment:
- COUCHDB_USER=
- COUCHDB_PASSWORD=
# Comment/Uncomment the port mapping if you want to hide/expose the CouchDB service,
# for example map it to utilize Fauxton User Interface in dev environments.
ports:
- "5984:5984"
2.配置CouchDB連接信息
參考fabric-samples/first-network/docker-compose-couch.yaml
peer0.org1.example.com:
environment:
- CORE_LEDGER_STATE_STATEDATABASE=CouchDB
- CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb0:5984
# The CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME and CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD
# provide the credentials for ledger to connect to CouchDB. The username and password must
# match the username and password set for the associated CouchDB.
- CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=
- CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=
depends_on:
- couchdb0
修改:fabric-samples/chaincode-docker-devmode/docker-compose-simple.yaml 中peer模塊
修改前
peer:
container_name: peer
image: hyperledger/fabric-peer
environment:
- CORE_PEER_ID=peer
- CORE_PEER_ADDRESS=peer:7051
- CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer:7051
- CORE_PEER_LOCALMSPID=DEFAULT
- CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
- CORE_LOGGING_LEVEL=DEBUG
- CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/msp
volumes:
- /var/run/:/host/var/run/
- ./msp:/etc/hyperledger/msp
working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer
command: peer node start --peer-chaincodedev=true -o orderer:7050
ports:
- 7051:7051
- 7053:7053
depends_on:
- orderer
修改後
peer:
container_name: peer
image: hyperledger/fabric-peer
environment:
- CORE_PEER_ID=peer
- CORE_PEER_ADDRESS=peer:7051
- CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer:7051
- CORE_PEER_LOCALMSPID=DEFAULT
- CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
- CORE_LOGGING_LEVEL=DEBUG
- CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/msp
- CORE_LEDGER_STATE_STATEDATABASE=CouchDB
- CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb:5984
- CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=
- CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=
volumes:
- /var/run/:/host/var/run/
- ./msp:/etc/hyperledger/msp
working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer
command: peer node start --peer-chaincodedev=true -o orderer:7050
ports:
- 7051:7051
- 7053:7053
depends_on:
- orderer
- couchdb
註意JSON文件的格式以及配置信息的一致性,如couchdb名稱等
3.啟動測試環境
# docker-compose -f docker-compose-simple.yaml up -d
# docker container ls
三.編寫鏈碼
1.代碼結構
代碼包:testdb
代碼文件
- domain.go //數據結構代碼
- main.go //業務測試代碼
2.數據結構
package main
type BillStruct struct {
ObjectType string `json:"DocType"` //對象類型定義
BillInfoID string `json:"BillInfoID"` //票據ID
BillInfoAmt string `json:"BillInfoAmt"` //票據金額
BillInfoType string `json:"BillInfoType"` //票據類型
BillIsseData string `json:"BillIsseData"` //出票日期
BillDueDate string `json:"BillDueDate"` //到期日期
HoldrAcct string `json:"HoldrAcct"` //持票人名稱
HoldrCmID string `json:"HoldrCmID"` //持票人ID
WaitEndroseAcct string `json:"WaitEndroseAcct"` //待背書人名稱
WaitEndorseCmID string `json:"WaitEndorseCmID"` //待背書人ID
}
3.測試代碼
請仔細閱讀註釋信息,此處不做代碼分割描述
package main
import (
"github.com/hyperledger/fabric/core/chaincode/shim"
"fmt"
"github.com/hyperledger/fabric/protos/peer"
"encoding/json"
"bytes"
)
//定義結構體CouchDBChaincode,作為shim.ChaincodeStubInterface實現類對象
type CouchDBChaincode struct {
}
//重寫shim.ChaincodeStubInterface接口的Init方法
func (t *CouchDBChaincode) Init(stub shim.ChaincodeStubInterface) peer.Response {
return shim.Success(nil)
}
//重寫shim.ChaincodeStubInterface接口的Invoke方法
func (t *CouchDBChaincode) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
//獲取用戶意圖與參數
fun, args := stub.GetFunctionAndParameters()
//根據用戶意圖判斷使用何種實現函數
if fun == "billInit" {
return billInit(stub)
} else if fun == "queryBills" {
return queryBills(stub, args)
} else if fun == "queryWaitBills" {
return queryWaitBills(stub, args)
}
//如果用戶意圖不符合如上,進行錯誤提示
return shim.Error("非法操作,指定的函數名無效")
}
//billInit函數:初始化票據數據
func billInit(stub shim.ChaincodeStubInterface) peer.Response {
/*
定義第一個票據:
持票人名稱:AAA
持票人ID:AID
待背書人名稱:無
待背書人ID:無
*/
billA := BillStruct{
ObjectType: "billObj",
BillInfoID: "POC001",
BillInfoAmt: "1000",
BillInfoType: "111",
BillIsseData: "20180501",
BillDueDate: "20180508",
HoldrAcct: "AAA",
HoldrCmID: "AID",
WaitEndroseAcct: "",
WaitEndorseCmID: "",
}
//通過json.Marshal方法對票據進行序列化操作
billAByte, _ := json.Marshal(billA)
//通過stub.PutState方法存儲序列化後的字節數組
err := stub.PutState(billA.BillInfoID, billAByte)
if err != nil {
return shim.Error("初始化第一個票據失敗:" + err.Error())
}
billB := BillStruct{
ObjectType: "billObj",
BillInfoID: "POC002",
BillInfoAmt: "1000",
BillInfoType: "111",
BillIsseData: "20180501",
BillDueDate: "20180508",
HoldrAcct: "AAA",
HoldrCmID: "AID",
WaitEndroseAcct: "BBB",
WaitEndorseCmID: "BID",
}
billBByte, _ := json.Marshal(billB)
err = stub.PutState(billB.BillInfoID, billBByte)
if err != nil {
return shim.Error("初始化第二個票據失敗:" + err.Error())
}
billC := BillStruct{
ObjectType: "billObj",
BillInfoID: "POC003",
BillInfoAmt: "1000",
BillInfoType: "111",
BillIsseData: "20180501",
BillDueDate: "20180508",
HoldrAcct: "BBB",
HoldrCmID: "BID",
WaitEndroseAcct: "CCC",
WaitEndorseCmID: "CID",
}
billCByte, _ := json.Marshal(billC)
err = stub.PutState(billC.BillInfoID, billCByte)
if err != nil {
return shim.Error("初始化第三個票據失敗:" + err.Error())
}
billD := BillStruct{
ObjectType: "billObj",
BillInfoID: "POC004",
BillInfoAmt: "1000",
BillInfoType: "111",
BillIsseData: "20180501",
BillDueDate: "20180508",
HoldrAcct: "CCC",
HoldrCmID: "CID",
WaitEndroseAcct: "BBB",
WaitEndorseCmID: "BID",
}
billDByte, _ := json.Marshal(billD)
err = stub.PutState(billD.BillInfoID, billDByte)
if err != nil {
return shim.Error("初始化第四個票據失敗:" + err.Error())
}
return shim.Success([]byte("所有票據初始化成功"))
}
//queryBills函數:批量查詢指定用戶的持票列表
func queryBills(stub shim.ChaincodeStubInterface, args []string) peer.Response {
//判斷是否有參數傳入
if len(args) != 1 {
return shim.Error("必須指定持票人的證件號碼")
}
//將第一個參數作為用戶ID
holdrCmID := args[0]
/*將CouchDB查詢字符串拼接成一個JSON串,格式如下:
{
"selector": {
"docType": "billObj",
"HoldrCmID": "%s"
}
}
*/
queryString := fmt.Sprintf("{\"selector\":{\"DocType\":\"billObj\",\"HoldrCmID\":\"%s\"}}", holdrCmID)
//通過自定義的getBillByQueryString函數進行數據查詢操作
result, err := getBillByQueryString(stub, queryString)
if err != nil {
return shim.Error("根據持票人的證件號碼批量查詢持票人持有票據列表時發生錯誤" + err.Error())
}
return shim.Success(result)
}
//queryWaitBills函數:批量查詢指定用戶的待背書票據列表
func queryWaitBills(stub shim.ChaincodeStubInterface, args []string) peer.Response {
if len(args) != 1 {
return shim.Error("必須指定待背書人的證件號碼")
}
waitEndorseCmID := args[0]
queryString := fmt.Sprintf("{\"selector\":{\"docType\":\"billObj\",\"WaitEndorseCmID\":\"%s\"}}", waitEndorseCmID)
result, err := getBillByQueryString(stub, queryString)
if err != nil {
return shim.Error("根據待背書人的證件號碼批量查詢待背書票據列表時發生錯誤" + err.Error())
}
return shim.Success(result)
}
//自定義函數:getBillByQueryString:根據指定的查詢字符串(CouchDB查詢語句)查詢數據
func getBillByQueryString(stub shim.ChaincodeStubInterface, queryString string) ([]byte, error) {
//通過stub.GetQueryResult方法獲取叠代器iterator
iterator, err := stub.GetQueryResult(queryString)
if err != nil {
return nil, err
}
//延遲關閉叠代器iterator
defer iterator.Close()
//定義字節緩沖變量
var buffer bytes.Buffer
//定義分割符
var isSplit bool
//對叠代器進行遍歷操作
for iterator.HasNext() {
//通過叠代器的Next()方法獲取下一個對象的Key與Value值(*queryresult.KV)
result, err := iterator.Next()
if err != nil {
return nil, err
}
if isSplit {
buffer.WriteString(";")
}
//定義格式
// key:result.key result.Value
buffer.WriteString("key:")
buffer.WriteString(result.Key)
buffer.WriteString(",value:")
buffer.WriteString(string(result.Value))
//獲取到第一個值後,將isSplit設置為true,用於跟第二個值進行分割
isSplit = true
}
//返回buffer對象的字節類型
return buffer.Bytes(), nil
}
func main() {
//啟動鏈碼CouchDBChaincode
err := shim.Start(new(CouchDBChaincode))
//如有報錯,提示報錯信息
if err != nil {
fmt.Errorf(err.Error())
}
}
四.安裝鏈碼
1.上傳鏈碼
上傳鏈碼包testdb至:fabric-samples/chaincode中
# ls /home/bruce/hyfa/fabric-samples/chaincode/testdb/
domain.go main.go
2.編譯鏈碼
# cd /home/bruce/hyfa/fabric-samples/chaincode/testdb/
# go build
# ls
domain.go main.go testdb
3.啟動鏈碼
進入chaincode容器進行操作
# docker container exec -it chaincode bash #進入chaincode容器進行操作
# cd testdb/
# CORE_PEER_ADDRESS=peer:7052 CORE_CHAINCODE_ID_NAME=testCouchDB:1.0 ./testdb
2018-08-05 10:33:37.063 UTC [shim] SetupChaincodeLogging -> INFO 001 Chaincode log level not provided; defaulting to: INFO
2018-08-05 10:33:37.063 UTC [shim] SetupChaincodeLogging -> INFO 002 Chaincode (build level: ) starting up ...
4.安裝與實例化鏈碼
進入cli容器進行操作
# docker container exec -it cli bash
# peer chaincode install -n testCouchDB -v 1.0 -p chaincodedev/chaincode/testdb
# peer chaincode instantiate -n testCouchDB -v 1.0 -C myc -c ‘{"Args":["init"]}‘
如有更新請用如下命令進行操作
# peer chaincode install -n testCouchDB -v 1.1 -p chaincodedev/chaincode/testdb
# peer chaincode upgrade -n testCouchDB -v 1.1 -C myc -c ‘{"Args":["init"]}‘
五.測試鏈碼
1.初始化票據
# peer chaincode invoke -n testCouchDB -C myc -c ‘{"Args":["billInit"]}‘
2.查詢指定用戶所持票據
# peer chaincode query -n testCouchDB -C myc -c ‘{"Args":["queryBills","AID"]}‘
key: POC001, value: {
"BillDueDate": "20180508",
"BillInfoAmt": "1000",
"BillInfoID": "POC001",
"BillInfoType": "111",
"BillIsseData": "20180501",
"HoldrAcct": "AAA",
"HoldrCmID": "AID",
"WaitEndorseCmID": "",
"WaitEndroseAcct": "",
"docType": "billObj"
};
key: POC002, value: {
"BillDueDate": "20180508",
"BillInfoAmt": "1000",
"BillInfoID": "POC002",
"BillInfoType": "111",
"BillIsseData": "20180501",
"HoldrAcct": "AAA",
"HoldrCmID": "AID",
"WaitEndorseCmID": "BID",
"WaitEndroseAcct": "BBB",
"docType": "billObj"
}
查詢結果可以看到我們定義的分隔符;
3.查詢指定用戶待背書票據
# peer chaincode query -n testCouchDB -C myc -c ‘{"Args":["queryWaitBills","BID"]}‘
key: POC002, value: {
"BillDueDate": "20180508",
"BillInfoAmt": "1000",
"BillInfoID": "POC002",
"BillInfoType": "111",
"BillIsseData": "20180501",
"HoldrAcct": "AAA",
"HoldrCmID": "AID",
"WaitEndorseCmID": "BID",
"WaitEndroseAcct": "BBB",
"docType": "billObj"
};
key: POC004, value: {
"BillDueDate": "20180508",
"BillInfoAmt": "1000",
"BillInfoID": "POC004",
"BillInfoType": "111",
"BillIsseData": "20180501",
"HoldrAcct": "CCC",
"HoldrCmID": "CID",
"WaitEndorseCmID": "BID",
"WaitEndroseAcct": "BBB",
"docType": "billObj"
}
另外關於LevelDB,CouchDB還是MongoDB,今後可能隨著Hyperledger Fabric的版本變化而采取不同的數據庫類型,我們拭目以待,現在唯一能做的,就是在已有的資源下面用Hyperledger Fabric為業務場景創造最大的業務價值。
Hyperledger Fabric啟用CouchDB為狀態數據庫