翻译:https://hyperledger-fabric.readthedocs.io/en/latest/private_data_tutorial.html
本教程将演示如何使用区块链上的私有数据。本文档介绍如何使用是由数据存储和用例。更多信息,查看 Private data.
通过以下几步来定义,配置,使用私有数据。
1.建集合定义JSON文件。
2.使用链码读写私有数据。
3.使用集合安装定义链码。
4.存储私有数据。
5.有权限的peer查询私有数据
6.无权限的peer查询私有数据
7.清除私有数据
8.在私有数据中使用索引
9.附加资源
文档会部署 marbles private data sample到test network来证明怎样创建,部署,且使用私有数据。
建集合定义JSON文件
第一步需要建立个集合定义的文件,用来进入通道上的私有数据。
集合定义描述了谁拥有数据,数据分发到多少个peer上,需要多少个peer来分发数据,私有数据在数据库中保存多久。然后,我们将演示如何使用链码API PutPrivateData 和GetPrivateData 来将集合映射到受保护的私有数据。
集合定义由以下属性组成:
- name:结合的名字
- policy:定义允许拥有集合数据的组织peer
- requiredPeerCount:作为链码认可的条件,需要传播私有数据peer的数量。
- maxPeerCount:出于数据冗余的目的,当前的背书peer将尝试将数据分发到其它peer的数量。如果背书的peer关停了,此时有请求拉取私有数据,其它peer可用。
- blockToLive:对于敏感信息(如定价或个人信息),这代表了数据在私有数据库中保存的时间,以块为单位。数据将保留指定数量的块,然后会清除,标记数据从网络中清除。如果无限期保留,将此属性设置为 0
- .memberOnlyRead:为true表示peer强制只允许集合成员组织之一的客户端读取私有数据。
为说明私有数据的用法,示例marbles包含两个私有数据集合定义: collectionMarbles 和collectionMarblePrivateDetails。collectionMarbles 中的policy 允许所有通道成员(Org1和Org2)拥有私有数据。collectionMarblesPrivateDetails 集合只允许Org1拥有私有数据。
策略定义的更多信息查看 Endorsement policies。
// collections_config.json
[
{
"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
}
]
这些策略要保护的数据映射到链码中,稍后教程中介绍
当链码定义提交到通道时,集合定义文件也会部署。以下第三部分会介绍。
使用链码API读写私有数据
了解如何私有化通道上的数据的下一步是在链码中构建数据定义。示例marbles根据访问权限将私有数据分为两种 。
// Peers in Org1 and Org2 will have this private data in a side database
type marble struct {
ObjectType string `json:"docType"`
Name string `json:"name"`
Color string `json:"color"`
Size int `json:"size"`
Owner string `json:"owner"`
}
// Only peers in Org1 will have this private data in a side database
type marblePrivateDetails struct {
ObjectType string `json:"docType"`
Name string `json:"name"`
Price int `json:"price"`
}
具体来说,对私有数据的访问将收到如下限制:
- name, color, size, and owner对所有通道成员开放
- price 只能被Org1访问
这样两种不同的私有数据的设置就被定义了。数据和集合策略映射,策略由链码API控制。读写私有数据由集合定义中的GetPrivateData() 和 PutPrivateData(),可以在这里查看 here。
下图说了marbles私有数据示例使用的私有数据模型。
读数据
使用链码接口GetPrivateData()来查询私有数据。需要两个参数,集合名字和数据的key。调用集合collectionMarbles 允许Org1和Org2操作,调用集合collectionMarblePrivateDetails 允许Org1操作。更多细节查看marbles private data functions:
- readMarble 查找name, color, size and owner
- readMarblePrivateDetails 查找price
稍后使用peer命令调用
写数据
链码接口PutPrivateData()用来存储私有数据。接口也需要集合名称做参数。因为有两个集合,要在链码中调用两次:
1.写私有数据name, color, size and owner使用集合名collectionMarbles
2.写私有数据price,使用集合collectionMarblePrivateDetails
下面initMarble 函数的一部分,展示了PutPrivateData()被调用两次
// ==== Create marble object, marshal to JSON, and save to state ====
marble := &marble{
ObjectType: "marble",
Name: marbleInput.Name,
Color: marbleInput.Color,
Size: marbleInput.Size,
Owner: marbleInput.Owner,
}
marbleJSONasBytes, err := json.Marshal(marble)
if err != nil {
return shim.Error(err.Error())
}
// === Save marble to state ===
err = stub.PutPrivateData("collectionMarbles", marbleInput.Name, marbleJSONasBytes)
if err != nil {
return shim.Error(err.Error())
}
// ==== Create marble private details object with price, marshal to JSON, and save to state ====
marblePrivateDetails := &marblePrivateDetails{
ObjectType: "marblePrivateDetails",
Name: marbleInput.Name,
Price: marbleInput.Price,
}
marblePrivateDetailsBytes, err := json.Marshal(marblePrivateDetails)
if err != nil {
return shim.Error(err.Error())
}
err = stub.PutPrivateData("collectionMarblePrivateDetails", marbleInput.Name, marblePrivateDetailsBytes)
if err != nil {
return shim.Error(err.Error())
}
总结,在collection.json中定义的策略允许Org1和Org2的peer读写name, color, size, owner。但只有Org1中的peer才能读写price。
因为使用集合,所以只有私有数据hash通过orderer,而数据本身对orderer保密。
启动网络
下面使用一些命令来展示怎样使用私有数据。
首先初始化test network
cd fabric-samples/test-network
./network.sh down
如果还没有运行过本文档,那首先需要安装依赖包,命令如下:
cd ../chaincode/marbles02_private/go
GO111MODULE=on go mod vendor
cd ../../../test-network
如果已经运行过本文档,你需要删除之前运行链码的容器。运行下面的命令清除之前的容器:
docker rm -f $(docker ps -a | awk '($2 ~ /dev-peer.*.marblesp.*/) {print $1}')
docker rmi -f $(docker images | awk '($1 ~ /dev-peer.*.marblesp.*/) {print $3}')
在test-network目录下,你可以使用如下命令来启动网络(使用了CouchDB)
./network.sh up createChannel -s couchdb
命令会创建一个通道mychannel,通道上两个组织(每个组织含一个peer),一个ordering service且使用CouchDB作为状态数据库。collection可以使用 LevelDB和CouchDB。选择CouchDB是为了在私有数据中使用索引。
注意:保证collection正常工作,必须正确设置跨组织的gossip。在Gossip data dissemination protocol部分,特别注意“anchor peers”部分。本文档的test network已经正确配置了。当配置了通道,gossip锚节点就会正确的去工作。
在collection安装和定义链码
客户端应用通过链码和区块链账本交互。因此我们需要在每个peer上安装链码这样会有足够的交易背书。然而,在我们和链码交互之前,通道成员需要就链码定义达成一致,包含了私有数据collection配置。使用 peer lifecycle chaincode,来打包,安装,审议定义。
复制如下的环境变量切换为Org1 admin的身份。在test-network目录中:
export PATH=${PWD}/../bin:$PATH
export FABRIC_CFG_PATH=$PWD/../config/
export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
1.打包marbles链码
peer lifecycle chaincode package marblesp.tar.gz --path ../chaincode/marbles02_private/go/ --lang golang --label marblespv1
2.在peer0.org1.example.com上安装链码
peer lifecycle chaincode install marblesp.tar.gz
成功安装后返回:
2019-04-22 19:09:04.336 UTC [cli.lifecycle.chaincode] submitInstallProposal -> INFO 001 Installed remotely: response:<status:200 payload:"\nKmarblespv1:57f5353b2568b79cb5384b5a8458519a47186efc4fcadb98280f5eae6d59c1cd\022\nmarblespv1" >
2019-04-22 19:09:04.336 UTC [cli.lifecycle.chaincode] submitInstallProposal -> INFO 002 Chaincode code package identifier: marblespv1:57f5353b2568b79cb5384b5a8458519a47186efc4fcadb98280f5eae6d59c1cd
3.切换为Org2 admin
export CORE_PEER_LOCALMSPID="Org2MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=localhost:9051
4.Org2 peer上安装链码
peer lifecycle chaincode install marblesp.tar.gz
审议链码定义
通道成员在使用链码之前需要审议链码定义。本文档中每个组织都要使用链码,因此两个组织都要审议链码定义peer lifecycle chaincode approveformyorg。链码定义中包含了私有数据collection的定义。需要--collections-config指向collection JSON文件。
1.查询安装在peer上的package ID
peer lifecycle chaincode queryinstalled
2.将package ID放入环境变量
export CC_PACKAGE_ID=marblespv1:f8c8e06bfc27771028c4bbc3564341887881e29b92a844c66c30bac0ff83966e
3.确保以Org1运行,复制如下环境变量
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
4.审议链码定义,此命令中包含了collection的定义文件。
export ORDERER_CA=${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name marblesp --version 1.0 --collections-config ../chaincode/marbles02_private/collections_config.json --signature-policy "OR('Org1MSP.member','Org2MSP.member')" --package-id $CC_PACKAGE_ID --sequence 1 --tls --cafile $ORDERER_CA
5.切换到Org2
export CORE_PEER_LOCALMSPID="Org2MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=localhost:9051
6.Org2审议链码定义
peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name marblesp --version 1.0 --collections-config ../chaincode/marbles02_private/collections_config.json --signature-policy "OR('Org1MSP.member','Org2MSP.member')" --package-id $CC_PACKAGE_ID --sequence 1 --tls --cafile $ORDERER_CA
提交链码定义
一旦有足够(通常是大多数)的组织审议了链码定义,其中的某个组织就可以提交定义到通道上。
使用 peer lifecycle chaincode commit 提交链码定义,此命令也会部署collection定义到链码上。
marbles 链码中包含有初始化函数,因此首先需要peer chaincode invoke调用Init()。
1.提交链码定义到通道
export ORDERER_CA=${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
export ORG1_CA=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export ORG2_CA=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
peer lifecycle chaincode commit -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name marblesp --version 1.0 --sequence 1 --collections-config ../chaincode/marbles02_private/collections_config.json --signature-policy "OR('Org1MSP.member','Org2MSP.member')" --tls --cafile $ORDERER_CA --peerAddresses localhost:7051 --tlsRootCertFiles $ORG1_CA --peerAddresses localhost:9051 --tlsRootCertFiles $ORG2_CA
保存私有数据
Org1在示例中有操作私有数据的权限,切换到Org1,且增加一条数据
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
调用initMarble 函数创建一条数据,name:marble1 ,owned:tom,color:blue,size:35,price:99。记得prce是独立存储的。initMarble 函数会调用PutPrivateData()两次来保存数据,每个collection一次。注意,私有数据的传值使用--transient。作为临时数据传递的输入将不会在事务中持久化,以保持数据的私有性。临时数据作为二进制数据传递,因此在使用CLI时,必须对其进行base64编码。我们使用环境变量来存放base64编码的值,并使用tr命令去除linux base64命令中的换行符。
export MARBLE=$(echo -n "{\"name\":\"marble1\",\"color\":\"blue\",\"size\":35,\"owner\":\"tom\",\"price\":99}" | base64 | tr -d \\n)
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c '{"Args":["InitMarble"]}' --transient "{\"marble\":\"$MARBLE\"}"
输出结果如下:
[chaincodeCmd] chaincodeInvokeOrQuery->INFO 001 Chaincode invoke successful. result: status:200
以授权peer的身份查询私有数据
Org1有两个collection的权限。
第一步查询调用readMarble 函数,且传值collectionMarbles
// ===============================================
// 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)
}
第二步查询调用readMarblePrivateDetails ,传参数collectionMarblePrivateDetails
// ===============================================
// readMarblePrivateDetails - 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)
}
请注意,由于查询不会记录在分类帐上,因此不必将marble作为临时值传递。
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarble","marble1"]}'
你会看到如下结果
{"color":"blue","docType":"marble","name":"marble1","owner":"tom","size":35}
Org1查price
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarblePrivateDetails","marble1"]}'
结果:
{"docType":"marblePrivateDetails","name":"marble1","price":99}
没有权限的peer查询私有数据
现在切换到Org2,Org2没有储存price的数据。
切换到Org2
export CORE_PEER_LOCALMSPID="Org2MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=localhost:9051
查询有权限的私有数据
Org2可以使用readMarble()查询,此函数有collectionMarbles 参数。
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarble","marble1"]}'
结果:
{"docType":"marble","name":"marble1","color":"blue","size":35,"owner":"tom"}
查询无权限私有数据
Org2没有price私有数据在它这边的数据库中,当查询这个数据时,会返回一个哈希值,代表了公开状态的key,但不会有私有状态。
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarblePrivateDetails","marble1"]}'
结果:
Error: endorsement failure during query. response: status:500
message:"{\"Error\":\"Failed to get private details for marble1:
GET_STATE failed: transaction ID: d9c437d862de66755076aeebe79e7727791981606ae1cb685642c93f102b03e5:
tx creator does not have read access permission on privatedata in chaincodeName:marblesp collectionName: collectionMarblePrivateDetails\"}"
Org2只允许看私有数据的公开哈希值。
清除私有数据
用例中,私有数据在复制到链外数据库之前需要存储在链上,可以在一定数量区块之后清除数据,仅留下数据的哈希作为事务存在过的证据。
私有数据包含了个人和保密信息,例如本例中price数据。事务方不想让通道上的其它组织知道。因此,它的生命是有限的,并且可以使用集合定义中的blockToLive属性在区块链上对指定数量的区块保持不变后进行清除。
collectionMarblePrivateDetails 的定义中blockToLive 是3,意味着数据会存在3个区块然后消失。将所有的部分结合在一起,回想一下这个集合定义collectionMarblePrivateDetails 是在initMarble()函数,当在PutPrivateData()接口中传入collectionMarblePrivateDetails 参数。
我们将添加区块,并且观察当有四个事务后price清空(创建一条,后面三个交易)。
切换到Org1,并在peer容器中执行:
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
新的终端中,观看私有数据日志,注意区块高度:
docker logs peer0.org1.example.com 2>&1 | grep -i -a -E 'private|pvt|privdata'
回到peer容器中,查询price数据(query查询不会创建区块)
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarblePrivateDetails","marble1"]}'
结果:
{"docType":"marblePrivateDetails","name":"marble1","price":99}
此时price数据还在。
创建新区块:
export MARBLE=$(echo -n "{\"name\":\"marble2\",\"color\":\"blue\",\"size\":35,\"owner\":\"tom\",\"price\":99}" | base64 | tr -d \\n)
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c '{"Args":["InitMarble"]}' --transient "{\"marble\":\"$MARBLE\"}"
切换到终端查看日志。会看到区块高度+1
docker logs peer0.org1.example.com 2>&1 | grep -i -a -E 'private|pvt|privdata'
再回到peer容器,查询marble1的price
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarblePrivateDetails","marble1"]}'
结果:
{"docType":"marblePrivateDetails","name":"marble1","price":99}
交易marble2,增加区块
export MARBLE_OWNER=$(echo -n "{\"name\":\"marble2\",\"owner\":\"joe\"}" | base64 | tr -d \\n)
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c '{"Args":["TransferMarble"]}' --transient "{\"marble_owner\":\"$MARBLE_OWNER\"}"
回到终端,查看区块+1
docker logs peer0.org1.example.com 2>&1 | grep -i -a -E 'private|pvt|privdata'
再到peer容器,查marble1 price
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarblePrivateDetails","marble1"]}'
结果:
{"docType":"marblePrivateDetails","name":"marble1","price":99}
交易marble2 ,增加第三个区块
export MARBLE_OWNER=$(echo -n "{\"name\":\"marble2\",\"owner\":\"tom\"}" | base64 | tr -d \\n)
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c '{"Args":["TransferMarble"]}' --transient "{\"marble_owner\":\"$MARBLE_OWNER\"}"
再到终端查看区块+1
docker logs peer0.org1.example.com 2>&1 | grep -i -a -E 'private|pvt|privdata'
返回peer容器,查price
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarblePrivateDetails","marble1"]}'
结果:
{"docType":"marblePrivateDetails","name":"marble1","price":99}
最后再交易marble2,price会在交易结束后消失
export MARBLE_OWNER=$(echo -n "{\"name\":\"marble2\",\"owner\":\"jerry\"}" | base64 | tr -d \\n)
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c '{"Args":["TransferMarble"]}' --transient "{\"marble_owner\":\"$MARBLE_OWNER\"}"
到终端查看区块+1
docker logs peer0.org1.example.com 2>&1 | grep -i -a -E 'private|pvt|privdata'
到容器查看marble1 price
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarblePrivateDetails","marble1"]}'
price已经清空,结果:
Error: endorsement failure during query. response: status:500
message:"{\"Error\":\"Marble private details does not exist: marble1\"}"
在私有数据中使用索引
私有数据collection可以使用索引,打包索引在META-INF/statedb/couchdb/collections/<collection_name>/indexes目录在链码旁。一个例子 here
部署链码到生产环境,建议在链码旁边定义索引,以便链码和支持索引作为一个单元自动部署。一旦链码安装在peer上并在通道上实例化。当指定--collections-config标志指向集合JSON文件的位置时,将在通道上实例化链码时自动部署相关索引。