私有数据
私有数据的应用
在同一通道内某一组织持有私有数据,只要被认证的组织才可以访问。如果为了保持数据的隐私性而建立不同的通道,可能会增加通道以及相关链码的维护成本。
私有数据集
一个私有数据集由两个元素组成:
- 真实的私有数据
只有在被认证的组织中的peer节点才能查看通过gossip协议通讯的私有数据。这些数据被保存在peer节点的私有数据库(SideDB)中。共识节点中不包含私有数据。 - 数据的哈希值
数据的哈希值被确认、排序公示、以及记录在通道中每个peer节点账本中。这些哈希数据作为交易的证据,用于状态的检验,以及审计
案例
考虑一个由5个组织组成的联盟,在一个通道内进行贸易交易:
- 农场主
- 分销商
- 货运商
- 批发商
- 零售商
a. 分销商想与农场主以及货运商建立一条私有的机密的交易线路,交易数据不让批发商以及零售商知道(不暴露交易价格)
b. 分销商想与批发商建立一条私有的交易关系,应为他向他们收取的费用低于零售商
b. 批发商想与零售商以及货运商建立一条私有的交易关系
这样在5个组织之间存在3条各自隐私的私有数据集合(private data collections PDC)
- PDC1:分销商、农场主和货运商
- PDC2:分销商和批发商
- PDC3:批发商、零售商和货运商
[图片上传失败...(image-b10218-1532614924062)]
私有数据在fabric中的应用
私有数据集合定义配置文件
私有数据集合的定义,使用控制台命令 peer chaincode 链码部署时,使用配置项 --collections-config 指定配置文件,如果使用sdk部署链码请参考相关文档
集合定义配置文件由5个属性组成:
- name:集合名称
- policy:定义组织peer节点,允许使用签名策略语法来持久化收集数据,每个成员被包含在签名策略列表中。
- requiredPeerCount:在peer节点签名背书并返回提案响应给客户端,成功传送私有数据的最小认可节点。
- maxPeerCount:为了数据冗余保存的目的,需要同步保存私有数据的被认可的最大peer节点数量。如果一个可信节点宕机后,其他的持有私有冗余数据的节点依然可以提供数据请求访问服务。
- blockToLive:私有数据在sideDB保存时长。如果要一致保存,则配置为0.
样例如下:
[
{
"name": "collectionMarbles",
"policy": "OR('Org1MSP.member', 'Org2MSP.member')",
"requiredPeerCount": 0,
"maxPeerCount": 3,
"blockToLive":1000000
},
{
"name": "collectionMarblePrivateDetails",
"policy": "OR('Org1MSP.member')",
"requiredPeerCount": 0,
"maxPeerCount": 3,
"blockToLive":3
}
]
提交私有数据
授权peer节点之间私有数据的同步需要通过core.yaml配置文件中gossip模块下面的pvtData子模块的配置项
- pullRetryThreshold 限定将尝试从peer节点中提取对应于给定块的私有数据的最大持续时间,直到在没有私有数据的情况下提交块为止。
- 如果请求节点在pullRetryThreshold限定时间内检索完私有数据,节点将向账本提交交易(包含私有数据的哈希值),并在状态数据库保存私有数据,从逻辑上区分其他通道的状态数据
- 如果请求节点没有在pullRetryThreshold限定时间内检索完私有数据,节点将向他的区块链(*不明白)提交交易(包含私有数据的哈希值),不包括私有数据。
- 如果一个有权拥有私有数据的节点丢失了私有数据,他将没有能力为将来涉及到该私有数据的交易背书。
- transientstoreMaxBlockRetention 从当前账本高度开始计算至设定值以及设定值倍数的提交区块,删除临时保存的私有数据。
- pushAckTimeout 等待每个peer节点推送背书私有数据确认信息的最长时间。
- btlPullMargin 阻止实时拉取边界,用作缓冲区以防止peer节点尝试从即将在下N个块中清除的peer节点处获取私有数据。这有助于新加入的peer节点更快的同步当前区块链数据。
背书
背书节点在向其他被认可的节点散播私有数据中扮演重要的角色,他确保在通道中私有数据的有效性。为了协助散播,在集合定义中的参数maxPeerCount 和requiredPeerCount 控制了传播行为。
当背书节点不能成功的向小于requiredPeerCount配置的节点传播私有数据,他将向客户端返回一个错误。背书节点将尝试向其他组织的节点散播私有数据,以确保每个授权组织都有一份私有数据的拷贝。由于交易不是在链码执行时提交,因此背书节点和接收节点将私有数据的副本与区块链一起存储在本地临时存储中,直到交易被提交。
在链码中操作私有数据集合
链码的shim APIs接口提供常用私有数据操作方法:
- PutPrivateData(collection,key,value)
- GetPrivateData(collection,key)
- GetPrivateDataByRange(collection, startKey, endKey string)
- GetPrivateDataByPartialCompositeKey(collection, objectType string, keys []string)
- GetPrivateDataQueryResult(collection, query string) (*只有使用couchdb才能使用富查询语句)
在链码提案中传递私有数据
由于链码提案保存在区块链中,因此不要在链码提案的主要部分中包含私有数据也很重要。在链码提案中有一块特殊的区域叫做临时区域,被用于从客户端传递私有数据到peer节点调用链码。链码可以使用GetTransient()接口从临时区域获取数据。
注意事项
- 调用执行范围查询或者富查询链码的客户端应该知道,他们可能会受到结构集的子集,具体取决于私有数据的传播。客户端可以查询多个peer节点,并比较结果以确定peer节点是否可能缺少某些结果集。
- 不支持在单个交易中执行查询(范围查询或者富查询)并且更新数据,由于查询结果无法在无法访问私有数据或者丢失私有数据的节点进行验证。如果链码同时调用查询、更新私有数据,提案请求将会返回一个错误。如果你的应用程序可以容仍链码执行和验证/提交时间之间的结果集更改,那么你可以调用一个链码函数来执行查询,然后调用第二个链码函数来进行更新。注意,在同一交易中,调用GetPrivateData()检索单个键,并且调用PutPrivateData()更新数据,因为所有的peer节点可以一句散列键版本来验证键读取。
- 注意,私有数据集合仅仅定义了哪些组织的节点被授权接收以及存储私有数据,意味着哪些peer节点可以被用于查询私有数据,私有数据集合本身不限制链码中的访问控制。例如,如果非授权客户端能够在有权访问私有数据的peer节点调用链码,则链码在逻辑上仍然像往常一样强制执行访问控制,例如通过GetCreator()链码API或使用客户端身份链码库。
为私有数据集合建立索引
在使用couchdb作为状态数据库的时候,可以使用json格式的文件配置数据库的索引,并且索引文件需要放在指定目录下,比如:
索引文件路径:META-INF/statedb/couchdb/indexes
索引文件内容:
{"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}
私有数据清理
通过上面提到的配置项blockToLive,来执行私有数据的定期清理。
回想一下,在提交之前,peer节点私有数据保存在本地临时数据库。
升级集合定义
如果链码引用了集合,则链码将使用先前的集合定义,除非在升级时指定了新的集合定义。如果在升级期间指定了集合配置,则必须包含每个现有集合的定义,并且可以添加新的集合定义。
当peer节点提交包含链码升级的交易时,集合更新将生效,请注意,无法删除集合,因为在通道的区块链中可能存在无法删除的先前私有数据哈希
实际操作
1. 创建一个集合定义的json格式文件,配置项参数说明见上面部分。
// collections_config.json
[
{
"name": "collectionMarbles",
"policy": "OR('Org1MSP.member', 'Org2MSP.member')",
"requiredPeerCount": 0,
"maxPeerCount": 3,
"blockToLive":1000000
},
{
"name": "collectionMarblePrivateDetails",
"policy": "OR('Org1MSP.member')",
"requiredPeerCount": 0,
"maxPeerCount": 3,
"blockToLive":3
}
]
在链码初始化的时候使用命令加载该配置文件
peer chaincode instantiate ... --collections-config
2. 使用链码API对私有数据进行读写操作
在链码中定义私有数据
// Org1 与 Org2 组织中的peer节点可以从sidedb中访问私有数据
type marble struct {
ObjectType string `json:"docType"`
Name string `json:"name"`
Color string `json:"color"`
Size int `json:"size"`
Owner string `json:"owner"`
}
// Org1组织中的Peer节点可以从sidedb中访问私有数据
type marblePrivateDetails struct {
ObjectType string `json:"docType"`
Name string `json:"name"`
Price int `json:"price"`
}type marble struct {
ObjectType string `json:"docType"`
Name string `json:"name"`
Color string `json:"color"`
Size int `json:"size"`
Owner string `json:"owner"`
}
type marblePrivateDetails struct {
ObjectType string `json:"docType"`
Name string `json:"name"`
Price int `json:"price"`
}
定义的规则如下:
- "name,color,size,and owner" 可以被Org1和Org2组织下的所有成员访问
- "price" 仅仅被Org1组织下的成员访问
在弹珠游戏私人数据样例中定义了两组不同的私有数据。此数据到限制其访问的集合策略的映射由链码API控制。具体来说,使用集合定义读写私人数据是通过接口GetPrivateData()和PutPrivateData()来执行的,具体说明。
下图说明了弹珠游戏私有数据样本使用的私有数据模型。
[图片上传失败...(image-b56201-1532614924062)]
[图片上传失败...(image-944cf3-1532614924062)]
-
读集合数据
使用链码API GetPrivateData()从数据库中查询私有数据。GetPrivateData有两个参数,集合名称与数据键。回想一下集合collectionMarbles 允许Org1和Org2中的成员访问sideDB中的私有数据,集合collectionMarblePrivateDetails允许Org1中的成员访问sideDB中的私有数据。相关实现细节,请参阅一下两个弹珠游戏私有数据函数:- readMarble 查询"name, color, size, owner"属性的值
- readMarblePrivateDetails 查询"price"属性的值
-
写集合数据
使用链码API PutPrivateData() 往私有数据库中存储私有数据。- 使用集合名称collectionMarbles,写私有数据"name, color, size, owner"
- 使用集合名称collectionMarblePrivateDetails,写私有数据"price"
如下的样例中,在initMarble函数中,调用PutPrivateData() 两次,分别设置不同的私有数据。
// ==== Create marble object and marshal to JSON ====
objectType := "marble"
marble := &marble{objectType, marbleName, color, size, owner}
marbleJSONasBytes, err := json.Marshal(marble)
if err != nil {
return shim.Error(err.Error())
}
//Alternatively, build the marble json string manually if you don't want to use struct marshalling
//marbleJSONasString := `{"docType":"Marble", "name": "` + marbleName + `", "color": "` + color + `", "size": ` + strconv.Itoa(size) + `, "owner": "` + owner + `"}`
//marbleJSONasBytes := []byte(str)
// === Save marble to state ===
err = stub.PutPrivateData("collectionMarbles", marbleName, marbleJSONasBytes)
if err != nil {
return shim.Error(err.Error())
}
// ==== Save marble private details ====
objectType = "marblePrivateDetails"
marblePrivateDetails := &marblePrivateDetails{objectType, marbleName, price}
marblePrivateDetailsBytes, err := json.Marshal(marblePrivateDetails)
if err != nil {
return shim.Error(err.Error())
}
err = stub.PutPrivateData("collectionMarblePrivateDetails", marbleName, marblePrivateDetailsBytes)
if err != nil {
return shim.Error(err.Error())
}
作为额外的数据隐私优势,由于正在使用集合,因此z还有私有数据哈希值传递给orderer节点,而不是私有数据本身,从而使私有数据对orderer节点保密
3. 使用集合安装并初始化链码
创建一个solo共识的fabric网络(1个orderer,两个org组织,分别包含两个peer节点),包含以下节点:
- peer0.org1.example.com
- peer1.org1.example.com
- peer0.org2.example.com
- peer1.org2.example.com
- orderer.example.com
a. 安装链码
使用如下命令分别在4个peer节点安装弹珠游戏链码
peer chaincode install -n marblesp -v 1.0 -0 github.com/chaincode/marbles02_private/go/
安装成功后,peer节点日志会看到如下信息:
install -> INFO 003 Installed remotely response:<status:200 payload:"OK">
b. 初始化链码
使用管理员MSP在peer节点初始化链码
peer chaincode instantiate -o orderer.example.com:7050 -C mychannel -n marblesp -v 1.0 -c '{"Args":["init"]}' -P "OR('Org1MSP.member','Org2MSP.member')" --collections-config $GOPATH/src/github.com/chaincode/marbles02_private/collections_config.json
上面的参数根据实际情况进行改写修改
当链码初始化成功以后,可以看到如下日志信息:
[chaincodeCmd] checkChaincodeCmdParams -> INFO 001 Using default escc
[chaincodeCmd] checkChaincodeCmdParams -> INFO 002 Using default vscc
4. 存储私有数据
使用如下命令初始化一个弹珠:
peer chaincode invoke -o orderer.example.com:7050 -C mychannel -n marblesp -c '{"Args":["initMarble","marble1","blue","35","tom","99"]}'
初始化成功后,可以看到如下日志:
[chaincodeCmd] chaincodeInvokeOrQuery->INFO 001 Chaincode invoke successful. result: status:200
5. 在授权peer节点上查询私有数据
如下的链码例子,使用readMarble与readMarblereadMarblePrivateDetails方法分别查询collectionMarble与collectionMarblePrivateDetails集合中的私有数据:
// ===============================================
// readMarble - read a marble from chaincode state
// ===============================================
func (t *SimpleChaincode) readMarble(stub shim.ChaincodeStubInterface, args []string) pb.Response {
var name, jsonResp string
var err error
if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting name of the marble to query")
}
name = args[0]
valAsbytes, err := stub.GetPrivateData("collectionMarbles", name) //get the marble from chaincode state
if err != nil {
jsonResp = "{\"Error\":\"Failed to get state for " + name + "\"}"
return shim.Error(jsonResp)
} else if valAsbytes == nil {
jsonResp = "{\"Error\":\"Marble does not exist: " + name + "\"}"
return shim.Error(jsonResp)
}
return shim.Success(valAsbytes)
}
// ===============================================
// readMarblereadMarblePrivateDetails - read a marble private details from chaincode state
// ===============================================
func (t *SimpleChaincode) readMarblePrivateDetails(stub shim.ChaincodeStubInterface, args []string) pb.Response {
var name, jsonResp string
var err error
if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting name of the marble to query")
}
name = args[0]
valAsbytes, err := stub.GetPrivateData("collectionMarblePrivateDetails", name) //get the marble private details from chaincode state
if err != nil {
jsonResp = "{\"Error\":\"Failed to get private details for " + name + ": " + err.Error() + "\"}"
return shim.Error(jsonResp)
} else if valAsbytes == nil {
jsonResp = "{\"Error\":\"Marble private details does not exist: " + name + "\"}"
return shim.Error(jsonResp)
}
return shim.Success(valAsbytes)
}
在Org1的成员节点下查询marble1的私有数据"name,color,size,owner":
peer chaincode query -C mychannel -n marblesp -c '{"Args":["readMarble","marble1"]}'
查询结果如下:
{"color":"blue","docType":"marble","name":"marble1","owner":"tom","size":35}
在Org1的成员节点下查询marble1的私有数据"price":
peer chaincode query -C mychannel -n marblesp -c '{"Args":["readMarblePrivateDetails","marble1"]}'
查询结果如下:
{"docType":"marblePrivateDetails","name":"marble1","price":99}
6. 在未授权peer节点上查询私有数据
从上面定义的私有数据访问集合中,Org2定义了"name,color,size,owner"私有数据的访问权限,但是没有定义"price"私有数据的访问权限。
- 查询被授权的私有数据
peer chaincode query -C mychannel -n marblesp -c '{"Args":["readMarble","marble1"]}'
查询结果如下:
{"docType":"marble","name":"marble1","color":"blue","size":35,"owner":"tom"}
- 查询未被授权的私有数据
peer chaincode query -C mychannel -n marblesp -c '{"Args":["readMarblePrivateDetails","marble1"]}'
查询结果如下:
{"Error":"Failed to get private details for marble1: GET_STATE failed:
transaction ID: b04adebbf165ddc90b4ab897171e1daa7d360079ac18e65fa15d84ddfebfae90:
Private data matching public hash version is not available. Public hash
version = &version.Height{BlockNum:0x6, TxNum:0x0}, Private data version =
(*version.Height)(nil)"}"
7. 清理私有数据
在私有数据集合中配置的属性blockToLive,可以设置在多少个区块后清理私有数据,只留下数据的哈希值,作为交易的不可改变的证据。
比如我们在创建上面的样例链码的时候,设置私有数据集合属性blockToLive=4。
在上面的样例中,我们已经初始化了一个marble1,其中price=99,使用如下命令查询:
peer chaincode query -C mychannel -n marblesp -c '{"Args":["readMarblePrivateDetails","marble1"]}'
查询结果如下:
peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c '{"Args":["transferMarble","marble2","tom"]}'
然后再创建一个新的marble2资产:
peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c '{"Args":["initMarble","marble2","blue","35","tom","99"]}'
此时区块增加1。
此时查询marble1资产中的price值:
peer chaincode query -C mychannel -n marblesp -c '{"Args":["readMarblePrivateDetails","marble1"]}'
查询结果如下:
{"docType":"marblePrivateDetails","name":"marble1","price":99}
在使用如下命令分别把marble2资产转移给其他角色:
peer chaincode invoke -o orderer.example.com:7050 -C mychannel -n marblesp -c '{"Args":["transferMarble","marble2","tom"]}'
在把资产转移3次以后,区块再次增加3次。此时再查询marble1资产中的price值:
peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c '{"Args":["transferMarble","marble2","tom"]}'
查询结果如下:
Error: endorsement failure during query. response: status:500
message:"{\"Error\":\"Marble private details does not exist: marble1\"}"
表明私有数据已经被清除。
8. 使用带有索引的私有数据
索引也可用于私有数据集合,通过打包与链码同级目录下META-INF/statedb/couchdb/collections/<collection_name>/indexes的索引文件。
为了将链码部署到生产环境,建议在链码同级目录下定义索引,以便链码和相关所以作为一个单元一起自动部署,一旦链码被安装在节点并被初始化到通道。当指定--collections-config 标志指向集合json文件的位置时,关联索引将在通道上的链码实例化时自动部署。