0%

Hyperledger Fabric 扫盲之私有数据

CHFA-hyperledger-fabric.png

前言

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 dataprivate data hash 两个元素组成,用于控制哪些组织或用户有权访问私有数据。

私有数据集存储

Untitled-1-Fabric-private-data-peer.png

上图是 peer 开启私有数据的示意图,由两部分组织,第一部分为公共数据就是我们平常指的的区块链和世界状态,第二部分就是私有数据,它包含以下三块:

  1. Private Writeset Storage (私有写集库)保存每一个私有数据集的所有交易的历史,每个 peer 中可以配置有多个 Private Writeset Storage实例。注意,这种存储不是区块链,而是一种典型的日志持久化数据库。
  2. Private State Database (私有数据库)类似于公共部分的世界状态,保存私有集合的当前状态。与私有写集库一样可以配置多个私有数据库实例。
  3. Transient Store (瞬态存储库)私有数据暂存库,属于过程态的临时库。

fabric-private-data-collection-Fabric-Explained-7-sharp-2.png

上图显示了同一个通道内来自不同组织的三个 peer,存储了#1#2 两个私有数据集实例。#1 对三个 peer 可见,#2 仅对 Org1-peerOrg2-peer 开放。

交易流程

fabric-private-data-collection-交易流程图.png

  1. 客户端应用程序提交一个调用链码函数(读或写私有数据)的提案请求,私有数据被发送到提案的 transient 字段中
  2. 背书节点仿真执行交易,并把私有数据存储至瞬态存储库,然后根据私有数据集策略,私有数据通过 gossip 协议分发至被授权的记账节点,这些记账节点同样将私有数据存储到瞬态存储库
  3. 只有当传完指定数量的记账节点后,背书节点将结果进行签名,返回给客户端的数据不包含私有数据,只会返回签名和私有数据 keyvalue 的哈希值
  4. 客户端将提案的响应发送给排序节点,排序节点无法看到私有数据的内容,只能看到一串哈希值
  5. 排序节点出块将交易区块广播给记账节点
  6. 对于公共数据记账节点会验证背书策略,验证成功会新增区块,更新世界状态,对于私有数据首先判断是否有为私有数据授权节点,如果是授权节点,检查本地瞬态存储库是否收到私有数据,如果没有收到则尝试从其他授权节点拉取,然后根据公共区块中的哈希验证私有数据,以上过程验证成功后,记账节点就会将瞬态存储库中的数据正式存储到私有数据库,最后删除瞬态存储库
  7. 记账节点存储完成后会通知客户端记账的情况

私有数据集配置

私有数据集只有在链码实例化或升级时才能配置,可以使用命令行 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
{
"name": "collection#1",
"policy": "OR('Org1MSP.member', 'Org2MSP.member', 'Org3MSP.member')",
"requiredPeerCount": 1,
"maxPeerCount": 3,
"blockToLive":50, // 第50个区块链后数据删除
"memberOnlyRead": true
},
{
"name": "collection#2",
"policy": "OR('Org1MSP.member', 'Org2MSP.member')",
"requiredPeerCount": 1,
"maxPeerCount": 2,
"blockToLive":20, // 第20个区块链后数据删除
"memberOnlyRead": true
}
]

前文说过私有数据集只有在链码实例化或升级时才能配置,所以如果需要更新私有数据集配置,只能升级链码版本,使用命令行 peer chaincode upgrade --collections-config ,升级时原有的集合配置必须包含在内

  1. 私有数据升级时 nameblockToLive 值必须保持一致
  2. 集合更新生效后,集合不能被删除,因为区块链上可能有先前私有数据的哈希,而区块链数据无法删除

Peer 间私有数据的同步

