前言
从 Hyperledger Fabric v1.2 开始引入了 Private Data,对于该特性一直处于空白阶段,现把落下的安排啦,并将知识点整理成文,欢迎交流讨论
本文中的实验环境为 Hyperledger Fabric v1.4
背景
在 v1.2 之前,一个通道内账本数据对所有组织成员是公开的,当对手交易方不想让交易数据对其它成员可见,则必须为对手交易方创建一个新的通道,这会产生额外的管理开销,例如:大量的通道配置,链码管理,背书策略,成员服务管理等。另外,即使为每类对手交易创建不同的通道仍无法解决敏感数据的保密,因此 Hyperledger Fabric v1.2 开始引入了 private data 特性,解决通道内数据保密性问题。
术语
private data
private data
(私有数据)实际场景中多指商业机密数据(客户,金额,折扣等)和用户隐私数据(账户信息,交易信息,个人身份信息,财产信息等)。私有数据不会存储在排序节点和分类账本中,仅保存在被授权 peer 的 私有数据库(private state database 也叫 SideDB )或 瞬态存储库(transient store),并且私有数据只能通过 gossip 协议分发至被授权的 peer,因此每个 peer 需要配置CORE_PEER_GOSSIP_EXTERNALENDPOINT
保证跨组织通讯。
private data hash
private data hash
(私有数据哈希)由背书节点生成,经过排序服务写入每个账本数据中。Hash 用于状态验证及私有交易的证明(可用于审计),是零知积证明(zero-knowledge proof)技术应用。
如果私有交易成员间发生分歧又或者他们想出让资产给第三方,则他们可以决定与第三方共享数据,然后第三方可以计算私有数据的哈希,并查看它是否与账上的状态匹配,从而证明在某个时间点上交易成员之间存在状态。
即使私有数据的哈希将公开存储在通道中,也无法将哈希反转为原始内容
private data collection
private data collection
(私有数据集)由 private data
和 private data hash
两个元素组成,用于控制哪些组织或用户有权访问私有数据。
私有数据集存储
上图是 peer 开启私有数据的示意图,由两部分组织,第一部分为公共数据就是我们平常指的的区块链和世界状态,第二部分就是私有数据,它包含以下三块:
Private Writeset Storage
(私有写集库)保存每一个私有数据集的所有交易的历史,每个 peer 中可以配置有多个Private Writeset Storage
实例。注意,这种存储不是区块链,而是一种典型的日志持久化数据库。Private State Database
(私有数据库)类似于公共部分的世界状态,保存私有集合的当前状态。与私有写集库一样可以配置多个私有数据库实例。Transient Store
(瞬态存储库)私有数据暂存库,属于过程态的临时库。
上图显示了同一个通道内来自不同组织的三个 peer,存储了#1
和 #2
两个私有数据集实例。#1
对三个 peer 可见,#2
仅对 Org1-peer 和 Org2-peer 开放。
交易流程
- 客户端应用程序提交一个调用链码函数(读或写私有数据)的提案请求,私有数据被发送到提案的
transient
字段中 - 背书节点仿真执行交易,并把私有数据存储至瞬态存储库,然后根据私有数据集策略,私有数据通过 gossip 协议分发至被授权的记账节点,这些记账节点同样将私有数据存储到瞬态存储库
- 只有当传完指定数量的记账节点后,背书节点将结果进行签名,返回给客户端的数据不包含私有数据,只会返回签名和私有数据 key 和 value 的哈希值
- 客户端将提案的响应发送给排序节点,排序节点无法看到私有数据的内容,只能看到一串哈希值
- 排序节点出块将交易区块广播给记账节点
- 对于公共数据记账节点会验证背书策略,验证成功会新增区块,更新世界状态,对于私有数据首先判断是否有为私有数据授权节点,如果是授权节点,检查本地瞬态存储库是否收到私有数据,如果没有收到则尝试从其他授权节点拉取,然后根据公共区块中的哈希验证私有数据,以上过程验证成功后,记账节点就会将瞬态存储库中的数据正式存储到私有数据库,最后删除瞬态存储库
- 记账节点存储完成后会通知客户端记账的情况
私有数据集配置
私有数据集只有在链码实例化或升级时才能配置,可以使用命令行 peer chaincode instantiate --collections-config
引用集合配置文件即可;如果是客户端SDK,请查看SDK文档。
私有数据集配置包含一个或多个集合定义,每个集合由以下参数构成:
name
: 集合名字policy
: 私有数据策略,定义了哪些组织可以存储私有数据,使用OR
/AND
签名语法列表。因为peer
得有私有数据才能给提案背书,所以私有数据策略得比背书策略更宽泛requiredPeerCount
: 背书peer
返回执行结果前必须分发私有数据给提交记账peer
的节点最小数量,如果值为0
,则不分发,不建议设0
,因为没有冗余数据,一旦peer
不可用,可能造成私有数据丢失maxPeerCount
: 最大要分发的节点数量,如果值为0
,则在背书阶段不会分发私有数据,强制在提交记账阶段拉取私有数据blockToLive
: 私有数据的保存期限(以区块为单位),超过这个期限私有数据会自动销毁。如果值为0
,则永不销毁memberOnlyRead
: 仅集合成员访问权限,布尔值。不允许非集合成员访问设值true
,如果需要颗粒度更细的权限控制设值为false
,并在链码函数中定义集合成员权限。
接下来写个配置样例,就以私有数据集存储图二所示,同一个通道内来自不同组织的三个 peer
1 | [ |
前文说过私有数据集只有在链码实例化或升级时才能配置,所以如果需要更新私有数据集配置,只能升级链码版本,使用命令行 peer chaincode upgrade --collections-config
,升级时原有的集合配置必须包含在内
- 私有数据升级时
name
和blockToLive
值必须保持一致 - 集合更新生效后,集合不能被删除,因为区块链上可能有先前私有数据的哈希,而区块链数据无法删除
Peer 间私有数据的同步
有关私有数据 Peer 配置项可查看 core.yaml
文件的 peer.gossip.pvtData
部分,以下我们将其截图出来解析
1 | pvtData: |
为私有数据集合建立索引
在使用CouchDB作为状态数据库时,可以使用 json
格式的文件配置数据库的索引,索引文件放到META-INF/statedb/couchdb/indexes
目录下。同样的私有数据也可以使用索引,方法如下:
- 文件路径
META-INF/statedb/couchdb/collections/<collection_name>/indexes
- 索引文件
1
{"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}
链码引用私有数据
任何一个链码都可以引用多个私有数据集合。链码的 shim APIs 接口可用设置和检索私有数据,以下提供常用方法:
PutPrivateData(collection string, key string, value []byte) error
GetPrivateData(collection, key string) ([]byte, error)
GetPrivateDataByRange(collection, startKey, endKey string) (StateQueryIteratorInterface, error)
GetPrivateDataByPartialCompositeKey(collection, objectType string, keys []string) (StateQueryIteratorInterface, error)
GetPrivateDataQueryResult(collection, query string) (StateQueryIteratorInterface, error)
//只有使用couchdb才能使用富查询语句GetTransient() (map[string][]byte, error)
有关 shim
包文档也可以至Golang文档了解(需翻墙)
- SDK 调用 chaincode 执行范围查询或富查询时,如果查询的 peer 缺少私有数据,可能会返回结果集的一个子集,有些 Peer 可能不包含私有数据,SDK 可以查询多个 Peer 并比较结果,以确定是否丢失了私有数据。
- 不支持同一个事务中执行范围查询或富查询时,执行数据更新,因为无法判断 peer 是否有权限拥有私密数据权限,或是否丢失私密数据。如果在同一事务中同时包含查询和更新私密数据操作,提案将返回
Error
。建议分为两个交易执行。但是一个 chaincode 方法中既包含GetPrivateData()
和PutPrivateData()
是可以的,因为所有 peer 都可以基于key
的哈希校验key
私有数据内容保护
如果私有数据相对简单且可预测(例如交易金额),未授权的组织成员可能暴力哈希作用域内的私数据内容,以期匹配区块链上私有数据哈希。因此,可预计的私有数据内容可以混入随机数,比如安全的伪随机源,然后再调用链码时将私有数据传递到 transient
字段中。
实战
纸上得来终觉浅 绝知此事要躬行,正好最近在学习 Go,撸一遍加深印象吧!
- 开发环境:
go version go1.14 windows/amd64
- 参考教程: fabric-samples
开撸之前咱得先设计一下应用场景,考虑一下实现功能。我简单想了个供应商场景
- 供应商数据属于机密数据
- 数据分两类,供应商基本资料和报价
- 成员之间访问权限不同
有了场景,私有数据配置就出来了
1 | [ |
接下来就是撸代码了,尽量把 chaincode
中引用私有数据的常用方法试一下,代码就不放了,有兴趣可以去我的 github 参考
供应商链码使用
利用 fabric-samples 样例快速拉套环境 ./byfn.sh up
,把项目代码挂载到 chaincode
目录下
- 安装链码,记得安装 Org2MSP 链码时得切换环境变量
1
peer chaincode install -n vendor -v 1.0 -p github.com/github.com/hoke58/fabric-chaincode/vendorPDC/
- 实例化
1
peer chaincode instantiate -o orderer.example.com:7050 --tls --cafile $ORDERER_CA -C mychannel -n vendor -v 1.0 -c '{"Args":["init"]}' -P "OR('Org1MSP.member','Org2MSP.member')" --collections-config $GOPATH/src/github.com/github.com/hoke58/fabric-chaincode/vendorPDC/collections_config.json
- 创建供应商
1
2export VENDOR=$(echo -n "{\"Name\":\"test0\",\"Project\":\"supplychain\",\"Status\":\"yes\",\"Expiry\":\"2020-05-01\",\"Price\":6666}" | base64 | tr -d \\n)
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 vendor -c '{"Args":["putVendor"]}' --transient "{\"vendor\":\"$VENDOR\"}" - 查询供应商基本信息
1
peer chaincode query -C mychannel -n vendor -c '{"Args":["getVendor","test0"]}'
- 查核供应商报价,Org2MSP 是看不到报价的
1
peer chaincode query -C mychannel -n vendor -c '{"Args":["getVendorPrice","test0"]}'
- 再创建几个供应商,试一下范围查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 供应商test1
export VENDOR=$(echo -n "{\"Name\":\"test1\",\"Project\":\"supplychain\",\"Status\":\"yes\",\"Expiry\":\"2020-05-01\",\"Price\":7777}" | base64 | tr -d \\n)
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 vendor -c '{"Args":["putVendor"]}' --transient "{\"vendor\":\"$VENDOR\"}"
# 供应商test2
export VENDOR=$(echo -n "{\"Name\":\"test2\",\"Project\":\"supplychain\",\"Status\":\"yes\",\"Expiry\":\"2020-05-01\",\"Price\":8888}" | base64 | tr -d \\n)
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 vendor -c '{"Args":["putVendor"]}' --transient "{\"vendor\":\"$VENDOR\"}"
#供应商test3
export VENDOR=$(echo -n "{\"Name\":\"test3\",\"Project\":\"supplychain\",\"Status\":\"no\",\"Expiry\":\"2020-05-01\",\"Price\":9999}" | base64 | tr -d \\n)
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 vendor -c '{"Args":["putVendor"]}' --transient "{\"vendor\":\"$VENDOR\"}"
# 范围查询
peer chaincode query -C mychannel -n vendor -c '{"Args":["getVendorByRange","test0","test3"]}'
参考
- https://hyperledger-fabric.readthedocs.io/en/release-1.4/private-data/private-data.html
- https://hyperledger-fabric.readthedocs.io/en/release-1.4/private-data-arch.html
- https://github.com/IBM/private-data-collections-on-fabric
- https://hyperledger-fabric.readthedocs.io/en/release-1.4/chaincode4ade.html
- P2P 网络核心技术:Gossip 协议