何爲隱私數據?
如果某個通道上的某些組織想要保持數據的隱私,對通道上的其它組織保密,那麼一個直接的做法是創建一個新的通道,只讓具有隱私數據訪問權的組織加入,然而建立單獨的通道會產生額外的管理開銷(維護鏈碼版本,背書策略,MSP等。
從Fabric v1.2開始,Fabric可以創建私有數據集合,從而使通道上已定義的組織子集能夠背書、提交或查詢私有數據,而無需創建單獨的通道。
此篇文章主要針對fabric1.4.x版本,2.0版本參考:Fabric Hyperledger(2.0 )之隱私數據(Private data)
隱私數據集合(private data collection)
包括以下兩個方面:
- 私有數據本身:通過gossip協議在有權訪問的組織節點之間傳輸(peer-to-peer),此數據存儲在被授予私有數據訪問權限的私有狀態數據庫(有時稱之爲邊數據庫SideDB),可以通過授權peer的鏈碼進行訪問。過程中排序節點不參與,並且不會看到數據。由於使用gossip協議分發數據,因此需要更新每個組織的錨節點,並且需要在對應的peer節點配置CORE_PEER_GOSSIP_EXTERNALENDPOINT以實現跨組織通信
- 數據哈希:通過背書、排序寫入通道中每個節點的賬本中,hash作爲交易的證據用於狀態驗證和審計
下圖描述了被授予私有數據訪問權的節點和未被授予權限的節點:
關於資產轉讓
假設A組織擁有私有數據的權限,想轉讓給沒有權限的組織B,那麼組織B可以通過驗證私有數據的hash來判斷數據的真實性。
什麼時候使用私有數據vs單獨的通道
使用單獨的通道:如果整個交易(賬本)必須在屬於該通道成員的一組組織內保持機密。
使用私有數據:當必須在一組組織之間共享賬本時,但是當這些組織的子集應該可以訪問某些(或全部)數據時。 此外,由於私有數據是通過點對點而不是通過塊進行分發的,當必須對排序節點交易數據保密時,使用私有數據集合。
隱私數據初體驗
測試環境:Fabric 1.4.4
對於隱私數據而言,可通過json文件定義一個或多個集合(name屬性),每個集合通過定義具體的策略(policy)來控制背書時隱私數據的分發,背書節點會嘗試分發隱私數據給不同組織的節點,以保證每個組織擁有一份隱私數據的副本,由於交易並非智能合約調用時提交,所以背書節點和被分發隱私數據的節點會將隱私數據臨時存儲到本地,直到交易提交成功,才存儲到鏈上;當隱私數據授權節點在交易提交時本地臨時存儲沒有隱私數據副本(要麼非背書節點,要麼背書節點未分發其隱私數據),在配置的時間(節點配置文件中可通過peer.gossip.pvtData.pullRetryThreshold配置)內拉取其它授權節點的隱私數據。在較爲合理的設計中,隱私數據分發策略應比背書策略要求更高,例如隱私數據分發策略需要五個組織認可,而背書策略僅需要三個組織認可。
requiredPeerCount:背書節點必須成功分發隱私數據給peer的最小數量,當小於這個值時,交易失敗。確保了當背書節點不可用,私有數據也可以正常使用。requiredPeerCout設置成0,當背書節點不可用,隱私數據會丟失。
maxPeerCount:背書節點分發私有數據給peer的最大數量。如果設置成0,則所有具有隱私數據權限的節點,會在交易commit時,從授權節點拉取數據。
blockToLive:以塊爲單位,指定隱私數據再side數據庫的存活時間,隱私數據在指定的塊上保留,此後將被清除,從而在鏈上刪除隱私數據,無法通過鏈碼查詢,也無法在節點上得到隱私數據,如果想無限期地保留隱私數據,可將blockToLive屬性設置成0。
memberOnlyRead:值爲true表示peer自動強制僅允許屬於集合成員組織之一的客戶端讀取對私有數據。
編寫collections_config.json文件,聲明兩個集合collectionMedium、collectionPrivate。
[{
"name": "collectionMedium",
"policy": "OR('Org1MSP.member', 'Org2MSP.member','Org3MSP.member')",
"requiredPeerCount": 0,
"maxPeerCount": 3,
"blockToLive": 1000000,
"memberOnlyRead": true
},
{
"name": "collectionPrivate",
"policy": "OR('Org1MSP.member')",
"requiredPeerCount": 0,
"maxPeerCount": 3,
"blockToLive": 5,
"memberOnlyRead": true
}
]
collection文件在鏈碼實例化/升級時部署在通道,如果使用cli在peer節點實例化/升級鏈碼,則通過–collections-config指定collection文件的路徑,在SDK裏面調用,請查閱對應的SDK文檔。
爲了方便測試,我們編寫測試鏈碼文件
package main
import (
"encoding/json"
"fmt"
"strconv"
"github.com/hyperledger/fabric/core/chaincode/shim"
pb "github.com/hyperledger/fabric/protos/peer"
)
type product struct {
Id string `json:"Id"`
Name string `json:"Name"`
Color string `json:"Color"`
Length string `json:"Length"`
Width string `json:"Width"`
}
type productPrice struct {
Id string `json:"Id"`
BuyPrice float64 `json:"BuyPrice"`
SellPrice float64 `json:"SellPrice"`
}
type MediumChaincode struct { // define to implement CC interface
}
func main() {
err := shim.Start(new(MediumChaincode))
if err != nil {
fmt.Printf("Error starting the Medium Contract: %s", err)
}
}
// Implement Barebone Init
func (t *MediumChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {
fmt.Println("Successfully init chaincode")
return shim.Success(nil)
}
// Implement Invoke
func (t *MediumChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
fmt.Println("Start Invoke")
defer fmt.Println("Stop Invoke")
// Get function name and args
function, args := stub.GetFunctionAndParameters()
switch function {
case "createProduct":
return t.createProduct(stub, args)
case "getProduct":
return t.getProduct(stub, args)
case "getProductPrice":
return t.getProductPrice(stub, args)
default:
return shim.Error("Invalid invoke function name.")
}
}
func (t *MediumChaincode) createProduct(stub shim.ChaincodeStubInterface, args []string) pb.Response {
id := args[0]
name := args[1]
color := args[2]
length := args[3]
width := args[4]
buyPrice, err1 := strconv.ParseFloat(args[5], 32)
sellPrice, err2 := strconv.ParseFloat(args[6], 32)
if err1 != nil || err2 != nil {
return shim.Error("Error parsing the values")
}
product := &product{id, name, color, length, width}
productBytes, err3 := json.Marshal(product)
if err3 != nil {
return shim.Error(err1.Error())
}
productPrice := &productPrice{id, buyPrice, sellPrice}
productPriceBytes, err4 := json.Marshal(productPrice)
if err4 != nil {
return shim.Error(err2.Error())
}
err5 := stub.PutPrivateData("collectionMedium", id, productBytes)
if err5 != nil {
return shim.Error(err5.Error())
}
err6 := stub.PutPrivateData("collectionPrivate", id, productPriceBytes)
if err6 != nil {
return shim.Error(err6.Error())
}
jsonProduct, err7 := json.Marshal(product)
if err7 != nil {
return shim.Error(err7.Error())
}
return shim.Success(jsonProduct)
}
func (t *MediumChaincode) getProduct(stub shim.ChaincodeStubInterface, args []string) pb.Response {
id := args[0]
product := product{}
productBytes, err1 := stub.GetPrivateData("collectionMedium", id)
if err1 != nil {
return shim.Error(err1.Error())
}
err2 := json.Unmarshal(productBytes, &product)
if err2 != nil {
fmt.Println("Error unmarshalling object with id: " + id)
return shim.Error(err2.Error())
}
jsonProduct, err3 := json.Marshal(product)
if err3 != nil {
return shim.Error(err3.Error())
}
return shim.Success(jsonProduct)
}
func (t *MediumChaincode) getProductPrice(stub shim.ChaincodeStubInterface, args []string) pb.Response {
id := args[0]
productPrice := productPrice{}
productPriceBytes, err1 := stub.GetPrivateData("collectionPrivate", id)
if err1 != nil {
return shim.Error(err1.Error())
}
err2 := json.Unmarshal(productPriceBytes, &productPrice)
if err2 != nil {
fmt.Println("Error unmarshalling object with id: " + id)
return shim.Error(err2.Error())
}
jsonProductPrice, err3 := json.Marshal(productPrice)
if err3 != nil {
return shim.Error(err3.Error())
}
return shim.Success(jsonProductPrice)
}
實例化鏈碼:
root@013939cef3cd:/opt/gopath/src/github.com/hyperledger/fabric/peer# peer chaincode instantiate -o orderer0.example.com:7050 --tls true --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer0.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C testchannel -n new -l golang -v 1.0 -c '{"Args":["Init"]}' -P 'OR("Org1MSP.peer","Org2MSP.peer","Org3MSP.peer")' --collections-config=/opt/gopath/src/github.com/chaincode/new/collections_config.json
2019-12-17 08:20:59.392 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 001 Using default escc
2019-12-17 08:20:59.392 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 002 Using default vscc
其中/opt/gopath/src/github.com/chaincode/test/collections_config.json爲配置文件所在目錄(docker容器掛載後的路徑)
添加一條隱私數據
root@013939cef3cd:/opt/gopath/src/github.com/hyperledger/fabric/peer# peer chaincode invoke -o orderer0.example.com:7050 --tls true --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer0.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C testchannel -n new -c '{"Args":["createProduct","1","log","brown","1m","2m", "100", "200"]}'
在Org1MSP節點執行查詢
2019-12-17 08:22:04.163 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200 payload:"{\"Id\":\"1\",\"Name\":\"log\",\"Color\":\"brown\",\"Length\":\"1m\",\"Width\":\"2m\"}"
root@013939cef3cd:/opt/gopath/src/github.com/hyperledger/fabric/peer# peer chaincode query -o orderer0.example.com:7050 --tls true --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer0.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C testchannel -n new -c '{"Args":["getProductPrice","1"]}'
{"Id":"1","BuyPrice":100,"SellPrice":200}
root@013939cef3cd:/opt/gopath/src/github.com/hyperledger/fabric/peer# peer chaincode query -o orderer0.example.com:7050 --tls true --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer0.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C testchannel -n test -c '{"Args":["getProduct","1"]}'
{"Id":"1","Name":"log","Color":"brown","Length":"1m","Width":"2m"}
由於Org1MSP成員在collectionMedium及collectionPrivate集合中,因此能夠查詢到相應的隱私數據,而Org3MSP成員執行查詢,則只能查看collectionMedium中的數據
root@admin1:~/go/src/github.com/hyperledger/C39/channel-artifacts# docker exec -it cli bash
root@1746d372d8b9:/opt/gopath/src/github.com/hyperledger/fabric/peer# mv ./channel-artifacts/new.1.0.out /opt/gopath/src/github.com/hyperledger/fabric/peer
root@1746d372d8b9:/opt/gopath/src/github.com/hyperledger/fabric/peer# peer chaincode install new.1.0.out
2019-12-17 08:19:28.069 UTC [chaincodeCmd] install -> INFO 001 Installed remotely response:<status:200 payload:"OK" >
root@1746d372d8b9:/opt/gopath/src/github.com/hyperledger/fabric/peer# peer chaincode query -o orderer0.example.com:7050 --tls true --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer0.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C testchannel -n new -c '{"Args":["getProductPrice","1"]}'
Error: endorsement failure during query. response: status:500 message:"GET_STATE failed: transaction ID: 8ff57728e6046b621d3dee7caba577f55ea27c1fbcbf01ab8abc2aff0fa59053: tx creator does not have read access permission on privatedata in chaincodeName:new collectionName: collectionPrivate"
root@1746d372d8b9:/opt/gopath/src/github.com/hyperledger/fabric/peer# peer chaincode query -o orderer0.example.com:7050 --tls true --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer0.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C testchannel -n new -c '{"Args":["getProduct","1"]}'
{"Id":"1","Name":"log","Color":"brown","Length":"1m","Width":"2m"}
當查看collectionPrivate集合對應的隱私數據,報錯:
Error: endorsement failure during query. response: status:500 message:“GET_STATE failed: transaction ID: 8ff57728e6046b621d3dee7caba577f55ea27c1fbcbf01ab8abc2aff0fa59053: tx creator does not have read access permission on privatedata in chaincodeName:new collectionName: collectionPrivate”
由此隱私數據,測試完畢.
隱私數據再體驗
前文介紹了隱私數據的理論,並編寫測試鏈碼進行了測試,基本能夠滿足應用層面的需求。在研究隱私數據過程中,一直有一個疑問,在執行交易伴隨隱私數據時,交易裏面會隱含什麼信息?基於此,解析了區塊信息,和正常交易一樣,塊裏面會包含塊號、數據哈希,前一區塊哈希,交易參數,交易背書信息、讀寫集。不同之處在於隱私數據不會出現在讀寫集中,下面是我們鏈碼調用(createProduct方法)時的讀寫集信息(信息已做處理)。
[{
"readSet": {
"namespace": "lscc",
"version": {
"blockNum_": 15,
"txNum_": 0,
"memoizedIsInitialized": -1,
"unknownFields": {
"fields": {},
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": 0
},
"key": "new~collection"
}
}, {}]