有关私有数据 Peer 配置项可查看 core.yaml 文件的 peer.gossip.pvtData 部分,以下我们将其截图出来解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pvtData:
# 记账阶段 peer 拉取私有数据的最大时间阈值,如果未在阈值内拉取完成则不会存值私有数据
pullRetryThreshold: 60s
# 前面我们学习了当私有交易提交记账后,私有数据会从瞬态存储库中删除,但如果这条交易永不提交呢,transientstoreMaxBlockRetention 就发挥作用了,表示瞬态存储库保留时间
# 当前区块高度开始计算至设定值以及设定值倍数的提交区块,将触发删除瞬态存储库存储的私有数据
transientstoreMaxBlockRetention: 1000
# 在背书阶段,私有数据推送到其他 peer 的等待超时时间
pushAckTimeout: 3s
# BTL缓冲器,防止 peer 拉取即将清除的私有数据
# 新增 peer 时,当 peer 解析到私有数据的公共信息时,会得到私有数据所在区块的编号,这时会去判断 ((区块高度 + btlPullMargin) > (私有数据所在区块高度+ BTL)),如果 true ,不会去拉取私密数据
# btlPullMargin 默认值是 10
btlPullMargin: 10
# 为了保障私有数据一致性,v1.4 版后新增补偿机制,它会无限循环拉取带有私有数据的最近丢失的区块, reconcileBatchSize 表示在每次迭代补偿中丢失数据最大批大小,默认为 10
reconcileBatchSize: 10
# 每次补偿迭代的间隔时间
reconcileSleepInterval: 1m
# 补偿器是否启用
reconciliationEnabled: true

为私有数据集合建立索引

使用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文档了解(需翻墙)

  1. SDK 调用 chaincode 执行范围查询或富查询时,如果查询的 peer 缺少私有数据,可能会返回结果集的一个子集,有些 Peer 可能不包含私有数据,SDK 可以查询多个 Peer 并比较结果,以确定是否丢失了私有数据。
  2. 不支持同一个事务中执行范围查询或富查询时,执行数据更新,因为无法判断 peer 是否有权限拥有私密数据权限,或是否丢失私密数据。如果在同一事务中同时包含查询和更新私密数据操作,提案将返回 Error。建议分为两个交易执行。但是一个 chaincode 方法中既包含 GetPrivateData()PutPrivateData() 是可以的,因为所有 peer 都可以基于 key 的哈希校验 key

私有数据内容保护

如果私有数据相对简单且可预测(例如交易金额),未授权的组织成员可能暴力哈希作用域内的私数据内容,以期匹配区块链上私有数据哈希。因此,可预计的私有数据内容可以混入随机数,比如安全的伪随机源,然后再调用链码时将私有数据传递到 transient 字段中。

实战

纸上得来终觉浅 绝知此事要躬行,正好最近在学习 Go,撸一遍加深印象吧!

  • 开发环境: go version go1.14 windows/amd64
  • 参考教程: fabric-samples

开撸之前咱得先设计一下应用场景,考虑一下实现功能。我简单想了个供应商场景

  1. 供应商数据属于机密数据
  2. 数据分两类,供应商基本资料和报价
  3. 成员之间访问权限不同

有了场景,私有数据配置就出来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
{
"name": "vendor", //供应商基本信息
"policy": "OR('Org1MSP.member', 'Org2MSP.member')", // 供应商基本信息org1和org2个有权限访问
"requiredPeerCount": 1,
"maxPeerCount": 3,
"blockToLive":10,
"memberOnlyRead": true
},
{
"name": "vendorPrice", //供应商报价
"policy": "OR('Org1MSP.member')", // 供应商报价只有Org1有权限
"requiredPeerCount": 1,
"maxPeerCount": 3,
"blockToLive":10,
"memberOnlyRead": true
}
]

接下来就是撸代码了,尽量把 chaincode 中引用私有数据的常用方法试一下,代码就不放了,有兴趣可以去我的 github 参考

供应商链码使用

利用 fabric-samples 样例快速拉套环境 ./byfn.sh up ,把项目代码挂载到 chaincode 目录下

  1. 安装链码,记得安装 Org2MSP 链码时得切换环境变量
    1
    peer chaincode install -n vendor -v 1.0 -p github.com/github.com/hoke58/fabric-chaincode/vendorPDC/
  2. 实例化
    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
  3. 创建供应商
    1
    2
    export 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\"}"
  4. 查询供应商基本信息
    1
    peer chaincode query -C mychannel -n vendor -c '{"Args":["getVendor","test0"]}'
  5. 查核供应商报价,Org2MSP 是看不到报价的
    1
    peer chaincode query -C mychannel -n vendor -c '{"Args":["getVendorPrice","test0"]}'
  6. 再创建几个供应商,试一下范围查询
    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 协议
坚持原创技术分享,您的支持将鼓励我继续创作!