阅读本文章前建议先了解《使用VS Code开发智能合约》
Private Data功能想要解决的问题
在区块链中,上链的数据可说是“永久的”、“公开的”的在各个参与方之间共享。但是现实应用场景中,很多数据出于隐私保护或者安全性的要求,希望数据不是“公开的”存在区块链账本里;另外也会出于存储成本等要求,希望数据不是“永久的”存在区块链账本里。这种场景中,我们最常见的解决方法是hash上链,也就是数据本身不上链,数据在链下根据也许需求在各方之间流转,数据的hash值存储到链上,使得各方都可以通过区块链技术来验证数据的完整性。但是这种方案最大的问题在于数据的存储、共享传输以及处理逻辑都和区块链脱钩了,用户需要单独开发这些系统,其中数据处理逻辑和智能合约的脱钩,更是一个非常棘手的问题。Fabric提供了Private Data相关的功能,直接在区块链中融入了解决上述问题的技术。
Private Data功能技术架构简述
Fabric官方的Private Data说明文档:https://hyperledger-fabric.readthedocs.io/en/release-1.4/private-data-arch.html
基于collection的数据隔离
在智能合约的部署、升级过程中,用户可以定义一系列的collections。collections长这样:
[
{
"name": "collectionMarbles",
"policy": "OR('Org1MSP.member', 'Org2MSP.member')",
"requiredPeerCount": 0,
"maxPeerCount": 3,
"blockToLive":1000000,
"memberOnlyRead": true
},
{
"name": "collectionMarblePrivateDetails",
"policy": "OR('Org1MSP.member')",
"requiredPeerCount": 0,
"maxPeerCount": 3,
"blockToLive":3,
"memberOnlyRead": true
}
]
对于每一个collection,peer节点们都会开一个“独立的账本”用来存储这部分数据,智能合约也需要使用专门的“PrivateData”系列API来访问这些账本。大家可以对比一下普通账本的接口和private data的接口:
// GetState returns the value of the specified `key` from the
// ledger. Note that GetState doesn't read data from the writeset, which
// has not been committed to the ledger. In other words, GetState doesn't
// consider data modified by PutState that has not been committed.
// If the key does not exist in the state database, (nil, nil) is returned.
GetState(key string) ([]byte, error)
// PutState puts the specified `key` and `value` into the transaction's
// writeset as a data-write proposal. PutState doesn't effect the ledger
// until the transaction is validated and successfully committed.
// Simple keys must not be an empty string and must not start with null
// character (0x00), in order to avoid range query collisions with
// composite keys, which internally get prefixed with 0x00 as composite
// key namespace.
PutState(key string, value []byte) error
// DelState records the specified `key` to be deleted in the writeset of
// the transaction proposal. The `key` and its value will be deleted from
// the ledger when the transaction is validated and successfully committed.
DelState(key string) error
// GetPrivateData returns the value of the specified `key` from the specified
// `collection`. Note that GetPrivateData doesn't read data from the
// private writeset, which has not been committed to the `collection`. In
// other words, GetPrivateData doesn't consider data modified by PutPrivateData
// that has not been committed.
GetPrivateData(collection, key string) ([]byte, error)
// PutPrivateData puts the specified `key` and `value` into the transaction's
// private writeset. Note that only hash of the private writeset goes into the
// transaction proposal response (which is sent to the client who issued the
// transaction) and the actual private writeset gets temporarily stored in a
// transient store. PutPrivateData doesn't effect the `collection` until the
// transaction is validated and successfully committed. Simple keys must not be
// an empty string and must not start with null character (0x00), in order to
// avoid range query collisions with composite keys, which internally get
// prefixed with 0x00 as composite key namespace.
PutPrivateData(collection string, key string, value []byte) error
// DelState records the specified `key` to be deleted in the private writeset of
// the transaction. Note that only hash of the private writeset goes into the
// transaction proposal response (which is sent to the client who issued the
// transaction) and the actual private writeset gets temporarily stored in a
// transient store. The `key` and its value will be deleted from the collection
// when the transaction is validated and successfully committed.
DelPrivateData(collection, key string) error
Private Data的流转流程
client通过transient字段传递Private Data到智能合约
- 普通的交易的参数(Args)会完整的记录在交易里,也就意味着会完整的记录在区块中。而通过transient字段传递的参数不会出现在交易里,智能合约使用完后就销毁了。
- 智能合约通过如下接口从交易中获取transient字段
// GetTransient returns the `ChaincodeProposalPayload.Transient` field.
// It is a map that contains data (e.g. cryptographic material)
// that might be used to implement some form of application-level
// confidentiality. The contents of this field, as prescribed by
// `ChaincodeProposalPayload`, are supposed to always
// be omitted from the transaction and excluded from the ledger.
GetTransient() (map[string][]byte, error)
智能合约通过Private Data系列接口读、写collention。在此过程中,Fabric会负责隐私数据的分发。
由此产生的交易中的写集不会包含数据本身,而是数据hash
- 数据本身Fabric会遵循collection中定义的policy,通过p2p网络发送给符合policy要求的peer节点
requiredPeerCount、maxPeerCount是collection中定义的用来保护数据可用性的,具体来说
- requiredPeerCount决定了peer在背书签名前,至少要把隐私数据分发给多少个其他peer
- maxPeerCount决定了peer在交易处理阶段,会主动推送隐私数据给多少个其他peer
- memberOnlyRead是一个推荐开启的访问控制策略。在绝大多数的Private Data使用场景中,我们不希望隐私数据流出到policy定义以外的组织中,自然也不希望那些组织的用户通过智能合约的接口来读取数据,开启这个访问控制策略,系统会自动拒绝policy定义以外的组织读取隐私数据。
交易提交orderer、并被peer节点commit
- 符合policy定义的peer在收到隐私交易后,会向其他peer拉取数据
- 同时peer会根据blockToLive的设定,清理过期的数据
在Visual Studio Code (VS Code) 中开发Private Data功能的合约
其实开发和调试Private Data合约和普通合约开发没有太大的区别。以下合约代码和collection-config.json可以供大家参考:
/*
* SPDX-License-Identifier: Apache-2.0
*/
package main
import (
"fmt"
"github.com/hyperledger/fabric/core/chaincode/shim"
sc "github.com/hyperledger/fabric/protos/peer"
)
// Chaincode is the definition of the chaincode structure.
type Chaincode struct {
}
// Init is called when the chaincode is instantiated by the blockchain network.
func (cc *Chaincode) Init(stub shim.ChaincodeStubInterface) sc.Response {
fcn, params := stub.GetFunctionAndParameters()
fmt.Println("Init()", fcn, params)
return shim.Success(nil)
}
// Invoke is called as a result of an application request to run the chaincode.
func (cc *Chaincode) Invoke(stub shim.ChaincodeStubInterface) sc.Response {
fcn, params := stub.GetFunctionAndParameters()
fmt.Println("Invoke()", fcn, params)
ts, err := stub.GetTransient()
if err != nil {
return shim.Error("GetTransient error!")
}
if fcn == "get" {
pd, err := stub.GetPrivateData("col1", "key")
if err != nil {
return shim.Error("GetPrivateData failed!")
}
return shim.Success(pd)
} else if fcn == "put" {
value, ok := ts["value"]
if !ok {
return shim.Error("Get value failed!")
}
err := stub.PutPrivateData("col1", "key", value)
if err != nil {
return shim.Error("PutPrivateData failed!")
}
return shim.Success(nil)
} else {
return shim.Error("Unknown function!")
}
}
[
{
"name": "col1",
"policy": {
"identities": [
{
"role": {
"name": "member",
"mspId": "Org1MSP"
}
}
],
"policy": {
"1-of": [
{
"signed-by": 0
}
]
}
},
"requiredPeerCount": 0,
"maxPeerCount": 0,
"blockToLive": 10000000,
"memberOnlyRead": true
}
]
其中policy是需要大家注意的以上policy是OR('Org1MSP.member')解析之后的样子。因为VS Code插件使用了fabric-nodejs-sdk,在nodejs sdk中对policy的定义是基于底层数据结构的。而Fabric的peer命令行会把"OR('Org1MSP.member')"之类的抽象规则解析为policy的底层定义后再提交,所以如果我们用peer chaincode instantiate之类命令来指定背书策略或者collection-config里的策略时,需要提供的是一个抽象的描述字符串。
在阿里云BaaS中使用private data功能
我们可以使用阿里云BaaS提供的VSCode 配套插件来把我们带private data功能的合约在云上部署、并实例化。要使用private data功能,我们需要提前准备好一个collections-config文件,这个文件中policy的定义和fabric官方文档保持一致,是一个描述字符串。
[
{
"name": "col1",
"policy": "OR('org1MSP.member')",
"requiredPeerCount": 1,
"maxPeerCount": 3,
"blockToLive": 10000000,
"memberOnlyRead": true
}
]
在activate chaincode这一步操作中,会提示我们是否需要设置collections。我们输入并选择collections-config文件。
memberOnlyRead策略验证
上图是一个两个组织的通道中,部署链码后,我们依次向org1的peer节点发起3次交易的情况
- 用org1用户put
- 用org1用户get
- 用org2用户get(这是一个使用org2的user向org1的gateway发起交易的例子。直接连接gateway只能使用本组织的身份。因此需要一些小技巧。操作步骤如下:在插件中断开gateway的连接,直接使用shift+command+p,使用evaluate transaction命令,此时由于未选择任何gateway和wallet,系统会列出所有选项)
前两个交易成功,而最后一个失败,可以看到memberOnlyRead策略阻止了org2的用户的get请求。
留一些课后作业给大家思考和尝试:
- org2用户无法读取,那是否可以写入呢?
- 读写请求发送到org2的peer上会发生什么?
- memberOnlyRead设置为false后进行上述测试,又会发生什么?
联系我们
欢迎感兴趣的同学加入钉钉群(钉钉群号: 23181816)。