何为隐私数据?
如果某个通道上的某些组织想要保持数据的隐私,对通道上的其它组织保密,那么一个直接的做法是创建一个新的通道,只让具有隐私数据访问权的组织加入,然而建立单独的通道会产生额外的管理开销(维护链码版本,背书策略,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
}
]
集合配置文件中的集合定义在链码实例化/升级时生效:如果使用cli实例化/升级链码,需通过 --collections-config指定collection文件的路径;在SDK里面实现实例化/升级,请查阅对应的SDK文档。
为了方便测试,我们编写测试链码,使用我们collections_config.json文件中定义的两个隐私集合(collection)
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"
}
}, {}]
常见问题(持续更新中…)
链码API相关: 插入隐私数据对应shim包里面的方法是PutPrivateData,查询隐私数据用的GetPrivateData,注意和公共数据调用的interface是不同的,具体可参考shim包interface定义