Hyperledger Fabric 2.0 官方文档中文版 第6章 教程(下)


总目录

第1章 引言
第2章 Hyperledger Fabric v2.0的新增功能
第3章 关键概念
第4章 入门
第5章 开发应用程序
第6章 教程(上)
第6章 教程(下)
第7章 部署生产网络
第8章 操作指南
第9章 升级到最新版本


6.教程(下)

使用CouchDB

本教程将描述使用CouchDB作为带有Hyperledger Fabric的状态数据库所需的步骤。现在,您应该已经熟悉了Fabric的概念,并且已经研究了一些示例和教程。

这些说明使用Fabric v2.0版本中引入的新Fabric链码生命周期。如果您希望使用以前的生命周期模型将索引与链码一起使用,请访问Using CouchDB的v1.4版本。

要深入了解CouchDB,请将CouchDB作为状态数据库,有关Fabric ledger的更多信息,请参阅ledger主题。按照下面的教程了解如何在区块链网络中利用CouchDB的详细信息。

在本教程中,我们将使用Marbles示例作为我们的用例来演示如何在Fabric中使用CouchDB,并将Marbles部署到Fabric测试网络中。您应该已经完成了安装示例、二进制文件和Docker镜像的任务。


为什么使用CouchDB?

Fabric支持两种类型的节点数据库。LevelDB是嵌入节点中的默认状态数据库。LevelDB将链码数据存储为简单的键值对,只支持key、key range和复合键查询。CouchDB是一个可选的备用状态数据库,允许您将账本上的数据建模为JSON,并针对数据值而不是键发出丰富的查询。CouchDB还允许您使用链码部署索引,以提高查询效率,并使您能够查询大型数据集。

为了利用CouchDB的优点,即基于内容的JSON查询,您的数据必须以JSON格式建模。在设置网络之前,必须决定是使用LevelDB还是CouchDB。由于数据兼容性问题,不支持将节点从使用LevelDB切换到CouchDB。网络上的所有节点必须使用相同的数据库类型。如果您混合了JSON和二进制数据值,那么仍然可以使用CouchDB,但是只能根据key、key range和组合键查询查询二进制值。


在Hyperledger Fabric中启用CouchDB

CouchDB作为一个独立的数据库进程与节点进程一起运行。在设置、管理和操作方面还有其他注意事项。CouchDB的Docker镜像是可用的,我们建议它与节点服务器运行在同一个服务器上。您将需要为每个节点设置一个CouchDB容器,并通过更改core.yaml中的配置来更新每个对等容器,以指向CouchDB容器。core.yaml文件必须位于环境变量FABRIC_CFG_PATH指定的目录中

  • 对于Docker部署,core.yaml是预先配置的,位于对等容器FABRIC_CFG_PATH文件夹中。但是,当使用Docker环境时,通常通过编辑docker-compose-couch.yaml来覆盖core.yaml来传递环境变量
  • 对于本机二进制部署,core.yaml包含在发布工件发行版中。

编辑core.yamlstateDatabase部分。指定CouchDB作为stateDatabase并填充相关联的couchDBConfig属性。有关更多信息,请参阅CouchDB配置


创建索引

为什么索引很重要?

索引允许查询数据库,而不必在每次查询中检查每一行,从而使它们运行得更快、更高效。通常,索引是为频繁出现的查询条件而构建的,这样可以更有效地查询数据。为了利用CouchDB的主要优点(对JSON数据执行丰富查询的能力),不需要索引,但是强烈建议使用索引来提高性能。另外,如果查询中需要排序,CouchDB需要排序字段的索引。

注意:没有索引的富查询可以工作,但可能会在CouchDB日志中抛出一个警告,说明找不到索引。但是,如果富查询包含排序规范,则需要该字段上的索引;否则,查询将失败并引发错误。

为了演示如何建立索引,我们将使用Marbles样本中的数据。在本例中,Marbles数据结构定义为:

type marble struct {
         ObjectType string `json:"docType"` //docType is used to distinguish the various types of objects in state database
         Name       string `json:"name"`    //the field tags are needed to keep case from bouncing around
         Color      string `json:"color"`
         Size       int    `json:"size"`
         Owner      string `json:"owner"`
}

在此结构中,属性(docType、name、color、size、owner)定义与资产关联的账本数据。docType属性是链码中使用的一种模式,用于区分可能需要单独查询的不同数据类型。使用CouchDB时,建议包含此docType属性以区分chaincode命名空间中的每种类型的文档。(每个链码都表示为自己的CouchDB数据库,也就是说,每个链码都有自己的键名称空间。)

对于Marbles数据结构,docType用于标识此文档/资产是Marbles资产。链码数据库中可能存在其他文档/资产。数据库中的文档可以根据所有这些属性值进行搜索。

定义用于链码查询的索引时,每个索引都必须在其扩展名为*.json的文本文件中定义,并且索引定义的格式必须为CouchDB index json格式。

要定义索引,需要三条信息:

  • 字段:这些是经常查询的字段
  • 名称:索引的名称
  • 类型:在这个环境中总是json

例如,对于名为foo的字段,一个名为foo-index的简单索引。

{
    "index": {
        "fields": ["foo"]
    },
    "name" : "foo-index",
    "type" : "json"
}

可以选择在索引定义上指定设计文档属性ddoc设计文档是为包含索引而设计的CouchDB构造。为了提高效率,可以将索引分组到设计文档中,但是CouchDB建议每个设计文档使用一个索引。

小贴士:定义索引时,最好将ddoc属性和值与索引名称一起包含在内。重要的是要包含此属性,以确保以后可以根据需要更新索引。它还使您能够显式指定在查询上使用哪个索引。

以下是来自Marbles示例的另一个索引定义示例,索引名称为indexOwner,使用多个字段docTypeowner,并包含ddoc属性:

{
  "index":{
      "fields":["docType","owner"] // Names of the fields to be queried
  },
  "ddoc":"indexOwnerDoc", // (optional) Name of the design document in which the index will be created.
  "name":"indexOwner",
  "type":"json"
}

在上面的示例中,如果设计文档indexOwnerDoc不存在,则在部署索引时会自动创建它。可以使用字段列表中指定的一个或多个属性来构造索引,并且可以指定任何属性组合。一个属性可以存在于同一docType的多个索引中。在下面的示例中,index1仅包含属性所有者index2包含属性所有者颜色index3包括属性所有者颜色大小。另外,请注意,遵循CouchDB推荐的实践,每个索引定义都有自己的ddoc值。

{
  "index":{
      "fields":["owner"] // Names of the fields to be queried
  },
  "ddoc":"index1Doc", // (optional) Name of the design document in which the index will be created.
  "name":"index1",
  "type":"json"
}

{
  "index":{
      "fields":["owner", "color"] // Names of the fields to be queried
  },
  "ddoc":"index2Doc", // (optional) Name of the design document in which the index will be created.
  "name":"index2",
  "type":"json"
}

{
  "index":{
      "fields":["owner", "color", "size"] // Names of the fields to be queried
  },
  "ddoc":"index3Doc", // (optional) Name of the design document in which the index will be created.
  "name":"index3",
  "type":"json"
}

通常,您应该为索引字段建模,以匹配将在查询过滤器和排序中使用的字段。有关以JSON格式构建索引的更多详细信息,请参阅CouchDB文档

关于索引的最后一句话,Fabric使用一种称为index warming的模式为数据库中的文档编制索引。CouchDB通常在下一个查询之前不会索引新的或更新的文档。Fabric通过在提交每个数据块后请求索引更新来确保索引保持“温暖”。这样可以确保查询的速度很快,因为它们在运行查询之前不必索引文档。此过程使索引保持最新状态,并在每次向状态数据库添加新记录时进行刷新。


将索引添加到链码文件夹

完成索引后,需要将其与链码一起打包以进行部署,方法是将其放在适当的元数据文件夹中。可以使用peer lifecycle chaincode命令安装链码。JSON索引文件必须位于路径META-INF/statedb/couchdb/index下,该路径位于链码所在的目录中。

下面的Marbles示例说明了如何使用链码打包索引。
couchdb pkg 示例
此示例包含一个名为indexOwnerDoc的索引:

{"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}

启动网络

我们将启动Fabric测试网络,并用它来部署弹珠链。使用以下命令导航到Fabric samples中的test-network目录:

cd fabric-samples/test-network

对于本教程,我们希望从已知的初始状态开始操作。以下命令将终止任何活动或过时的Docker容器,并移除以前生成的构件:

./network.sh down

如果您以前没有看过本教程,那么在我们将其部署到网络之前,您需要提供链码依赖项。运行以下命令:

cd ../chaincode/marbles02/go
GO111MODULE=on go mod vendor
cd ../../../test-network

在test-network目录中,使用CouchDB使用以下命令部署测试网络:

./network.sh up createChannel -s couchdb

这将创建两个使用CouchDB作为状态数据库的fabric普通节点。它还将创建一个排序节点和一个名为mychannel的通道。


安装并定义链码

客户端应用程序通过链码与区块链账本交互。因此,我们需要在每一个执行和背书我们的交易的节点上安装一个链码。然而,在我们可以与我们的链码交互之前,通道成员需要就建立链码管理的链码定义达成一致。在上一节中,我们演示了如何将索引添加到chaincode文件夹,以便将索引与chaincode一起部署。

链码需要打包后才能安装到我们的节点上。我们可以使用peer lifecycle chaincode package命令来打包弹珠链码。

  1. 启动测试网络后,在CLI中复制并粘贴以下环境变量,以Org1管理员的身份与网络交互。确保您在测试网络目录中。
export PATH=${PWD}/../bin:${PWD}:$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. 使用以下命令打包弹珠链码:
peer lifecycle chaincode package marbles.tar.gz --path ../chaincode/marbles02/go --lang golang --label marbles_1

此命令将创建一个名为marbles.tar.gz的链码包

  1. 使用以下命令将链码包安装到节点peer0.org1.example.com
peer lifecycle chaincode install marbles.tar.gz

成功的安装命令将返回链码标识符,类似于下面的响应:

2019-04-22 18:47:38.312 UTC [cli.lifecycle.chaincode] submitInstallProposal -> INFO 001 Installed remotely: response:<status:200 payload:"\nJmarbles_1:0907c1f3d3574afca69946e1b6132691d58c2f5c5703df7fc3b692861e92ecd3\022\tmarbles_1" >
2019-04-22 18:47:38.312 UTC [cli.lifecycle.chaincode] submitInstallProposal -> INFO 002 Chaincode code package identifier: marbles_1:0907c1f3d3574afca69946e1b6132691d58c2f5c5703df7fc3b692861e92ecd3

peer0.org1.example.com上安装链码之后,我们需要批准Org1的链码定义。

  1. 使用下面的命令查询节点以获取已安装的链代码的包ID。
peer lifecycle chaincode queryinstalled

该命令将返回与install命令相同的包标识符。您将看到类似于以下内容的输出:

Installed chaincodes on peer:
Package ID: marbles_1:60ec9430b221140a45b96b4927d1c3af736c1451f8d432e2a869bdbf417f9787, Label: marbles_1
  1. 将包ID声明为环境变量。

peer lifecycle chaincode queryinstalled命令返回的marbles_1包ID粘贴到下面的命令中。对于所有用户,包ID可能不相同,因此您需要使用从控制台返回的包ID来完成此步骤。

export CC_PACKAGE_ID=marbles_1:60ec9430b221140a45b96b4927d1c3af736c1451f8d432e2a869bdbf417f9787
  1. 使用以下命令批准Org1的Marbles链码定义。
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 marbles --version 1.0 --signature-policy "OR('Org1MSP.member','Org2MSP.member')" --init-required --package-id $CC_PACKAGE_ID --sequence 1 --tls --cafile $ORDERER_CA

当命令成功完成时,您将看到类似于:

2020-01-07 16:24:20.886 EST [chaincodeCmd] ClientWait -> INFO 001 txid [560cb830efa1272c85d2f41a473483a25f3b12715d55e22a69d55abc46581415] committed with status (VALID) at

我们需要大多数组织在将链码定义提交到通道之前批准它。这意味着我们也需要Org2来批准链码定义。因为我们不需要Org2来背书链码,也没有在任何Org2节点上安装包,所以我们不需要提供packageID作为链码定义的一部分。

  1. 使用CLI作为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
  1. 使用以下命令批准Org2的链码定义:
peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name marbles --version 1.0 --signature-policy "OR('Org1MSP.member','Org2MSP.member')" --init-required --sequence 1 --tls --cafile $ORDERER_CA
  1. 我们现在可以使用peer lifecycle chaincode commit命令将链码定义提交到通道:
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 marbles --version 1.0 --sequence 1 --signature-policy "OR('Org1MSP.member','Org2MSP.member')" --init-required --tls --cafile $ORDERER_CA --peerAddresses localhost:7051 --tlsRootCertFiles $ORG1_CA --peerAddresses localhost:9051 --tlsRootCertFiles $ORG2_CA

当提交交易成功完成时,您应该看到类似于:

2019-04-22 18:57:34.274 UTC [chaincodeCmd] ClientWait -> INFO 001 txid [3da8b0bb8e128b5e1b6e4884359b5583dff823fce2624f975c69df6bce614614] committed with status (VALID) at peer0.org2.example.com:9051
2019-04-22 18:57:34.709 UTC [chaincodeCmd] ClientWait -> INFO 002 txid [3da8b0bb8e128b5e1b6e4884359b5583dff823fce2624f975c69df6bce614614] committed with status (VALID) at peer0.org1.example.com:7051
  1. 因为弹珠链码包含一个初始化函数,我们需要使用peer chaincode invoke命令来调用Init(),然后才能使用链码中的其他函数:
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name marbles --isInit --tls --cafile $ORDERER_CA --peerAddresses localhost:7051 --tlsRootCertFiles $ORG1_CA -c '{"Args":["Init"]}'

验证索引是否已部署

在节点上安装了链码并将其部署到通道后,索引将被部署到每个节点的CouchDB状态数据库中。您可以通过检查Docker容器中的对等日志来验证CouchDB索引是否已成功创建。

要查看peer Docker容器中的日志,请打开一个新的终端窗口,并运行以下命令以grep确认索引已创建。

docker logs peer0.org1.example.com  2>&1 | grep "CouchDB index"

您将看到如下所示的结果:

[couchdb] CreateIndex -> INFO 0be Created CouchDB index [indexOwner] in state database [mychannel_marbles] using design document [_design/indexOwnerDoc]

注意:如果您在不同于peer0.org1.example.com的节点上安装了Marbles,则可能需要将其替换为安装Marbles的其他节点的名称。


查询CouchDB状态数据库

既然索引已经在JSON文件中定义并与chaincode一起部署,那么chaincode函数可以对CouchDB状态数据库执行JSON查询,因此节点命令可以调用chaincode函数。

在查询中指定索引名称是可选的。如果未指定,并且正在查询的字段已经存在索引,则将自动使用现有索引。

小贴士:在查询中使用use_index关键字显式包含索引名称是一个很好的做法。没有它,CouchDB可能会选择一个不太理想的指数。另外,CouchDB可能根本就不使用索引,而且在测试期间的低容量下您可能没有意识到。只有在更高的数量上,您可能会意识到性能变慢,因为CouchDB没有使用索引,而您假设它是这样的。

在链码中构建查询

您可以使用在链码中定义的查询对账本上的数据执行复杂的富格式查询。marbles02示例包含两个丰富的查询函数:

  • queryMarbles -
    特别丰富查询的示例。这是一个可以将(选择器)字符串传递到函数中的查询。此查询对于需要在运行时动态构建自己的选择器的客户端应用程序非常有用。有关选择器的更多信息,请参阅CouchDB选择器语法。
  • queryMarblesByOwner -
    参数化查询的示例,其中查询逻辑被刻到链码中。在本例中,函数接受一个参数,即Marbles所有者。然后,它使用JSON查询语法在状态数据库中查询与docType“marble”和所有者id相匹配的JSON文档。

使用peer命令运行查询

在没有客户端应用程序的情况下,我们可以使用peer命令来测试链码中定义的查询。我们将定制peer chaincode query命令,以使用Marbles索引indexOwner,并使用queryMarbles函数查询“tom”拥有的所有Marbles。

在查询数据库之前,我们应该添加一些数据。以Org1身份运行以下命令以创建“tom”拥有的Marbles:

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
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 marbles -c '{"Args":["initMarble","marble1","blue","35","tom"]}'

在初始化链码时部署索引后,链码查询将自动使用该索引。CouchDB可以根据所查询的字段来确定使用哪个索引。如果查询条件存在索引,则将使用该索引。但是,建议的方法是在查询中指定use_index关键字。下面的peer命令是一个示例,说明如何通过包含use_index关键字在选择器语法中显式指定索引:

// Rich Query with index name explicitly specified:
peer chaincode query -C mychannel -n marbles -c '{"Args":["queryMarbles", "{\"selector\":{\"docType\":\"marble\",\"owner\":\"tom\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}"]}'

深入研究上面的查询命令,有三个感兴趣的参数:

  • queryMarbles

弹珠链码中函数的名称。注意一个shimshim.ChaincodeStubInterface用于访问和修改账本。getQueryResultForQueryString()将查询字符串传递给shim API getQueryResult()

func (t *SimpleChaincode) queryMarbles(stub shim.ChaincodeStubInterface, args []string) pb.Response {

        //   0
        // "queryString"
         if len(args) < 1 {
                 return shim.Error("Incorrect number of arguments. Expecting 1")
         }

         queryString := args[0]

         queryResults, err := getQueryResultForQueryString(stub, queryString)
         if err != nil {
               return shim.Error(err.Error())
         }
         return shim.Success(queryResults)
}
  • {"selector":{"docType":"marble","owner":"tom"}
    这是一个ad hoc selector字符串的示例,它查找类型为marble的所有文档,其中owner属性的值为tom

  • "use_index":["_design/indexOwnerDoc", "indexOwner"]
    指定设计文档名称indexOwnerDoc和索引名称indexOwner。在本例中,选择器查询显式包含索引名称,该名称通过使用use_index关键字指定。回顾上面的索引定义Create an index,它包含一个设计文档“ddoc”:“indexOwnerDoc”。对于CouchDB,如果您计划在查询中显式地包含索引名称,那么索引定义必须包含ddoc值,以便可以使用use_index关键字引用它。

查询成功运行,并利用索引得到以下结果:

Query Result: [{"Key":"marble1", "Record":{"color":"blue","docType":"marble","name":"marble1","owner":"tom","size":35}}]

对查询和索引使用最佳实践

使用索引的查询将更快地完成,而不必扫描couchDB中的完整数据库。理解索引将允许您编写查询以获得更好的性能,并帮助您的应用程序处理网络上的大量数据或块。

计划用链码安装的索引也很重要。对于支持大多数查询的链码,您应该只安装几个索引。添加过多的索引或在索引中使用过多的字段会降低网络的性能。这是因为索引是在每个块被提交后更新的。通过“索引预热”需要更新的索引越多,完成交易所需的时间就越长。

本节中的示例将有助于演示查询如何使用索引以及哪种类型的查询具有最佳性能。编写查询时请记住以下几点:

  • 索引中的所有字段也必须位于要使用的索引的查询的选择器或排序部分中。
  • 更复杂的查询将具有更低的性能,并且不太可能使用索引。
  • 您应该尽量避免使用会导致全表扫描或全索引扫描的运算符,如$or$in$regex

在本教程的上一节中,您针对弹珠链码发出了以下查询:

// Example one: query fully supported by the index
peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarbles", "{\"selector\":{\"docType\":\"marble\",\"owner\":\"tom\"}, \"use_index\":[\"indexOwnerDoc\", \"indexOwner\"]}"]}'

索引上安装了弹珠索引:

{"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}

请注意,查询中的两个字段docTypeowner都包含在索引中,使其成为一个完全受支持的查询。因此,此查询将能够使用索引中的数据,而不必搜索整个数据库。完全支持的查询(如此查询)将比链码中的其他查询更快地返回。

如果向上面的查询添加额外的字段,它仍将使用索引。但是,查询还必须扫描索引数据中的额外字段,从而导致更长的响应时间。例如,下面的查询仍将使用索引,但返回时间将比上一个示例长。

// Example two: query fully supported by the index with additional data
peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarbles", "{\"selector\":{\"docType\":\"marble\",\"owner\":\"tom\",\"color\":\"red\"}, \"use_index\":[\"/indexOwnerDoc\", \"indexOwner\"]}"]}'

如果查询不包括索引中的所有字段,则必须扫描整个数据库。例如,下面的查询搜索所有者,而不指定所属项的类型。由于ownerIndexDoc同时包含ownerdocType字段,因此此查询将无法使用索引。

// Example three: query not supported by the index
peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarbles", "{\"selector\":{\"owner\":\"tom\"}, \"use_index\":[\"indexOwnerDoc\", \"indexOwner\"]}"]}'

一般来说,更复杂的查询将具有较长的响应时间,并且受索引支持的可能性也更低。诸如$or$in$regex等运算符通常会导致查询扫描完整索引或根本不使用索引。

例如,下面的查询包含一个$or术语,它将搜索tom拥有的每个Marbles和每个项目。

// Example four: query with $or supported by the index
peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarbles", "{\"selector\":{"\$or\":[{\"docType\:\"marble\"},{\"owner\":\"tom\"}]}, \"use_index\":[\"indexOwnerDoc\", \"indexOwner\"]}"]}'

此查询仍将使用索引,因为它搜索indexOwnerDoc中包含的字段。但是,查询中的$or条件要求扫描索引中的所有项,从而导致响应时间更长。

下面是索引不支持的复杂查询的示例。

// Example five: Query with $or not supported by the index
peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarbles", "{\"selector\":{"\$or\":[{\"docType\":\"marble\",\"owner\":\"tom\"},{"\color\":"\yellow\"}]}, \"use_index\":[\"indexOwnerDoc\", \"indexOwner\"]}"]}'

该查询将搜索tom拥有的所有Marbles或任何其他黄色项目。此查询将不使用索引,因为它需要搜索整个表以满足$or条件。根据账本上的数据量,此查询将需要很长时间才能响应或可能超时。

虽然查询遵循最佳实践很重要,但使用索引并不是收集大量数据的解决方案。区块链数据结构经过优化以验证和确认交易,不适用于数据分析或报告。如果要将仪表板构建为应用程序的一部分或分析网络中的数据,最佳做法是查询从节点复制数据的链外数据库。这将允许您理解区块链上的数据,而不会降低网络性能或中断交易。

您可以使用应用程序中的块或链码事件将交易数据写入链外数据库或分析引擎。对于接收到的每个块,块侦听器应用程序将遍历块交易,并使用来自每个有效交易的rwset的键/值写入来构建数据存储。基于节点通道的事件服务提供可重放的事件,以确保下游数据存储的完整性。有关如何使用事件侦听器将数据写入外部数据库的示例,请访问Fabric示例中的链外数据示例


分页查询CouchDB状态数据库

当CouchDB查询返回大的结果集时,可以使用一组APIs,这些APIs可以由链码调用来对结果列表进行分页。分页提供了一种机制,通过指定pagesize和起始点(一个指示结果集开始位置的bookmark)来划分结果集。客户端应用程序反复调用执行查询的链码,直到不再返回任何结果。有关更多信息,请参阅有关使用CouchDB分页的主题

我们将使用Marbles示例函数queryMarblesWithPagination来演示如何在链码和客户端应用程序中实现分页。

  • 带分页的查询标记 -
    带有分页的特别富查询的示例。这是一个查询,其中可以将(选择器)字符串传递到与上述示例类似的函数中。在本例中,查询中还包括pageSizebookmark

为了演示分页,需要更多的数据。本例假设您已经从上面添加了marble1。在节点容器中运行以下命令以创建“tom”拥有的四个弹珠,以创建“tom”拥有的总共五个弹珠:

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
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 marbles -c '{"Args":["initMarble","marble2","yellow","35","tom"]}'
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 marbles -c '{"Args":["initMarble","marble3","green","20","tom"]}'
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 marbles -c '{"Args":["initMarble","marble4","purple","20","tom"]}'
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 marbles -c '{"Args":["initMarble","marble5","blue","40","tom"]}'

除了上例中查询的参数外,queryMarblesWithPagination还添加了pagesizebookmarkPageSize指定每个查询要返回的记录数。bookmark是一个“锚”,告诉couchDB从哪里开始页面。(每个结果页都返回一个唯一的书签。)

  • queryMarblesWithPagination
    弹珠链码中函数的名称。注意一个shim shim.ChaincodeStubInterface用于访问和修改账本。getQueryResultForQueryStringWithPagination()将查询字符串与pagesize和bookmark一起传递给shim API GetQueryResultWithPagination()
func (t *SimpleChaincode) queryMarblesWithPagination(stub shim.ChaincodeStubInterface, args []string) pb.Response {

      //   0
      // "queryString"
      if len(args) < 3 {
              return shim.Error("Incorrect number of arguments. Expecting 3")
      }

      queryString := args[0]
      //return type of ParseInt is int64
      pageSize, err := strconv.ParseInt(args[1], 10, 32)
      if err != nil {
              return shim.Error(err.Error())
      }
      bookmark := args[2]

      queryResults, err := getQueryResultForQueryStringWithPagination(stub, queryString, int32(pageSize), bookmark)
      if err != nil {
              return shim.Error(err.Error())
      }
      return shim.Success(queryResults)
}

下面的示例是一个peer命令,它使用pageSize 3调用queryMarblesWithPagination,但没有指定书签。

小贴士:当没有指定书签时,查询从记录的“第一”页开始。

// Rich Query with index name explicitly specified and a page size of 3:
peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarblesWithPagination", "{\"selector\":{\"docType\":\"marble\",\"owner\":\"tom\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}","3",""]}'

收到以下响应(为清楚起见,添加了回车符),五个弹珠中的三个返回,因为pagsize设置为3

[{"Key":"marble1", "Record":{"color":"blue","docType":"marble","name":"marble1","owner":"tom","size":35}},
 {"Key":"marble2", "Record":{"color":"yellow","docType":"marble","name":"marble2","owner":"tom","size":35}},
 {"Key":"marble3", "Record":{"color":"green","docType":"marble","name":"marble3","owner":"tom","size":20}}]
[{"ResponseMetadata":{"RecordsCount":"3",
"Bookmark":"g1AAAABLeJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYqz5yYWJeWkGoOkOWDSOSANIFk2iCyIyVySn5uVBQAGEhRz"}}]

注意:CouchDB为每个查询惟一地生成书签,并表示结果集中的占位符。在查询的后续迭代中传递返回的书签以检索下一组结果。

下面是一个peer命令,用于调用pageSize为3的queryMarblesWithPagination。请注意,这次查询包含了从上一个查询返回的书签。

peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarblesWithPagination", "{\"selector\":{\"docType\":\"marble\",\"owner\":\"tom\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}","3","g1AAAABLeJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYqz5yYWJeWkGoOkOWDSOSANIFk2iCyIyVySn5uVBQAGEhRz"]}'

收到以下响应(为清楚起见,添加回车符)。检索最后两条记录:

[{"Key":"marble4", "Record":{"color":"purple","docType":"marble","name":"marble4","owner":"tom","size":20}},
 {"Key":"marble5", "Record":{"color":"blue","docType":"marble","name":"marble5","owner":"tom","size":40}}]
[{"ResponseMetadata":{"RecordsCount":"2",
"Bookmark":"g1AAAABLeJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYqz5yYWJeWkmoKkOWDSOSANIFk2iCyIyVySn5uVBQAGYhR1"}}]

最后一个命令是一个peer命令,它使用pageSize为3的queryMarblesWithPagination调用前一个查询中的书签。

peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarblesWithPagination", "{\"selector\":{\"docType\":\"marble\",\"owner\":\"tom\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}","3","g1AAAABLeJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYqz5yYWJeWkmoKkOWDSOSANIFk2iCyIyVySn5uVBQAGYhR1"]}'

收到以下响应(为清楚起见,添加回车符)。不返回任何记录,表明已检索到所有页面:

[]
[{"ResponseMetadata":{"RecordsCount":"0",
"Bookmark":"g1AAAABLeJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYqz5yYWJeWkmoKkOWDSOSANIFk2iCyIyVySn5uVBQAGYhR1"}}]

有关客户机应用程序如何使用分页遍历结果集的示例,请在Marbles Samples中搜索getQueryResultForQueryStringWithPagination函数。


更新索引

随着时间的推移,可能需要更新索引。安装的链码的后续版本中可能存在相同的索引。为了更新索引,原始索引定义必须包含设计文档ddoc属性和索引名称。要更新索引定义,请使用相同的索引名称,但要更改索引定义。只需编辑索引JSON文件并在索引中添加或删除字段。Fabric只支持索引类型JSON。不支持更改索引类型。当链码定义提交到通道时,更新的索引定义被重新部署到节点的状态数据库。对索引名称或ddoc属性的更改将导致创建一个新索引,并且原始索引在CouchDB中保持不变,直到将其删除。

注意:如果状态数据库有大量的数据,则重新构建索引将需要一些时间,在此期间,链码调用问题查询可能会失败或超时。

迭代索引定义

如果您可以在开发环境中访问节点的CouchDB状态数据库,则可以迭代地测试各种索引,以支持链码查询。但对链码的任何更改都需要重新部署。使用CouchDB-Fauxton接口或命令行curl实用程序来创建和更新索引。

注意:Fauxton接口是一个用于创建、更新和将索引部署到CouchDB的web UI。如果你想试试这个界面,这里有一个faxton版本的Marbles索引格式示例。如果您已经使用CouchDB部署了测试网络,那么可以通过打开浏览器并导航到来加载Fauxton接口http://localhost:5984/_utils

或者,如果您不喜欢使用Fauxton UI,下面是一个curl命令的示例,该命令可用于在数据库mychannel_marbles上创建索引:

// Index for docType, owner.
// Example curl command line to define index in the CouchDB channel_chaincode database
 curl -i -X POST -H "Content-Type: application/json" -d
        "{\"index\":{\"fields\":[\"docType\",\"owner\"]},
          \"name\":\"indexOwner\",
          \"ddoc\":\"indexOwnerDoc\",
          \"type\":\"json\"}" http://hostname:port/mychannel_marbles/_index

如果您使用的是配置有CouchDB的测试网络,请替换端口:localhost:5984


删除索引

Fabric工具不管理索引删除。如果需要删除索引,可以手动对数据库发出curl命令,或者使用Fauxton接口删除它。

删除索引的curl命令的格式为:

curl -X DELETE http://localhost:5984/{database_name}/_index/{design_doc}/json/{index_name} -H  "accept: */*" -H  "Host: localhost:5984"

要删除本教程中使用的索引,curl命令将是:

curl -X DELETE http://localhost:5984/mychannel_marbles/_index/indexOwnerDoc/json/indexOwner -H  "accept: */*" -H  "Host: localhost:5984"

向通道添加组织

请确保您已下载了安装示例、二进制文件和Docker镜像以及符合本文档版本(可在左侧目录底部找到)的先决条件中列出的相应镜像和二进制文件。

本教程通过向应用程序通道添加一个新的组织Org3来扩展Fabric测试网络。

虽然我们将重点关注向频道添加新组织,但您可以使用类似的过程来进行其他频道配置更新(例如,更新修改策略或更改批处理大小)。要了解更多有关通道配置更新的过程和可能性,请查看更新通道配置)。同样值得注意的是,这里演示的通道配置更新通常由组织管理员(而不是链码或应用程序开发人员)负责。


设置环境

我们将从您的本地克隆fabric-samples中的test-network子目录的根目录进行操作。现在转到那个目录。

cd fabric-samples/test-network

首先,使用network.sh整理脚本。此命令将杀死任何活动或过时的Docker容器,并删除以前生成的构件。为了执行通道配置更新任务,绝不需要关闭Fabric网络。但是,为了本教程的目的,我们希望从已知的初始状态开始操作。因此,让我们运行以下命令来清理以前的任何环境:

./network.sh down

现在,您可以使用该脚本启动带有一个名为mychannel的通道的测试网络:

./network.sh up createChannel

如果命令成功,您可以在日志中看到以下消息:

========= Channel successfully joined ===========

现在您已经在您的机器上运行了测试网络的干净版本,我们可以开始向我们创建的频道添加一个新的org的过程。首先,我们将使用一个脚本将Org3添加到通道中,以确认流程是否正常工作。然后,我们将通过更新通道配置逐步完成添加Org3的过程。

把Org3带到通道

你应该在网络目录下测试。要使用脚本,只需发出以下命令:

cd addOrg3
./addOrg3.sh up

这里的输出很值得一读。您将看到Org3加密材料正在生成,Org3组织定义正在创建,然后通道配置正在更新、签名,然后提交到通道。

如果一切顺利,您将收到以下信息:

========= Finished adding Org3 to your test network! =========

既然我们已经确认可以将Org3添加到我们的频道中,那么我们就可以通过这些步骤来更新脚本在幕后完成的频道配置。


手动将Org3带入通道

如果您刚刚使用addOrg3.sh脚本,则需要关闭网络。以下命令将关闭所有正在运行的组件并删除所有组织的加密材料:

./addOrg3.sh down

网络关闭后,重新启动:

cd ..
./network.sh up createChannel

这将使您的网络恢复到执行addOrg3.sh脚本之前的状态。

现在我们准备手动将Org3添加到通道中。作为第一步,我们需要生成Org3的加密材料。


生成Org3加密材料

在另一个终端中,从test-network切换到addOrg3子目录。

cd addOrg3

首先,我们将为Org3节点创建证书和密钥,以及一个应用程序和管理员用户。因为我们正在更新一个示例通道,所以我们将使用cryptogen工具而不是使用证书颁发机构。以下命令使用cryptogen读取org3-crypto.yaml文件并在新的org3.example.com文件夹中生成Org3加密材料:

../../bin/cryptogen generate --config=org3-crypto.yaml --output="../organizations"

您可以在test-network/organizations/peerOrganizations目录中找到生成的Org3加密材料以及Org1和Org2的证书和密钥。

一旦我们创建了Org3加密材料,我们就可以使用configtxgen工具打印出Org3组织定义。我们将在命令前面告诉工具在当前目录中查找configtx.yaml它需要摄取的文件。

export FABRIC_CFG_PATH=$PWD
../../bin/configtxgen -printOrg Org3MSP > ../organizations/peerOrganizations/org3.example.com/org3.json

上面的命令创建一个JSON文件org3.JSON,并将其写入test-network/organizations/peerOrganizations/org3.example.com文件夹。组织定义包含Org3的策略定义,以及以base64格式编码的三个重要证书:

  • CA根证书,用于建立组织的信任根
  • TLS根证书,gossip协议使用它来标识Org3以进行块分发和服务发现
  • 管理员用户证书(以后将需要它作为Org3的管理员)

我们将通过将此组织定义附加到通道配置来向通道添加Org3。


打开Org3组件

在我们创建了Org3证书材料之后,我们现在可以启动Org3节点。从addOrg3目录发出以下命令:

docker-compose -f docker/docker-compose-org3.yaml up -d

如果命令成功,您将看到Org3节点的创建和一个名为Org3CLI的Fabric tools容器实例:

Creating peer0.org3.example.com ... done
Creating Org3cli                ... done

这个Docker-Compose文件被配置为跨越我们的初始网络,因此Org3节点和Org3CLI可以与测试网络的现有普通节点和排序节点进行解析。我们将使用Org3CLI容器与网络通信,并发出peer命令,将Org3添加到通道中。


准备CLI环境

更新过程使用配置转换器工具configtxlator。这个工具提供了一个独立于SDK的无状态restapi。此外,它还提供了一个CLI工具,可用于简化结构网络中的配置任务。该工具允许在不同的等价数据表示/格式之间进行轻松的转换(在本例中,是protobufs和JSON之间)。此外,该工具可以根据两个通道配置之间的差异计算配置更新交易。

使用以下命令执行到Org3CLI容器:

docker exec -it Org3cli bash

此容器已与organizations文件夹一起安装,使我们能够访问所有组织和排序节点组织的加密材料和TLS证书。我们可以使用环境变量作为Org1、Org2或Org3的管理员来操作Org3CLI容器。首先,我们需要为orderer TLS证书和通道名称设置环境变量:

export ORDERER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
export CHANNEL_NAME=mychannel

检查以确保变量设置正确:

echo $ORDERER_CA && echo $CHANNEL_NAME

注意:如果出于任何原因需要重新启动Org3CLI容器,还需要重新导出两个环境变量:order_CACHANNEL_NAME


获取配置

现在我们有了Org3CLI容器,其中包含两个关键的环境变量ORDERER_CACHANNEL_NAME已导出。让我们去获取通道的最新配置块mychannel

我们之所以必须获取最新版本的配置,是因为通道配置元素是版本控制的。版本控制很重要,有几个原因。它可以防止配置更改被重复或重放(例如,使用旧的CRL恢复到通道配置会带来安全风险)。它还有助于确保并发性(如果你想从你的渠道中删除一个组织,例如,在添加了一个新的组织之后,版本控制将有助于防止你同时删除两个组织,而不仅仅是你想删除的组织)。

因为Org3还不是通道的成员,我们需要作为另一个组织的管理员来获取通道配置。因为Org1是通道的成员,所以Org1管理员有权从排序服务获取通道配置。发出以下命令以作为Org1管理员操作。

# you can issue all of these commands at once

export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=peer0.org1.example.com:7051

我们现在可以发出命令来获取最新的配置块:

peer channel fetch config config_block.pb -o orderer.example.com:7050 -c $CHANNEL_NAME --tls --cafile $ORDERER_CA

此命令将二进制protobuf通道配置块保存到config_block.pb。请注意,名称和文件扩展名的选择是任意的。但是,建议遵循一个既标识所表示对象类型又标识其编码(protobuf或JSON)的约定。

发出peer channel fetch命令时,日志中将显示以下输出:

2017-11-07 17:17:57.383 UTC [channelCmd] readBlock -> DEBU 011 Received block: 2

这告诉我们,mychannel的最新配置块实际上是block 2,而不是genesis块。默认情况下,peer channel fetch config命令返回目标通道的最新配置块,在本例中是第三个块。这是因为测试网络脚本network.sh,在两个单独的通道更新交易中为我们的两个组织(Org1和Org2)定义了锚节点。因此,我们有以下配置顺序:

  • 区块0:genesis区块
  • 区块1:Org1锚节点更新
  • 区块2:Org2锚节点更新

将配置转换为JSON并对其进行精简

现在我们将使用configtxlator工具将这个通道配置块解码为JSON格式(可以由人类读取和修改)。我们还必须去掉所有与我们想要进行的更改无关的头、元数据、创建者签名等。我们通过jq工具实现这一点:

configtxlator proto_decode --input config_block.pb --type common.Block | jq .data.data[0].payload.data.config > config.json

这个命令留给我们一个精简的JSON对象-config.json–这将作为配置更新的基线。

花点时间在您选择的文本编辑器(或浏览器)中打开此文件。即使您完成了本教程,它仍然值得研究,因为它揭示了底层配置结构和其他类型的通道更新。我们将在更新信道配置中更详细地讨论它们。


添加Org3加密材料

无论您尝试进行何种配置更新,到目前为止所采取的步骤都将几乎相同。我们选择在本教程中添加一个org,因为这是您可以尝试的最复杂的通道配置更新之一。

我们将再次使用jq工具将Org3配置定义(Org3.json)附加到通道的application groups字段,并将输出命名为modified_config.json

jq -s '.[0] * {"channel_group":{"groups":{"Application":{"groups": {"Org3MSP":.[1]}}}}}' config.json ./organizations/peerOrganizations/org3.example.com/org3.json > modified_config.json

现在,在Org3 CLI容器中,我们有两个感兴趣的JSON文件-config.jsonmodified_config.json。 初始文件只包含Org1和Org2材料,而“修改”文件包含所有三个ORG。现在只需重新编码这两个JSON文件并计算增量。

首先,将config.json转换回一个名为config.pb的protobuf:

configtxlator proto_encode --input config.json --type common.Config --output config.pb

接下来,将modified_config.json编码为modified_config.pb

configtxlator proto_encode --input modified_config.json --type common.Config --output modified_config.pb

现在使用configtxlator计算这两个配置协议之间的增量。此命令将输出名为org3_update.pb的新protobuf二进制文件:

configtxlator compute_update --channel_id $CHANNEL_NAME --original config.pb --updated modified_config.pb --output org3_update.pb

这个新的原型org3_update.pb包含Org3定义和指向Org1和Org2材料的高级指针。我们可以放弃Org1和Org2的大量MSP材料和修改策略信息,因为这些数据已经存在于通道的genesis块中。因此,我们只需要两个配置之间的增量。

在提交频道更新之前,我们需要执行一些最后的步骤。首先,让我们将这个对象解码为可编辑的JSON格式并将其命名为org3_update.json

configtxlator proto_decode --input org3_update.pb --type common.ConfigUpdate | jq . > org3_update.json

现在,我们有一个解码的更新文件-org3_update.json–我们需要用信封包装信息。这一步将返回我们先前剥离的头字段。我们将把这个文件命名为org3_update_in_envelope.json

echo '{"payload":{"header":{"channel_header":{"channel_id":"'$CHANNEL_NAME'", "type":2}},"data":{"config_update":'$(cat org3_update.json)'}}}' | jq . > org3_update_in_envelope.json

在中使用格式正确的JSON–org3_update_in_envelope.json–我们将最后一次利用configtxlator工具,并将其转换为Fabric所需的成熟protobuf格式。我们将最终更新对象命名为org3_update_in_envelope.pb

configtxlator proto_encode --input org3_update_in_envelope.json --type common.Envelope --output org3_update_in_envelope.pb

签署并提交配置更新

快完成了!

我们现在有了一个protobuf二进制文件-org3_update_in_envelope.pb–在Org3 CLI容器内。但是,在配置写入账本之前,我们需要必需的管理员用户的签名。我们的channel应用程序组的修改策略(mod_policy)设置为默认的“多数”,这意味着我们需要大多数现有的组织管理员对其进行签名。因为我们只有两个组织——Org1和Org2——而这两个组织中的大多数是两个,所以我们需要它们都签字。如果没有这两个签名,排序服务将拒绝未能实现策略的交易。

首先,让我们将这个更新原型签名为Org1。请记住,我们导出了必要的环境变量,以作为Org1管理员操作Org3 CLI容器。因此,下面的peer channel signconfigtx命令将更新签名为Org1。

peer channel signconfigtx -f org3_update_in_envelope.pb

最后一步是切换容器的标识以反映Org2管理用户。我们通过导出特定于Org2 MSP的四个环境变量来实现这一点。

在组织之间切换以签署配置交易(或执行任何其他操作)并不能反映真实世界中的Fabric操作。一个容器永远不会装上整个网络的加密材料。相反,配置更新需要安全地传递到Org2管理员进行检查和批准。

导出Org2环境变量:

# you can issue all of these commands at once

export CORE_PEER_LOCALMSPID="Org2MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=peer0.org2.example.com:9051

最后,我们将发出peer channel update命令。Org2管理员签名将附加到此调用,因此无需再次手动签署协议:

注意:即将对排序服务的更新呼叫将进行一系列系统的签名和策略检查。因此,您可能会发现流式处理和检查排序节点的日志很有用。你可以发出一个docker logs -f orderer.example.com来自Org3 CLI容器外部的终端的命令来显示它们。

发送更新呼叫:

peer channel update -f org3_update_in_envelope.pb -c $CHANNEL_NAME -o orderer.example.com:7050 --tls --cafile $ORDERER_CA

如果您的更新已成功提交,您将看到类似于以下内容的消息:

2020-01-09 21:30:45.791 UTC [channelCmd] update -> INFO 002 Successfully submitted channel update

成功的通道更新调用将向通道上的所有节点返回一个新的块-块3。如果您还记得,块0-2是初始通道配置。块3用作最新的通道配置,现在在通道上定义了Org3。

您可以通过导航到Org3CLI容器外部的终端并发出以下命令来检查peer0.org1.example.com日志:

docker logs -f peer0.org1.example.com

加入Org3通道

此时,通道配置已经更新,包括我们的新组织Org3,这意味着附加到它的节点现在可以加入mychannel

在Org3 CLI容器中,导出以下环境变量以作为Org3管理员操作:

# you can issue all of these commands at once

export CORE_PEER_LOCALMSPID="Org3MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/organizations/peerOrganizations/org3.example.com/users/Admin@org3.example.com/msp
export CORE_PEER_ADDRESS=peer0.org3.example.com:11051

现在我们给排序服务打个电话,询问mychannel的genesis块。作为通道更新成功的结果,排序服务将验证Org3是否可以拉入genesis块并加入通道。如果Org3没有成功地附加到通道配置中,排序服务将拒绝此请求。

注意:同样,您可能会发现流化排序节点的日志以显示签名/验证逻辑和策略检查是很有用的。

使用peer channel fetch命令检索此块:

peer channel fetch 0 mychannel.block -o orderer.example.com:7050 -c $CHANNEL_NAME --tls --cafile $ORDERER_CA

注意,我们传递了一个0来表示我们需要通道账本上的第一个块;genesis块。如果我们简单地传递了peer channel fetch config命令,那么我们就会收到block 3–定义了Org3的更新配置。但是,我们不能从下游区块开始账本–我们必须从第0区块开始。

如果成功,命令将genesis块返回到名为mychannel.block。我们现在可以使用此块将节点连接到通道。发出peer channel join命令并通过genesis块将Org3节点连接到通道:

peer channel join -b mychannel.block

配置领导人选举

注意:本节作为一般参考,用于在初始通道配置完成后将组织添加到网络时了解领导人选举设置。此示例默认为动态领导人选举,这是为网络中的所有节点设置的。

新加入的节点使用genesis块引导,该块不包含要在通道配置更新中添加的组织的信息。因此,新的节点不能利用gossip,因为他们无法验证其他节点从他们自己的组织转发的块,直到他们得到将组织添加到通道的配置交易。因此,新添加的节点必须具有以下配置之一,以便从排序服务接收块:

  1. 要使用静态领导模式,请将节点配置为组织领导者:
CORE_PEER_GOSSIP_USELEADERELECTION=false
CORE_PEER_GOSSIP_ORGLEADER=true

注意:对于添加到通道的所有新节点,此配置必须相同。

  1. 要使用动态领导人选举,请将节点配置为使用领导人选举:
CORE_PEER_GOSSIP_USELEADERELECTION=true
CORE_PEER_GOSSIP_ORGLEADER=false

注意:因为新添加的组织的节点将无法形成成员资格视图,此选项将类似于静态配置,因为每个节点都将开始声明自己是领导者。但是,一旦他们使用将组织添加到通道的配置交易进行更新,组织将只有一个活动的领导。因此,如果您最终希望组织的同行利用领导人选举,建议使用此选项。


安装、定义和调用链码

我们可以通过在通道上安装和调用链码来确认Org3是mychannel的成员。如果现有的通道成员已经向通道提交了链码定义,则新组织可以通过批准链码定义开始使用链码。

注意:这些说明使用v2.0版本中引入的Fabric链码生命周期。如果您想使用以前的生命周期来安装和实例化一个链码,请访问v1.4版本的Adding an org to a channel教程

在我们将链码安装为Org3之前,我们可以使用./network.sh脚本在通道上部署Fabcar链码。在Org3 CLI容器外打开一个新的终端并导航到test-network目录。然后可以使用test-network脚本来部署Fabcar链码:

cd fabric-samples/test-network
./network.sh deployCC

该脚本将在Org1和Org2节点上安装Fabcar链码,批准Org1和Org2的链码定义,然后将链码定义提交给通道。一旦将链码定义提交给通道,Fabcar链码将被初始化并被调用以将初始数据放在账本上。下面的命令假设我们仍在使用mychannel通道。

在链接码被部署之后,我们可以使用以下步骤来作为Org3使用invoke Fabcar链码。这些步骤可以从test-network目录完成,而不必执行到Org3 CLI容器中。在终端中复制并粘贴以下环境变量,以便作为Org3管理员与网络交互:

export PATH=${PWD}/../bin:${PWD}:$PATH
export FABRIC_CFG_PATH=$PWD/../config/
export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_LOCALMSPID="Org3MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org3.example.com/users/Admin@org3.example.com/msp
export CORE_PEER_ADDRESS=localhost:11051

第一步是打包Fabcar链码:

peer lifecycle chaincode package fabcar.tar.gz --path ../chaincode/fabcar/go/ --lang golang --label fabcar_1

此命令将创建一个名为fabcar.tar.gz的链码包,我们可以把它安装在Org3节点上。如果通道正在运行用Java或Node.js. 发出以下命令来安装chaincode包peer0.org3.example.com

peer lifecycle chaincode install fabcar.tar.gz

下一步是批准Fabcar的链码定义为Org3。Org3需要批准与Org1和Org2批准并提交给通道的相同定义。为了调用链码,Org3需要在链码定义中包含包标识符。您可以通过查询节点来找到包标识符:

peer lifecycle chaincode queryinstalled

您将看到类似于以下内容的输出:

Get installed chaincodes on peer:
Package ID: fabcar_1:25f28c212da84a8eca44d14cf12549d8f7b674a0d8288245561246fa90f7ab03, Label: fabcar_1

我们将在将来的命令中需要包ID,所以让我们继续并将其保存为环境变量。将peer lifecycle chaincode queryinstalled命令返回的包ID粘贴到下面的命令中。对于所有用户,包ID可能不相同,因此您需要使用从控制台返回的包ID来完成此步骤。

export CC_PACKAGE_ID=fabcar_1:25f28c212da84a8eca44d14cf12549d8f7b674a0d8288245561246fa90f7ab03

使用以下命令批准Org3的Fabcar链码定义:

# use the --package-id flag to provide the package identifier
# use the --init-required flag to request the ``Init`` function be invoked to initialize the chaincode
peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name fabcar --version 1 --init-required --package-id $CC_PACKAGE_ID --sequence 1 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem

您可以使用peer lifecycle chaincode querycommitted命令检查您批准的链码定义是否已提交到通道。

# use the --name flag to select the chaincode whose definition you want to query
peer lifecycle chaincode querycommitted --channelID mychannel --name fabcar --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem

成功的命令将返回有关已提交定义的信息:

Committed chaincode definition for chaincode 'fabcar' on channel 'mychannel':
Version: 1, Sequence: 1, Endorsement Plugin: escc, Validation Plugin: vscc, Approvals: [Org1MSP: true, Org2MSP: true, Org3MSP: true]

Org3可以在批准提交给通道的链码定义后使用Fabcar链码。链码定义使用默认的背书策略,这要求通道上的大多数组织认可一个交易。这意味着,如果在通道中添加或删除组织,则背书策略将自动更新。我们之前需要Org1和Org2的支持(2/2)。现在我们需要Org1、Org2和Org3(三分之二)的两个组织的背书。

您可以查询链码以确保它已经在Org3节点上启动。请注意,您可能需要等待chaincode容器启动。

peer chaincode query -C mychannel -n fabcar -c '{"Args":["queryAllCars"]}'

您应该看到作为响应添加到账本中的汽车的初始列表。

现在,调用链码将一辆新车添加到账本中。在下面的命令中,我们以Org1和Org3中的节点为目标收集足够数量的背书。

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 fabcar --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:11051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls/ca.crt -c '{"function":"createCar","Args":["CAR11","Honda","Accord","Black","Tom"]}'

我们可以再次查询以查看我们的账本上的新车“CAR11”:

peer chaincode query -C mychannel -n fabcar -c '{"Args":["queryCar","CAR11"]}'

结论

通道配置更新过程确实涉及很多,但各个步骤都有一个逻辑方法。最后一步是形成一个以protobuf二进制格式表示的delta交易对象,然后获取所需数量的管理签名,以便通道配置更新交易满足通道的修改策略。

configtxlatorjq工具以及peer channel命令为我们提供了完成此任务的功能。


更新通道配置以包含Org3锚节点(可选)

由于Org1和Org2在通道配置中定义了锚节点,因此Org3节点能够与Org1和Org2节点建立gossip连接。同样,像Org3这样的新添加的组织也应该在通道配置中定义它们的锚节点,这样来自其他组织的任何新的节点都可以直接发现Org3对等点。在本节中,我们将更新通道配置以定义Org3锚节点。这个过程将与之前的配置更新类似,因此这次我们将加快速度。

如果没有打开它,请将exec返回到Org3CLI容器中:

docker exec -it Org3cli bash

导出$order_CA 和 $CHANNEL_NAME变量(如果尚未设置):

export ORDERER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
export CHANNEL_NAME=mychannel

如前所述,我们将获取最新的通道配置以开始。在Org3 CLI容器中,使用peer channel fetch命令获取通道的最新配置块。

peer channel fetch config config_block.pb -o orderer.example.com:7050 -c $CHANNEL_NAME --tls --cafile $ORDERER_CA

在获取config块之后,我们需要将其转换为JSON格式。为此,我们将使用configtxlator工具,正如前面在向通道添加Org3时所做的那样。在转换它时,我们需要删除所有的头、元数据和签名,这些都不需要通过使用jq工具来更新Org3以包含一个锚节点。在我们继续更新通道配置之前,稍后将重新合并这些信息。

configtxlator proto_decode --input config_block.pb --type common.Block | jq .data.data[0].payload.data.config > config.json

这个config.json是表示我们将要更新的最新通道配置的现已修剪的JSON。

再次使用jq工具,我们将使用我们想要添加的Org3锚节点来更新配置JSON。

jq '.channel_group.groups.Application.groups.Org3MSP.values += {"AnchorPeers":{"mod_policy": "Admins","value":{"anchor_peers": [{"host": "peer0.org3.example.com","port": 11051}]},"version": "0"}}' config.json > modified_anchor_config.json

我们现在有两个JSON文件,一个用于当前通道配置,config.json,另一个用于所需通道配置modified_anchor_config.json。 接下来,我们将每一个转换回protobuf格式,并计算两者之间的增量。

config.json转换回protobuf格式config.pb

configtxlator proto_encode --input config.json --type common.Config --output config.pb

modified_anchor_config.json转换回protobuf格式modified_anchor_config.pb

configtxlator proto_encode --input modified_anchor_config.json --type common.Config --output modified_anchor_config.pb

计算两个protobuf格式配置之间的增量。

configtxlator compute_update --channel_id $CHANNEL_NAME --original config.pb --updated modified_anchor_config.pb --output anchor_update.pb

既然我们已经获得了所需的频道更新,我们必须将其包装在信封消息中,以便能够正确读取。为此,我们必须首先将protobuf转换回可以包装的JSON。

我们将再次使用configtxlator命令将anchor_update.pb转换为anchor_update.json

configtxlator proto_decode --input anchor_update.pb --type common.ConfigUpdate | jq . > anchor_update.json

接下来,我们将把更新包在一个信封消息中,恢复之前剥离的头,并将其输出到anchor_update_in_envelope.json

echo '{"payload":{"header":{"channel_header":{"channel_id":"'$CHANNEL_NAME'", "type":2}},"data":{"config_update":'$(cat anchor_update.json)'}}}' | jq . > anchor_update_in_envelope.json

既然我们已经重新合并了信封,我们需要将其转换为protobuf,这样它就可以正确签名并提交给排序方进行更新。

configtxlator proto_encode --input anchor_update_in_envelope.json --type common.Envelope --output anchor_update_in_envelope.pb

现在更新已经被正确格式化,现在是时候签署并提交它了。因为这只是对Org3的更新,所以我们只需要Org3在更新上签字。运行以下命令以确保我们以Org3管理员的身份运行:

# you can issue all of these commands at once

export CORE_PEER_LOCALMSPID="Org3MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/organizations/peerOrganizations/org3.example.com/users/Admin@org3.example.com/msp
export CORE_PEER_ADDRESS=peer0.org3.example.com:11051

我们现在可以使用peer channel update命令在将更新提交给排序者之前以Org3管理员的身份对更新进行签名。

peer channel update -f anchor_update_in_envelope.pb -c $CHANNEL_NAME -o orderer.example.com:7050 --tls --cafile $ORDERER_CA

排序程序接收配置更新请求,并使用更新的配置剪切一个块。当节点接收到块时,它们将处理配置更新。

检查其中一个节点的日志。在处理来自新块的配置交易时,您将看到gossip使用Org3的新锚节点重新建立连接。这证明配置更新已成功应用!

docker logs -f peer0.org1.example.com
2019-06-12 17:08:57.924 UTC [gossip.gossip] learnAnchorPeers -> INFO 89a Learning about the configured anchor peers of Org1MSP for channel mychannel : [{peer0.org1.example.com 7051}]
2019-06-12 17:08:57.926 UTC [gossip.gossip] learnAnchorPeers -> INFO 89b Learning about the configured anchor peers of Org2MSP for channel mychannel : [{peer0.org2.example.com 9051}]
2019-06-12 17:08:57.926 UTC [gossip.gossip] learnAnchorPeers -> INFO 89c Learning about the configured anchor peers of Org3MSP for channel mychannel : [{peer0.org3.example.com 11051}]

恭喜您,您现在已经进行了两次配置更新-一次是将Org3添加到通道中,另一次是为Org3定义一个锚节点。


更新通道配置

受众:网络管理员、节点管理员

什么是信道配置?

像许多复杂系统一样,超级账本结构网络由结构和许多相关过程组成。

  • 结构:包括用户(如管理员)、组织、普通节点、排序节点、CA、智能合约和应用程序。
  • 过程:这些结构相互作用的方式。其中最重要的是策略,即控制哪些用户可以在什么条件下做什么的规则。

识别区块链网络结构和控制结构如何交互的过程的信息包含在通道配置中。这些配置由通道的成员共同决定,并包含在提交给通道账本的块中。通道配置可以使用名为configtxgen的工具构建,该工具使用configtx.yaml文件作为输入。您可以在这里查看一个configtx.yaml文件示例

由于配置包含在块中(第一块称为genesis块,最新块表示通道的当前配置),因此更新通道配置的过程(例如,通过添加成员来改变结构,或通过修改通道策略进行处理)称为配置更新交易

在生产网络中,这些配置更新交易通常由单个通道管理员在带外讨论后提出,就像通道的初始配置将由通道的初始成员在带外决定一样。

在本主题中,我们将:

  • 显示应用程序通道的完整示例配置。
  • 讨论许多可编辑的通道参数。
  • 显示更新通道配置的过程,包括将配置拉入、转换和限定为人类可以读取的内容所需的命令。
  • 讨论可用于编辑通道配置的方法。
  • 显示用于重新格式化配置的过程,并获取批准配置所需的签名。

可更新的通道参数

通道是高度可配置的,但不是无限的。一旦指定了某个通道的某些内容(例如,通道的名称),就无法更改这些内容。更改我们将在本主题中讨论的参数之一需要满足通道配置中指定的相关策略。

在本节中,我们将查看一个示例通道配置,并显示可以更新的配置参数。

样本通道配置

注意:为了简单起见,这里只展示了一个应用程序通道配置。排序程序系统通道的配置与应用程序通道的配置非常相似,但不完全相同。但是,基本的规则和结构与拉取和编辑配置的命令相同,您可以在我们关于更新通道功能级别的主题中看到。

请注意,这是应用程序通道的配置,而不是排序程序系统通道。

{
  "channel_group": {
    "groups": {
      "Application": {
        "groups": {
          "Org1MSP": {
            "groups": {},
            "mod_policy": "Admins",
            "policies": {
              "Admins": {
                "mod_policy": "Admins",
                "policy": {
                  "type": 1,
                  "value": {
                    "identities": [
                      {
                        "principal": {
                          "msp_identifier": "Org1MSP",
                          "role": "ADMIN"
                        },
                        "principal_classification": "ROLE"
                      }
                    ],
                    "rule": {
                      "n_out_of": {
                        "n": 1,
                        "rules": [
                          {
                            "signed_by": 0
                          }
                        ]
                      }
                    },
                    "version": 0
                  }
                },
                "version": "0"
              },
              "Endorsement": {
                "mod_policy": "Admins",
                "policy": {
                  "type": 1,
                  "value": {
                    "identities": [
                      {
                        "principal": {
                          "msp_identifier": "Org1MSP",
                          "role": "PEER"
                        },
                        "principal_classification": "ROLE"
                      }
                    ],
                    "rule": {
                      "n_out_of": {
                        "n": 1,
                        "rules": [
                          {
                            "signed_by": 0
                          }
                        ]
                      }
                    },
                    "version": 0
                  }
                },
                "version": "0"
              },
              "Readers": {
                "mod_policy": "Admins",
                "policy": {
                  "type": 1,
                  "value": {
                    "identities": [
                      {
                        "principal": {
                          "msp_identifier": "Org1MSP",
                          "role": "ADMIN"
                        },
                        "principal_classification": "ROLE"
                      },
                      {
                        "principal": {
                          "msp_identifier": "Org1MSP",
                          "role": "PEER"
                        },
                        "principal_classification": "ROLE"
                      },
                      {
                        "principal": {
                          "msp_identifier": "Org1MSP",
                          "role": "CLIENT"
                        },
                        "principal_classification": "ROLE"
                      }
                    ],
                    "rule": {
                      "n_out_of": {
                        "n": 1,
                        "rules": [
                          {
                            "signed_by": 0
                          },
                          {
                            "signed_by": 1
                          },
                          {
                            "signed_by": 2
                          }
                        ]
                      }
                    },
                    "version": 0
                  }
                },
                "version": "0"
              },
              "Writers": {
                "mod_policy": "Admins",
                "policy": {
                  "type": 1,
                  "value": {
                    "identities": [
                      {
                        "principal": {
                          "msp_identifier": "Org1MSP",
                          "role": "ADMIN"
                        },
                        "principal_classification": "ROLE"
                      },
                      {
                        "principal": {
                          "msp_identifier": "Org1MSP",
                          "role": "CLIENT"
                        },
                        "principal_classification": "ROLE"
                      }
                    ],
                    "rule": {
                      "n_out_of": {
                        "n": 1,
                        "rules": [
                          {
                            "signed_by": 0
                          },
                          {
                            "signed_by": 1
                          }
                        ]
                      }
                    },
                    "version": 0
                  }
                },
                "version": "0"
              }
            },
            "values": {
              "AnchorPeers": {
                "mod_policy": "Admins",
                "value": {
                  "anchor_peers": [
                    {
                      "host": "peer0.org1.example.com",
                      "port": 7051
                    }
                  ]
                },
                "version": "0"
              },
              "MSP": {
                "mod_policy": "Admins",
                "value": {
                  "config": {
                    "admins": [],
                    "crypto_config": {
                      "identity_identifier_hash_function": "SHA256",
                      "signature_hash_family": "SHA2"
                    },
                    "fabric_node_ous": {
                      "admin_ou_identifier": {
                        "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNKekNDQWMyZ0F3SUJBZ0lVYWVSeWNkQytlR1lUTUNyWTg2UFVXUEdzQUw0d0NnWUlLb1pJemowRUF3SXcKY0RFTE1Ba0dBMVVFQmhNQ1ZWTXhGekFWQmdOVkJBZ1REazV2Y25Sb0lFTmhjbTlzYVc1aE1ROHdEUVlEVlFRSApFd1pFZFhKb1lXMHhHVEFYQmdOVkJBb1RFRzl5WnpFdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekV1WlhoaGJYQnNaUzVqYjIwd0hoY05NakF3TXpJME1qQXhPREF3V2hjTk16VXdNekl4TWpBeE9EQXcKV2pCd01Rc3dDUVlEVlFRR0V3SlZVekVYTUJVR0ExVUVDQk1PVG05eWRHZ2dRMkZ5YjJ4cGJtRXhEekFOQmdOVgpCQWNUQmtSMWNtaGhiVEVaTUJjR0ExVUVDaE1RYjNKbk1TNWxlR0Z0Y0d4bExtTnZiVEVjTUJvR0ExVUVBeE1UClkyRXViM0puTVM1bGVHRnRjR3hsTG1OdmJUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJLWXIKSmtqcEhjRkcxMVZlU200emxwSmNCZEtZVjc3SEgvdzI0V09sZnphYWZWK3VaaEZ2YTFhQm9aaGx5RloyMGRWeApwMkRxb09BblZ4MzZ1V3o2SXl1alJUQkRNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVNCZ05WSFJNQkFmOEVDREFHCkFRSC9BZ0VCTUIwR0ExVWREZ1FXQkJTcHpDQWdPaGRuSkE3VVpxUWlFSVFXSFpnYXZEQUtCZ2dxaGtqT1BRUUQKQWdOSUFEQkZBaUVBbEZtYWdIQkJoblFUd3dDOXBQRTRGbFY2SlhIbTdnQ1JyWUxUbVgvc0VySUNJRUhLZG51KwpIWDgrVTh1ZkFKbTdrL1laZEtVVnlWS2E3bGREUjlWajNveTIKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=",
                        "organizational_unit_identifier": "admin"
                      },
                      "client_ou_identifier": {
                        "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNKekNDQWMyZ0F3SUJBZ0lVYWVSeWNkQytlR1lUTUNyWTg2UFVXUEdzQUw0d0NnWUlLb1pJemowRUF3SXcKY0RFTE1Ba0dBMVVFQmhNQ1ZWTXhGekFWQmdOVkJBZ1REazV2Y25Sb0lFTmhjbTlzYVc1aE1ROHdEUVlEVlFRSApFd1pFZFhKb1lXMHhHVEFYQmdOVkJBb1RFRzl5WnpFdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekV1WlhoaGJYQnNaUzVqYjIwd0hoY05NakF3TXpJME1qQXhPREF3V2hjTk16VXdNekl4TWpBeE9EQXcKV2pCd01Rc3dDUVlEVlFRR0V3SlZVekVYTUJVR0ExVUVDQk1PVG05eWRHZ2dRMkZ5YjJ4cGJtRXhEekFOQmdOVgpCQWNUQmtSMWNtaGhiVEVaTUJjR0ExVUVDaE1RYjNKbk1TNWxlR0Z0Y0d4bExtTnZiVEVjTUJvR0ExVUVBeE1UClkyRXViM0puTVM1bGVHRnRjR3hsTG1OdmJUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJLWXIKSmtqcEhjRkcxMVZlU200emxwSmNCZEtZVjc3SEgvdzI0V09sZnphYWZWK3VaaEZ2YTFhQm9aaGx5RloyMGRWeApwMkRxb09BblZ4MzZ1V3o2SXl1alJUQkRNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVNCZ05WSFJNQkFmOEVDREFHCkFRSC9BZ0VCTUIwR0ExVWREZ1FXQkJTcHpDQWdPaGRuSkE3VVpxUWlFSVFXSFpnYXZEQUtCZ2dxaGtqT1BRUUQKQWdOSUFEQkZBaUVBbEZtYWdIQkJoblFUd3dDOXBQRTRGbFY2SlhIbTdnQ1JyWUxUbVgvc0VySUNJRUhLZG51KwpIWDgrVTh1ZkFKbTdrL1laZEtVVnlWS2E3bGREUjlWajNveTIKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=",
                        "organizational_unit_identifier": "client"
                      },
                      "enable": true,
                      "orderer_ou_identifier": {
                        "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNKekNDQWMyZ0F3SUJBZ0lVYWVSeWNkQytlR1lUTUNyWTg2UFVXUEdzQUw0d0NnWUlLb1pJemowRUF3SXcKY0RFTE1Ba0dBMVVFQmhNQ1ZWTXhGekFWQmdOVkJBZ1REazV2Y25Sb0lFTmhjbTlzYVc1aE1ROHdEUVlEVlFRSApFd1pFZFhKb1lXMHhHVEFYQmdOVkJBb1RFRzl5WnpFdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekV1WlhoaGJYQnNaUzVqYjIwd0hoY05NakF3TXpJME1qQXhPREF3V2hjTk16VXdNekl4TWpBeE9EQXcKV2pCd01Rc3dDUVlEVlFRR0V3SlZVekVYTUJVR0ExVUVDQk1PVG05eWRHZ2dRMkZ5YjJ4cGJtRXhEekFOQmdOVgpCQWNUQmtSMWNtaGhiVEVaTUJjR0ExVUVDaE1RYjNKbk1TNWxlR0Z0Y0d4bExtTnZiVEVjTUJvR0ExVUVBeE1UClkyRXViM0puTVM1bGVHRnRjR3hsTG1OdmJUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJLWXIKSmtqcEhjRkcxMVZlU200emxwSmNCZEtZVjc3SEgvdzI0V09sZnphYWZWK3VaaEZ2YTFhQm9aaGx5RloyMGRWeApwMkRxb09BblZ4MzZ1V3o2SXl1alJUQkRNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVNCZ05WSFJNQkFmOEVDREFHCkFRSC9BZ0VCTUIwR0ExVWREZ1FXQkJTcHpDQWdPaGRuSkE3VVpxUWlFSVFXSFpnYXZEQUtCZ2dxaGtqT1BRUUQKQWdOSUFEQkZBaUVBbEZtYWdIQkJoblFUd3dDOXBQRTRGbFY2SlhIbTdnQ1JyWUxUbVgvc0VySUNJRUhLZG51KwpIWDgrVTh1ZkFKbTdrL1laZEtVVnlWS2E3bGREUjlWajNveTIKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=",
                        "organizational_unit_identifier": "orderer"
                      },
                      "peer_ou_identifier": {
                        "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNKekNDQWMyZ0F3SUJBZ0lVYWVSeWNkQytlR1lUTUNyWTg2UFVXUEdzQUw0d0NnWUlLb1pJemowRUF3SXcKY0RFTE1Ba0dBMVVFQmhNQ1ZWTXhGekFWQmdOVkJBZ1REazV2Y25Sb0lFTmhjbTlzYVc1aE1ROHdEUVlEVlFRSApFd1pFZFhKb1lXMHhHVEFYQmdOVkJBb1RFRzl5WnpFdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekV1WlhoaGJYQnNaUzVqYjIwd0hoY05NakF3TXpJME1qQXhPREF3V2hjTk16VXdNekl4TWpBeE9EQXcKV2pCd01Rc3dDUVlEVlFRR0V3SlZVekVYTUJVR0ExVUVDQk1PVG05eWRHZ2dRMkZ5YjJ4cGJtRXhEekFOQmdOVgpCQWNUQmtSMWNtaGhiVEVaTUJjR0ExVUVDaE1RYjNKbk1TNWxlR0Z0Y0d4bExtTnZiVEVjTUJvR0ExVUVBeE1UClkyRXViM0puTVM1bGVHRnRjR3hsTG1OdmJUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJLWXIKSmtqcEhjRkcxMVZlU200emxwSmNCZEtZVjc3SEgvdzI0V09sZnphYWZWK3VaaEZ2YTFhQm9aaGx5RloyMGRWeApwMkRxb09BblZ4MzZ1V3o2SXl1alJUQkRNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVNCZ05WSFJNQkFmOEVDREFHCkFRSC9BZ0VCTUIwR0ExVWREZ1FXQkJTcHpDQWdPaGRuSkE3VVpxUWlFSVFXSFpnYXZEQUtCZ2dxaGtqT1BRUUQKQWdOSUFEQkZBaUVBbEZtYWdIQkJoblFUd3dDOXBQRTRGbFY2SlhIbTdnQ1JyWUxUbVgvc0VySUNJRUhLZG51KwpIWDgrVTh1ZkFKbTdrL1laZEtVVnlWS2E3bGREUjlWajNveTIKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=",
                        "organizational_unit_identifier": "peer"
                      }
                    },
                    "intermediate_certs": [],
                    "name": "Org1MSP",
                    "organizational_unit_identifiers": [],
                    "revocation_list": [],
                    "root_certs": [
                      "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNKekNDQWMyZ0F3SUJBZ0lVYWVSeWNkQytlR1lUTUNyWTg2UFVXUEdzQUw0d0NnWUlLb1pJemowRUF3SXcKY0RFTE1Ba0dBMVVFQmhNQ1ZWTXhGekFWQmdOVkJBZ1REazV2Y25Sb0lFTmhjbTlzYVc1aE1ROHdEUVlEVlFRSApFd1pFZFhKb1lXMHhHVEFYQmdOVkJBb1RFRzl5WnpFdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekV1WlhoaGJYQnNaUzVqYjIwd0hoY05NakF3TXpJME1qQXhPREF3V2hjTk16VXdNekl4TWpBeE9EQXcKV2pCd01Rc3dDUVlEVlFRR0V3SlZVekVYTUJVR0ExVUVDQk1PVG05eWRHZ2dRMkZ5YjJ4cGJtRXhEekFOQmdOVgpCQWNUQmtSMWNtaGhiVEVaTUJjR0ExVUVDaE1RYjNKbk1TNWxlR0Z0Y0d4bExtTnZiVEVjTUJvR0ExVUVBeE1UClkyRXViM0puTVM1bGVHRnRjR3hsTG1OdmJUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJLWXIKSmtqcEhjRkcxMVZlU200emxwSmNCZEtZVjc3SEgvdzI0V09sZnphYWZWK3VaaEZ2YTFhQm9aaGx5RloyMGRWeApwMkRxb09BblZ4MzZ1V3o2SXl1alJUQkRNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVNCZ05WSFJNQkFmOEVDREFHCkFRSC9BZ0VCTUIwR0ExVWREZ1FXQkJTcHpDQWdPaGRuSkE3VVpxUWlFSVFXSFpnYXZEQUtCZ2dxaGtqT1BRUUQKQWdOSUFEQkZBaUVBbEZtYWdIQkJoblFUd3dDOXBQRTRGbFY2SlhIbTdnQ1JyWUxUbVgvc0VySUNJRUhLZG51KwpIWDgrVTh1ZkFKbTdrL1laZEtVVnlWS2E3bGREUjlWajNveTIKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
                    ],
                    "signing_identity": null,
                    "tls_intermediate_certs": [],
                    "tls_root_certs": [
                      "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNKekNDQWMyZ0F3SUJBZ0lVYWVSeWNkQytlR1lUTUNyWTg2UFVXUEdzQUw0d0NnWUlLb1pJemowRUF3SXcKY0RFTE1Ba0dBMVVFQmhNQ1ZWTXhGekFWQmdOVkJBZ1REazV2Y25Sb0lFTmhjbTlzYVc1aE1ROHdEUVlEVlFRSApFd1pFZFhKb1lXMHhHVEFYQmdOVkJBb1RFRzl5WnpFdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekV1WlhoaGJYQnNaUzVqYjIwd0hoY05NakF3TXpJME1qQXhPREF3V2hjTk16VXdNekl4TWpBeE9EQXcKV2pCd01Rc3dDUVlEVlFRR0V3SlZVekVYTUJVR0ExVUVDQk1PVG05eWRHZ2dRMkZ5YjJ4cGJtRXhEekFOQmdOVgpCQWNUQmtSMWNtaGhiVEVaTUJjR0ExVUVDaE1RYjNKbk1TNWxlR0Z0Y0d4bExtTnZiVEVjTUJvR0ExVUVBeE1UClkyRXViM0puTVM1bGVHRnRjR3hsTG1OdmJUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJLWXIKSmtqcEhjRkcxMVZlU200emxwSmNCZEtZVjc3SEgvdzI0V09sZnphYWZWK3VaaEZ2YTFhQm9aaGx5RloyMGRWeApwMkRxb09BblZ4MzZ1V3o2SXl1alJUQkRNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVNCZ05WSFJNQkFmOEVDREFHCkFRSC9BZ0VCTUIwR0ExVWREZ1FXQkJTcHpDQWdPaGRuSkE3VVpxUWlFSVFXSFpnYXZEQUtCZ2dxaGtqT1BRUUQKQWdOSUFEQkZBaUVBbEZtYWdIQkJoblFUd3dDOXBQRTRGbFY2SlhIbTdnQ1JyWUxUbVgvc0VySUNJRUhLZG51KwpIWDgrVTh1ZkFKbTdrL1laZEtVVnlWS2E3bGREUjlWajNveTIKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
                    ]
                  },
                  "type": 0
                },
                "version": "0"
              }
            },
            "version": "1"
          },
          "Org2MSP": {
            "groups": {},
            "mod_policy": "Admins",
            "policies": {
              "Admins": {
                "mod_policy": "Admins",
                "policy": {
                  "type": 1,
                  "value": {
                    "identities": [
                      {
                        "principal": {
                          "msp_identifier": "Org2MSP",
                          "role": "ADMIN"
                        },
                        "principal_classification": "ROLE"
                      }
                    ],
                    "rule": {
                      "n_out_of": {
                        "n": 1,
                        "rules": [
                          {
                            "signed_by": 0
                          }
                        ]
                      }
                    },
                    "version": 0
                  }
                },
                "version": "0"
              },
              "Endorsement": {
                "mod_policy": "Admins",
                "policy": {
                  "type": 1,
                  "value": {
                    "identities": [
                      {
                        "principal": {
                          "msp_identifier": "Org2MSP",
                          "role": "PEER"
                        },
                        "principal_classification": "ROLE"
                      }
                    ],
                    "rule": {
                      "n_out_of": {
                        "n": 1,
                        "rules": [
                          {
                            "signed_by": 0
                          }
                        ]
                      }
                    },
                    "version": 0
                  }
                },
                "version": "0"
              },
              "Readers": {
                "mod_policy": "Admins",
                "policy": {
                  "type": 1,
                  "value": {
                    "identities": [
                      {
                        "principal": {
                          "msp_identifier": "Org2MSP",
                          "role": "ADMIN"
                        },
                        "principal_classification": "ROLE"
                      },
                      {
                        "principal": {
                          "msp_identifier": "Org2MSP",
                          "role": "PEER"
                        },
                        "principal_classification": "ROLE"
                      },
                      {
                        "principal": {
                          "msp_identifier": "Org2MSP",
                          "role": "CLIENT"
                        },
                        "principal_classification": "ROLE"
                      }
                    ],
                    "rule": {
                      "n_out_of": {
                        "n": 1,
                        "rules": [
                          {
                            "signed_by": 0
                          },
                          {
                            "signed_by": 1
                          },
                          {
                            "signed_by": 2
                          }
                        ]
                      }
                    },
                    "version": 0
                  }
                },
                "version": "0"
              },
              "Writers": {
                "mod_policy": "Admins",
                "policy": {
                  "type": 1,
                  "value": {
                    "identities": [
                      {
                        "principal": {
                          "msp_identifier": "Org2MSP",
                          "role": "ADMIN"
                        },
                        "principal_classification": "ROLE"
                      },
                      {
                        "principal": {
                          "msp_identifier": "Org2MSP",
                          "role": "CLIENT"
                        },
                        "principal_classification": "ROLE"
                      }
                    ],
                    "rule": {
                      "n_out_of": {
                        "n": 1,
                        "rules": [
                          {
                            "signed_by": 0
                          },
                          {
                            "signed_by": 1
                          }
                        ]
                      }
                    },
                    "version": 0
                  }
                },
                "version": "0"
              }
            },
            "values": {
              "AnchorPeers": {
                "mod_policy": "Admins",
                "value": {
                  "anchor_peers": [
                    {
                      "host": "peer0.org2.example.com",
                      "port": 9051
                    }
                  ]
                },
                "version": "0"
              },
              "MSP": {
                "mod_policy": "Admins",
                "value": {
                  "config": {
                    "admins": [],
                    "crypto_config": {
                      "identity_identifier_hash_function": "SHA256",
                      "signature_hash_family": "SHA2"
                    },
                    "fabric_node_ous": {
                      "admin_ou_identifier": {
                        "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNIakNDQWNXZ0F3SUJBZ0lVQVFkb1B0S0E0bEk2a0RrMituYzk5NzNhSC9Vd0NnWUlLb1pJemowRUF3SXcKYkRFTE1Ba0dBMVVFQmhNQ1ZVc3hFakFRQmdOVkJBZ1RDVWhoYlhCemFHbHlaVEVRTUE0R0ExVUVCeE1IU0hWeQpjMnhsZVRFWk1CY0dBMVVFQ2hNUWIzSm5NaTVsZUdGdGNHeGxMbU52YlRFY01Cb0dBMVVFQXhNVFkyRXViM0puCk1pNWxlR0Z0Y0d4bExtTnZiVEFlRncweU1EQXpNalF5TURFNE1EQmFGdzB6TlRBek1qRXlNREU0TURCYU1Hd3gKQ3pBSkJnTlZCQVlUQWxWTE1SSXdFQVlEVlFRSUV3bElZVzF3YzJocGNtVXhFREFPQmdOVkJBY1RCMGgxY25OcwpaWGt4R1RBWEJnTlZCQW9URUc5eVp6SXVaWGhoYlhCc1pTNWpiMjB4SERBYUJnTlZCQU1URTJOaExtOXlaekl1ClpYaGhiWEJzWlM1amIyMHdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVFk3VGJqQzdYSHNheC8Kem1yVk1nWnpmODBlb3JFbTNIdis2ZnRqMFgzd2cxdGZVM3hyWWxXZVJwR0JGeFQzNnJmVkdLLzhUQWJ2cnRuZgpUQ1hKak93a28wVXdRekFPQmdOVkhROEJBZjhFQkFNQ0FRWXdFZ1lEVlIwVEFRSC9CQWd3QmdFQi93SUJBVEFkCkJnTlZIUTRFRmdRVWJJNkV4dVRZSEpjczRvNEl5dXZWOVFRa1lGZ3dDZ1lJS29aSXpqMEVBd0lEUndBd1JBSWcKWndjdElBNmdoSlFCZmpDRXdRK1NmYi9iemdsQlV4b0g3ZHVtOUJrUjFkd0NJQlRqcEZkWlcyS2UzSVBMS1h2aApERmQvVmMrcloyMksyeVdKL1BIYXZWWmkKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=",
                        "organizational_unit_identifier": "admin"
                      },
                      "client_ou_identifier": {
                        "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNIakNDQWNXZ0F3SUJBZ0lVQVFkb1B0S0E0bEk2a0RrMituYzk5NzNhSC9Vd0NnWUlLb1pJemowRUF3SXcKYkRFTE1Ba0dBMVVFQmhNQ1ZVc3hFakFRQmdOVkJBZ1RDVWhoYlhCemFHbHlaVEVRTUE0R0ExVUVCeE1IU0hWeQpjMnhsZVRFWk1CY0dBMVVFQ2hNUWIzSm5NaTVsZUdGdGNHeGxMbU52YlRFY01Cb0dBMVVFQXhNVFkyRXViM0puCk1pNWxlR0Z0Y0d4bExtTnZiVEFlRncweU1EQXpNalF5TURFNE1EQmFGdzB6TlRBek1qRXlNREU0TURCYU1Hd3gKQ3pBSkJnTlZCQVlUQWxWTE1SSXdFQVlEVlFRSUV3bElZVzF3YzJocGNtVXhFREFPQmdOVkJBY1RCMGgxY25OcwpaWGt4R1RBWEJnTlZCQW9URUc5eVp6SXVaWGhoYlhCc1pTNWpiMjB4SERBYUJnTlZCQU1URTJOaExtOXlaekl1ClpYaGhiWEJzWlM1amIyMHdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVFk3VGJqQzdYSHNheC8Kem1yVk1nWnpmODBlb3JFbTNIdis2ZnRqMFgzd2cxdGZVM3hyWWxXZVJwR0JGeFQzNnJmVkdLLzhUQWJ2cnRuZgpUQ1hKak93a28wVXdRekFPQmdOVkhROEJBZjhFQkFNQ0FRWXdFZ1lEVlIwVEFRSC9CQWd3QmdFQi93SUJBVEFkCkJnTlZIUTRFRmdRVWJJNkV4dVRZSEpjczRvNEl5dXZWOVFRa1lGZ3dDZ1lJS29aSXpqMEVBd0lEUndBd1JBSWcKWndjdElBNmdoSlFCZmpDRXdRK1NmYi9iemdsQlV4b0g3ZHVtOUJrUjFkd0NJQlRqcEZkWlcyS2UzSVBMS1h2aApERmQvVmMrcloyMksyeVdKL1BIYXZWWmkKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=",
                        "organizational_unit_identifier": "client"
                      },
                      "enable": true,
                      "orderer_ou_identifier": {
                        "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNIakNDQWNXZ0F3SUJBZ0lVQVFkb1B0S0E0bEk2a0RrMituYzk5NzNhSC9Vd0NnWUlLb1pJemowRUF3SXcKYkRFTE1Ba0dBMVVFQmhNQ1ZVc3hFakFRQmdOVkJBZ1RDVWhoYlhCemFHbHlaVEVRTUE0R0ExVUVCeE1IU0hWeQpjMnhsZVRFWk1CY0dBMVVFQ2hNUWIzSm5NaTVsZUdGdGNHeGxMbU52YlRFY01Cb0dBMVVFQXhNVFkyRXViM0puCk1pNWxlR0Z0Y0d4bExtTnZiVEFlRncweU1EQXpNalF5TURFNE1EQmFGdzB6TlRBek1qRXlNREU0TURCYU1Hd3gKQ3pBSkJnTlZCQVlUQWxWTE1SSXdFQVlEVlFRSUV3bElZVzF3YzJocGNtVXhFREFPQmdOVkJBY1RCMGgxY25OcwpaWGt4R1RBWEJnTlZCQW9URUc5eVp6SXVaWGhoYlhCc1pTNWpiMjB4SERBYUJnTlZCQU1URTJOaExtOXlaekl1ClpYaGhiWEJzWlM1amIyMHdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVFk3VGJqQzdYSHNheC8Kem1yVk1nWnpmODBlb3JFbTNIdis2ZnRqMFgzd2cxdGZVM3hyWWxXZVJwR0JGeFQzNnJmVkdLLzhUQWJ2cnRuZgpUQ1hKak93a28wVXdRekFPQmdOVkhROEJBZjhFQkFNQ0FRWXdFZ1lEVlIwVEFRSC9CQWd3QmdFQi93SUJBVEFkCkJnTlZIUTRFRmdRVWJJNkV4dVRZSEpjczRvNEl5dXZWOVFRa1lGZ3dDZ1lJS29aSXpqMEVBd0lEUndBd1JBSWcKWndjdElBNmdoSlFCZmpDRXdRK1NmYi9iemdsQlV4b0g3ZHVtOUJrUjFkd0NJQlRqcEZkWlcyS2UzSVBMS1h2aApERmQvVmMrcloyMksyeVdKL1BIYXZWWmkKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=",
                        "organizational_unit_identifier": "orderer"
                      },
                      "peer_ou_identifier": {
                        "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNIakNDQWNXZ0F3SUJBZ0lVQVFkb1B0S0E0bEk2a0RrMituYzk5NzNhSC9Vd0NnWUlLb1pJemowRUF3SXcKYkRFTE1Ba0dBMVVFQmhNQ1ZVc3hFakFRQmdOVkJBZ1RDVWhoYlhCemFHbHlaVEVRTUE0R0ExVUVCeE1IU0hWeQpjMnhsZVRFWk1CY0dBMVVFQ2hNUWIzSm5NaTVsZUdGdGNHeGxMbU52YlRFY01Cb0dBMVVFQXhNVFkyRXViM0puCk1pNWxlR0Z0Y0d4bExtTnZiVEFlRncweU1EQXpNalF5TURFNE1EQmFGdzB6TlRBek1qRXlNREU0TURCYU1Hd3gKQ3pBSkJnTlZCQVlUQWxWTE1SSXdFQVlEVlFRSUV3bElZVzF3YzJocGNtVXhFREFPQmdOVkJBY1RCMGgxY25OcwpaWGt4R1RBWEJnTlZCQW9URUc5eVp6SXVaWGhoYlhCc1pTNWpiMjB4SERBYUJnTlZCQU1URTJOaExtOXlaekl1ClpYaGhiWEJzWlM1amIyMHdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVFk3VGJqQzdYSHNheC8Kem1yVk1nWnpmODBlb3JFbTNIdis2ZnRqMFgzd2cxdGZVM3hyWWxXZVJwR0JGeFQzNnJmVkdLLzhUQWJ2cnRuZgpUQ1hKak93a28wVXdRekFPQmdOVkhROEJBZjhFQkFNQ0FRWXdFZ1lEVlIwVEFRSC9CQWd3QmdFQi93SUJBVEFkCkJnTlZIUTRFRmdRVWJJNkV4dVRZSEpjczRvNEl5dXZWOVFRa1lGZ3dDZ1lJS29aSXpqMEVBd0lEUndBd1JBSWcKWndjdElBNmdoSlFCZmpDRXdRK1NmYi9iemdsQlV4b0g3ZHVtOUJrUjFkd0NJQlRqcEZkWlcyS2UzSVBMS1h2aApERmQvVmMrcloyMksyeVdKL1BIYXZWWmkKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=",
                        "organizational_unit_identifier": "peer"
                      }
                    },
                    "intermediate_certs": [],
                    "name": "Org2MSP",
                    "organizational_unit_identifiers": [],
                    "revocation_list": [],
                    "root_certs": [
                      "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNIakNDQWNXZ0F3SUJBZ0lVQVFkb1B0S0E0bEk2a0RrMituYzk5NzNhSC9Vd0NnWUlLb1pJemowRUF3SXcKYkRFTE1Ba0dBMVVFQmhNQ1ZVc3hFakFRQmdOVkJBZ1RDVWhoYlhCemFHbHlaVEVRTUE0R0ExVUVCeE1IU0hWeQpjMnhsZVRFWk1CY0dBMVVFQ2hNUWIzSm5NaTVsZUdGdGNHeGxMbU52YlRFY01Cb0dBMVVFQXhNVFkyRXViM0puCk1pNWxlR0Z0Y0d4bExtTnZiVEFlRncweU1EQXpNalF5TURFNE1EQmFGdzB6TlRBek1qRXlNREU0TURCYU1Hd3gKQ3pBSkJnTlZCQVlUQWxWTE1SSXdFQVlEVlFRSUV3bElZVzF3YzJocGNtVXhFREFPQmdOVkJBY1RCMGgxY25OcwpaWGt4R1RBWEJnTlZCQW9URUc5eVp6SXVaWGhoYlhCc1pTNWpiMjB4SERBYUJnTlZCQU1URTJOaExtOXlaekl1ClpYaGhiWEJzWlM1amIyMHdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVFk3VGJqQzdYSHNheC8Kem1yVk1nWnpmODBlb3JFbTNIdis2ZnRqMFgzd2cxdGZVM3hyWWxXZVJwR0JGeFQzNnJmVkdLLzhUQWJ2cnRuZgpUQ1hKak93a28wVXdRekFPQmdOVkhROEJBZjhFQkFNQ0FRWXdFZ1lEVlIwVEFRSC9CQWd3QmdFQi93SUJBVEFkCkJnTlZIUTRFRmdRVWJJNkV4dVRZSEpjczRvNEl5dXZWOVFRa1lGZ3dDZ1lJS29aSXpqMEVBd0lEUndBd1JBSWcKWndjdElBNmdoSlFCZmpDRXdRK1NmYi9iemdsQlV4b0g3ZHVtOUJrUjFkd0NJQlRqcEZkWlcyS2UzSVBMS1h2aApERmQvVmMrcloyMksyeVdKL1BIYXZWWmkKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
                    ],
                    "signing_identity": null,
                    "tls_intermediate_certs": [],
                    "tls_root_certs": [
                      "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNIakNDQWNXZ0F3SUJBZ0lVQVFkb1B0S0E0bEk2a0RrMituYzk5NzNhSC9Vd0NnWUlLb1pJemowRUF3SXcKYkRFTE1Ba0dBMVVFQmhNQ1ZVc3hFakFRQmdOVkJBZ1RDVWhoYlhCemFHbHlaVEVRTUE0R0ExVUVCeE1IU0hWeQpjMnhsZVRFWk1CY0dBMVVFQ2hNUWIzSm5NaTVsZUdGdGNHeGxMbU52YlRFY01Cb0dBMVVFQXhNVFkyRXViM0puCk1pNWxlR0Z0Y0d4bExtTnZiVEFlRncweU1EQXpNalF5TURFNE1EQmFGdzB6TlRBek1qRXlNREU0TURCYU1Hd3gKQ3pBSkJnTlZCQVlUQWxWTE1SSXdFQVlEVlFRSUV3bElZVzF3YzJocGNtVXhFREFPQmdOVkJBY1RCMGgxY25OcwpaWGt4R1RBWEJnTlZCQW9URUc5eVp6SXVaWGhoYlhCc1pTNWpiMjB4SERBYUJnTlZCQU1URTJOaExtOXlaekl1ClpYaGhiWEJzWlM1amIyMHdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVFk3VGJqQzdYSHNheC8Kem1yVk1nWnpmODBlb3JFbTNIdis2ZnRqMFgzd2cxdGZVM3hyWWxXZVJwR0JGeFQzNnJmVkdLLzhUQWJ2cnRuZgpUQ1hKak93a28wVXdRekFPQmdOVkhROEJBZjhFQkFNQ0FRWXdFZ1lEVlIwVEFRSC9CQWd3QmdFQi93SUJBVEFkCkJnTlZIUTRFRmdRVWJJNkV4dVRZSEpjczRvNEl5dXZWOVFRa1lGZ3dDZ1lJS29aSXpqMEVBd0lEUndBd1JBSWcKWndjdElBNmdoSlFCZmpDRXdRK1NmYi9iemdsQlV4b0g3ZHVtOUJrUjFkd0NJQlRqcEZkWlcyS2UzSVBMS1h2aApERmQvVmMrcloyMksyeVdKL1BIYXZWWmkKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
                    ]
                  },
                  "type": 0
                },
                "version": "0"
              }
            },
            "version": "1"
          }
        },
        "mod_policy": "Admins",
        "policies": {
          "Admins": {
            "mod_policy": "Admins",
            "policy": {
              "type": 3,
              "value": {
                "rule": "MAJORITY",
                "sub_policy": "Admins"
              }
            },
            "version": "0"
          },
          "Endorsement": {
            "mod_policy": "Admins",
            "policy": {
              "type": 3,
              "value": {
                "rule": "MAJORITY",
                "sub_policy": "Endorsement"
              }
            },
            "version": "0"
          },
          "LifecycleEndorsement": {
            "mod_policy": "Admins",
            "policy": {
              "type": 3,
              "value": {
                "rule": "MAJORITY",
                "sub_policy": "Endorsement"
              }
            },
            "version": "0"
          },
          "Readers": {
            "mod_policy": "Admins",
            "policy": {
              "type": 3,
              "value": {
                "rule": "ANY",
                "sub_policy": "Readers"
              }
            },
            "version": "0"
          },
          "Writers": {
            "mod_policy": "Admins",
            "policy": {
              "type": 3,
              "value": {
                "rule": "ANY",
                "sub_policy": "Writers"
              }
            },
            "version": "0"
          }
        },
        "values": {
          "Capabilities": {
            "mod_policy": "Admins",
            "value": {
              "capabilities": {
                "V2_0": {}
              }
            },
            "version": "0"
          }
        },
        "version": "1"
      },
      "Orderer": {
        "groups": {
          "OrdererOrg": {
            "groups": {},
            "mod_policy": "Admins",
            "policies": {
              "Admins": {
                "mod_policy": "Admins",
                "policy": {
                  "type": 1,
                  "value": {
                    "identities": [
                      {
                        "principal": {
                          "msp_identifier": "OrdererMSP",
                          "role": "ADMIN"
                        },
                        "principal_classification": "ROLE"
                      }
                    ],
                    "rule": {
                      "n_out_of": {
                        "n": 1,
                        "rules": [
                          {
                            "signed_by": 0
                          }
                        ]
                      }
                    },
                    "version": 0
                  }
                },
                "version": "0"
              },
              "Readers": {
                "mod_policy": "Admins",
                "policy": {
                  "type": 1,
                  "value": {
                    "identities": [
                      {
                        "principal": {
                          "msp_identifier": "OrdererMSP",
                          "role": "MEMBER"
                        },
                        "principal_classification": "ROLE"
                      }
                    ],
                    "rule": {
                      "n_out_of": {
                        "n": 1,
                        "rules": [
                          {
                            "signed_by": 0
                          }
                        ]
                      }
                    },
                    "version": 0
                  }
                },
                "version": "0"
              },
              "Writers": {
                "mod_policy": "Admins",
                "policy": {
                  "type": 1,
                  "value": {
                    "identities": [
                      {
                        "principal": {
                          "msp_identifier": "OrdererMSP",
                          "role": "MEMBER"
                        },
                        "principal_classification": "ROLE"
                      }
                    ],
                    "rule": {
                      "n_out_of": {
                        "n": 1,
                        "rules": [
                          {
                            "signed_by": 0
                          }
                        ]
                      }
                    },
                    "version": 0
                  }
                },
                "version": "0"
              }
            },
            "values": {
              "MSP": {
                "mod_policy": "Admins",
                "value": {
                  "config": {
                    "admins": [],
                    "crypto_config": {
                      "identity_identifier_hash_function": "SHA256",
                      "signature_hash_family": "SHA2"
                    },
                    "fabric_node_ous": {
                      "admin_ou_identifier": {
                        "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNDekNDQWJHZ0F3SUJBZ0lVUkgyT0tlV1loaStFMkFHZ3IwWUdlVTRUOWs0d0NnWUlLb1pJemowRUF3SXcKWWpFTE1Ba0dBMVVFQmhNQ1ZWTXhFVEFQQmdOVkJBZ1RDRTVsZHlCWmIzSnJNUkV3RHdZRFZRUUhFd2hPWlhjZwpXVzl5YXpFVU1CSUdBMVVFQ2hNTFpYaGhiWEJzWlM1amIyMHhGekFWQmdOVkJBTVREbU5oTG1WNFlXMXdiR1V1ClkyOXRNQjRYRFRJd01ETXlOREl3TVRnd01Gb1hEVE0xTURNeU1USXdNVGd3TUZvd1lqRUxNQWtHQTFVRUJoTUMKVlZNeEVUQVBCZ05WQkFnVENFNWxkeUJaYjNKck1SRXdEd1lEVlFRSEV3aE9aWGNnV1c5eWF6RVVNQklHQTFVRQpDaE1MWlhoaGJYQnNaUzVqYjIweEZ6QVZCZ05WQkFNVERtTmhMbVY0WVcxd2JHVXVZMjl0TUZrd0V3WUhLb1pJCnpqMENBUVlJS29aSXpqMERBUWNEUWdBRS9yb2dWY0hFcEVQMDhTUTl3VTVpdkNxaUFDKzU5WUx1dkRDNkx6UlIKWXdyZkFxdncvT0FodVlQRkhnRFZ1SFExOVdXMGxSV2FKWmpVcDFxNmRCWEhlYU5GTUVNd0RnWURWUjBQQVFILwpCQVFEQWdFR01CSUdBMVVkRXdFQi93UUlNQVlCQWY4Q0FRRXdIUVlEVlIwT0JCWUVGTG9kWFpjaTVNNlFxYkNUCm1YZ3lTbU1aYlZHWE1Bb0dDQ3FHU000OUJBTUNBMGdBTUVVQ0lRQ0hFTElvajJUNG15ODI0SENQRFc2bEZFRTEKSDc1c2FyN1V4TVJSNmFWckZnSWdMZUxYT0ZoSDNjZ0pGeDhJckVyTjlhZmdjVVIyd0ZYUkQ0V0V0MVp1bmxBPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==",
                        "organizational_unit_identifier": "admin"
                      },
                      "client_ou_identifier": {
                        "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNDekNDQWJHZ0F3SUJBZ0lVUkgyT0tlV1loaStFMkFHZ3IwWUdlVTRUOWs0d0NnWUlLb1pJemowRUF3SXcKWWpFTE1Ba0dBMVVFQmhNQ1ZWTXhFVEFQQmdOVkJBZ1RDRTVsZHlCWmIzSnJNUkV3RHdZRFZRUUhFd2hPWlhjZwpXVzl5YXpFVU1CSUdBMVVFQ2hNTFpYaGhiWEJzWlM1amIyMHhGekFWQmdOVkJBTVREbU5oTG1WNFlXMXdiR1V1ClkyOXRNQjRYRFRJd01ETXlOREl3TVRnd01Gb1hEVE0xTURNeU1USXdNVGd3TUZvd1lqRUxNQWtHQTFVRUJoTUMKVlZNeEVUQVBCZ05WQkFnVENFNWxkeUJaYjNKck1SRXdEd1lEVlFRSEV3aE9aWGNnV1c5eWF6RVVNQklHQTFVRQpDaE1MWlhoaGJYQnNaUzVqYjIweEZ6QVZCZ05WQkFNVERtTmhMbVY0WVcxd2JHVXVZMjl0TUZrd0V3WUhLb1pJCnpqMENBUVlJS29aSXpqMERBUWNEUWdBRS9yb2dWY0hFcEVQMDhTUTl3VTVpdkNxaUFDKzU5WUx1dkRDNkx6UlIKWXdyZkFxdncvT0FodVlQRkhnRFZ1SFExOVdXMGxSV2FKWmpVcDFxNmRCWEhlYU5GTUVNd0RnWURWUjBQQVFILwpCQVFEQWdFR01CSUdBMVVkRXdFQi93UUlNQVlCQWY4Q0FRRXdIUVlEVlIwT0JCWUVGTG9kWFpjaTVNNlFxYkNUCm1YZ3lTbU1aYlZHWE1Bb0dDQ3FHU000OUJBTUNBMGdBTUVVQ0lRQ0hFTElvajJUNG15ODI0SENQRFc2bEZFRTEKSDc1c2FyN1V4TVJSNmFWckZnSWdMZUxYT0ZoSDNjZ0pGeDhJckVyTjlhZmdjVVIyd0ZYUkQ0V0V0MVp1bmxBPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==",
                        "organizational_unit_identifier": "client"
                      },
                      "enable": true,
                      "orderer_ou_identifier": {
                        "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNDekNDQWJHZ0F3SUJBZ0lVUkgyT0tlV1loaStFMkFHZ3IwWUdlVTRUOWs0d0NnWUlLb1pJemowRUF3SXcKWWpFTE1Ba0dBMVVFQmhNQ1ZWTXhFVEFQQmdOVkJBZ1RDRTVsZHlCWmIzSnJNUkV3RHdZRFZRUUhFd2hPWlhjZwpXVzl5YXpFVU1CSUdBMVVFQ2hNTFpYaGhiWEJzWlM1amIyMHhGekFWQmdOVkJBTVREbU5oTG1WNFlXMXdiR1V1ClkyOXRNQjRYRFRJd01ETXlOREl3TVRnd01Gb1hEVE0xTURNeU1USXdNVGd3TUZvd1lqRUxNQWtHQTFVRUJoTUMKVlZNeEVUQVBCZ05WQkFnVENFNWxkeUJaYjNKck1SRXdEd1lEVlFRSEV3aE9aWGNnV1c5eWF6RVVNQklHQTFVRQpDaE1MWlhoaGJYQnNaUzVqYjIweEZ6QVZCZ05WQkFNVERtTmhMbVY0WVcxd2JHVXVZMjl0TUZrd0V3WUhLb1pJCnpqMENBUVlJS29aSXpqMERBUWNEUWdBRS9yb2dWY0hFcEVQMDhTUTl3VTVpdkNxaUFDKzU5WUx1dkRDNkx6UlIKWXdyZkFxdncvT0FodVlQRkhnRFZ1SFExOVdXMGxSV2FKWmpVcDFxNmRCWEhlYU5GTUVNd0RnWURWUjBQQVFILwpCQVFEQWdFR01CSUdBMVVkRXdFQi93UUlNQVlCQWY4Q0FRRXdIUVlEVlIwT0JCWUVGTG9kWFpjaTVNNlFxYkNUCm1YZ3lTbU1aYlZHWE1Bb0dDQ3FHU000OUJBTUNBMGdBTUVVQ0lRQ0hFTElvajJUNG15ODI0SENQRFc2bEZFRTEKSDc1c2FyN1V4TVJSNmFWckZnSWdMZUxYT0ZoSDNjZ0pGeDhJckVyTjlhZmdjVVIyd0ZYUkQ0V0V0MVp1bmxBPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==",
                        "organizational_unit_identifier": "orderer"
                      },
                      "peer_ou_identifier": {
                        "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNDekNDQWJHZ0F3SUJBZ0lVUkgyT0tlV1loaStFMkFHZ3IwWUdlVTRUOWs0d0NnWUlLb1pJemowRUF3SXcKWWpFTE1Ba0dBMVVFQmhNQ1ZWTXhFVEFQQmdOVkJBZ1RDRTVsZHlCWmIzSnJNUkV3RHdZRFZRUUhFd2hPWlhjZwpXVzl5YXpFVU1CSUdBMVVFQ2hNTFpYaGhiWEJzWlM1amIyMHhGekFWQmdOVkJBTVREbU5oTG1WNFlXMXdiR1V1ClkyOXRNQjRYRFRJd01ETXlOREl3TVRnd01Gb1hEVE0xTURNeU1USXdNVGd3TUZvd1lqRUxNQWtHQTFVRUJoTUMKVlZNeEVUQVBCZ05WQkFnVENFNWxkeUJaYjNKck1SRXdEd1lEVlFRSEV3aE9aWGNnV1c5eWF6RVVNQklHQTFVRQpDaE1MWlhoaGJYQnNaUzVqYjIweEZ6QVZCZ05WQkFNVERtTmhMbVY0WVcxd2JHVXVZMjl0TUZrd0V3WUhLb1pJCnpqMENBUVlJS29aSXpqMERBUWNEUWdBRS9yb2dWY0hFcEVQMDhTUTl3VTVpdkNxaUFDKzU5WUx1dkRDNkx6UlIKWXdyZkFxdncvT0FodVlQRkhnRFZ1SFExOVdXMGxSV2FKWmpVcDFxNmRCWEhlYU5GTUVNd0RnWURWUjBQQVFILwpCQVFEQWdFR01CSUdBMVVkRXdFQi93UUlNQVlCQWY4Q0FRRXdIUVlEVlIwT0JCWUVGTG9kWFpjaTVNNlFxYkNUCm1YZ3lTbU1aYlZHWE1Bb0dDQ3FHU000OUJBTUNBMGdBTUVVQ0lRQ0hFTElvajJUNG15ODI0SENQRFc2bEZFRTEKSDc1c2FyN1V4TVJSNmFWckZnSWdMZUxYT0ZoSDNjZ0pGeDhJckVyTjlhZmdjVVIyd0ZYUkQ0V0V0MVp1bmxBPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==",
                        "organizational_unit_identifier": "peer"
                      }
                    },
                    "intermediate_certs": [],
                    "name": "OrdererMSP",
                    "organizational_unit_identifiers": [],
                    "revocation_list": [],
                    "root_certs": [
                      "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNDekNDQWJHZ0F3SUJBZ0lVUkgyT0tlV1loaStFMkFHZ3IwWUdlVTRUOWs0d0NnWUlLb1pJemowRUF3SXcKWWpFTE1Ba0dBMVVFQmhNQ1ZWTXhFVEFQQmdOVkJBZ1RDRTVsZHlCWmIzSnJNUkV3RHdZRFZRUUhFd2hPWlhjZwpXVzl5YXpFVU1CSUdBMVVFQ2hNTFpYaGhiWEJzWlM1amIyMHhGekFWQmdOVkJBTVREbU5oTG1WNFlXMXdiR1V1ClkyOXRNQjRYRFRJd01ETXlOREl3TVRnd01Gb1hEVE0xTURNeU1USXdNVGd3TUZvd1lqRUxNQWtHQTFVRUJoTUMKVlZNeEVUQVBCZ05WQkFnVENFNWxkeUJaYjNKck1SRXdEd1lEVlFRSEV3aE9aWGNnV1c5eWF6RVVNQklHQTFVRQpDaE1MWlhoaGJYQnNaUzVqYjIweEZ6QVZCZ05WQkFNVERtTmhMbVY0WVcxd2JHVXVZMjl0TUZrd0V3WUhLb1pJCnpqMENBUVlJS29aSXpqMERBUWNEUWdBRS9yb2dWY0hFcEVQMDhTUTl3VTVpdkNxaUFDKzU5WUx1dkRDNkx6UlIKWXdyZkFxdncvT0FodVlQRkhnRFZ1SFExOVdXMGxSV2FKWmpVcDFxNmRCWEhlYU5GTUVNd0RnWURWUjBQQVFILwpCQVFEQWdFR01CSUdBMVVkRXdFQi93UUlNQVlCQWY4Q0FRRXdIUVlEVlIwT0JCWUVGTG9kWFpjaTVNNlFxYkNUCm1YZ3lTbU1aYlZHWE1Bb0dDQ3FHU000OUJBTUNBMGdBTUVVQ0lRQ0hFTElvajJUNG15ODI0SENQRFc2bEZFRTEKSDc1c2FyN1V4TVJSNmFWckZnSWdMZUxYT0ZoSDNjZ0pGeDhJckVyTjlhZmdjVVIyd0ZYUkQ0V0V0MVp1bmxBPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="
                    ],
                    "signing_identity": null,
                    "tls_intermediate_certs": [],
                    "tls_root_certs": [
                      "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNDekNDQWJHZ0F3SUJBZ0lVUkgyT0tlV1loaStFMkFHZ3IwWUdlVTRUOWs0d0NnWUlLb1pJemowRUF3SXcKWWpFTE1Ba0dBMVVFQmhNQ1ZWTXhFVEFQQmdOVkJBZ1RDRTVsZHlCWmIzSnJNUkV3RHdZRFZRUUhFd2hPWlhjZwpXVzl5YXpFVU1CSUdBMVVFQ2hNTFpYaGhiWEJzWlM1amIyMHhGekFWQmdOVkJBTVREbU5oTG1WNFlXMXdiR1V1ClkyOXRNQjRYRFRJd01ETXlOREl3TVRnd01Gb1hEVE0xTURNeU1USXdNVGd3TUZvd1lqRUxNQWtHQTFVRUJoTUMKVlZNeEVUQVBCZ05WQkFnVENFNWxkeUJaYjNKck1SRXdEd1lEVlFRSEV3aE9aWGNnV1c5eWF6RVVNQklHQTFVRQpDaE1MWlhoaGJYQnNaUzVqYjIweEZ6QVZCZ05WQkFNVERtTmhMbVY0WVcxd2JHVXVZMjl0TUZrd0V3WUhLb1pJCnpqMENBUVlJS29aSXpqMERBUWNEUWdBRS9yb2dWY0hFcEVQMDhTUTl3VTVpdkNxaUFDKzU5WUx1dkRDNkx6UlIKWXdyZkFxdncvT0FodVlQRkhnRFZ1SFExOVdXMGxSV2FKWmpVcDFxNmRCWEhlYU5GTUVNd0RnWURWUjBQQVFILwpCQVFEQWdFR01CSUdBMVVkRXdFQi93UUlNQVlCQWY4Q0FRRXdIUVlEVlIwT0JCWUVGTG9kWFpjaTVNNlFxYkNUCm1YZ3lTbU1aYlZHWE1Bb0dDQ3FHU000OUJBTUNBMGdBTUVVQ0lRQ0hFTElvajJUNG15ODI0SENQRFc2bEZFRTEKSDc1c2FyN1V4TVJSNmFWckZnSWdMZUxYT0ZoSDNjZ0pGeDhJckVyTjlhZmdjVVIyd0ZYUkQ0V0V0MVp1bmxBPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="
                    ]
                  },
                  "type": 0
                },
                "version": "0"
              }
            },
            "version": "0"
          }
        },
        "mod_policy": "Admins",
        "policies": {
          "Admins": {
            "mod_policy": "Admins",
            "policy": {
              "type": 3,
              "value": {
                "rule": "MAJORITY",
                "sub_policy": "Admins"
              }
            },
            "version": "0"
          },
          "BlockValidation": {
            "mod_policy": "Admins",
            "policy": {
              "type": 3,
              "value": {
                "rule": "ANY",
                "sub_policy": "Writers"
              }
            },
            "version": "0"
          },
          "Readers": {
            "mod_policy": "Admins",
            "policy": {
              "type": 3,
              "value": {
                "rule": "ANY",
                "sub_policy": "Readers"
              }
            },
            "version": "0"
          },
          "Writers": {
            "mod_policy": "Admins",
            "policy": {
              "type": 3,
              "value": {
                "rule": "ANY",
                "sub_policy": "Writers"
              }
            },
            "version": "0"
          }
        },
        "values": {
          "BatchSize": {
            "mod_policy": "Admins",
            "value": {
              "absolute_max_bytes": 103809024,
              "max_message_count": 10,
              "preferred_max_bytes": 524288
            },
            "version": "0"
          },
          "BatchTimeout": {
            "mod_policy": "Admins",
            "value": {
              "timeout": "2s"
            },
            "version": "0"
          },
          "Capabilities": {
            "mod_policy": "Admins",
            "value": {
              "capabilities": {
                "V2_0": {}
              }
            },
            "version": "0"
          },
          "ChannelRestrictions": {
            "mod_policy": "Admins",
            "value": null,
            "version": "0"
          },
          "ConsensusType": {
            "mod_policy": "Admins",
            "value": {
              "metadata": {
                "consenters": [
                  {
                    "client_tls_cert": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN3akNDQW1pZ0F3SUJBZ0lVZG9JbmpzaW5vVnZua0llbE5WUU8wbDRMbEdrd0NnWUlLb1pJemowRUF3SXcKWWpFTE1Ba0dBMVVFQmhNQ1ZWTXhFVEFQQmdOVkJBZ1RDRTVsZHlCWmIzSnJNUkV3RHdZRFZRUUhFd2hPWlhjZwpXVzl5YXpFVU1CSUdBMVVFQ2hNTFpYaGhiWEJzWlM1amIyMHhGekFWQmdOVkJBTVREbU5oTG1WNFlXMXdiR1V1ClkyOXRNQjRYRFRJd01ETXlOREl3TVRnd01Gb1hEVEl4TURNeU5ESXdNak13TUZvd1lERUxNQWtHQTFVRUJoTUMKVlZNeEZ6QVZCZ05WQkFnVERrNXZjblJvSUVOaGNtOXNhVzVoTVJRd0VnWURWUVFLRXd0SWVYQmxjbXhsWkdkbApjakVRTUE0R0ExVUVDeE1IYjNKa1pYSmxjakVRTUE0R0ExVUVBeE1IYjNKa1pYSmxjakJaTUJNR0J5cUdTTTQ5CkFnRUdDQ3FHU000OUF3RUhBMElBQkdGaFd3SllGbHR3clBVellIQ3loNTMvU3VpVU1ZYnVJakdGTWRQMW9FRzMKSkcrUlRSOFR4NUNYTXdpV05sZ285dU00a1NGTzBINURZUWZPQU5MU0o5NmpnZjB3Z2Zvd0RnWURWUjBQQVFILwpCQVFEQWdPb01CMEdBMVVkSlFRV01CUUdDQ3NHQVFVRkJ3TUJCZ2dyQmdFRkJRY0RBakFNQmdOVkhSTUJBZjhFCkFqQUFNQjBHQTFVZERnUVdCQlJ2M3lNUmh5cHc0Qi9Cc1NHTlVJL0VpU1lNN2pBZkJnTlZIU01FR0RBV2dCUzYKSFYyWEl1VE9rS213azVsNE1rcGpHVzFSbHpBZUJnTlZIUkVFRnpBVmdoTnZjbVJsY21WeUxtVjRZVzF3YkdVdQpZMjl0TUZzR0NDb0RCQVVHQndnQkJFOTdJbUYwZEhKeklqcDdJbWhtTGtGbVptbHNhV0YwYVc5dUlqb2lJaXdpCmFHWXVSVzV5YjJ4c2JXVnVkRWxFSWpvaWIzSmtaWEpsY2lJc0ltaG1MbFI1Y0dVaU9pSnZjbVJsY21WeUluMTkKTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSVFESHNrWUR5clNqeWpkTVVVWDNaT05McXJUNkdCcVNUdmZXN0dXMwpqVTg2cEFJZ0VIZkloVWxVV0VpN1hTb2Y4K2toaW9PYW5PWG80TWxQbGhlT0xjTGlqUzA9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K",
                    "host": "orderer.example.com",
                    "port": 7050,
                    "server_tls_cert": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN3akNDQW1pZ0F3SUJBZ0lVZG9JbmpzaW5vVnZua0llbE5WUU8wbDRMbEdrd0NnWUlLb1pJemowRUF3SXcKWWpFTE1Ba0dBMVVFQmhNQ1ZWTXhFVEFQQmdOVkJBZ1RDRTVsZHlCWmIzSnJNUkV3RHdZRFZRUUhFd2hPWlhjZwpXVzl5YXpFVU1CSUdBMVVFQ2hNTFpYaGhiWEJzWlM1amIyMHhGekFWQmdOVkJBTVREbU5oTG1WNFlXMXdiR1V1ClkyOXRNQjRYRFRJd01ETXlOREl3TVRnd01Gb1hEVEl4TURNeU5ESXdNak13TUZvd1lERUxNQWtHQTFVRUJoTUMKVlZNeEZ6QVZCZ05WQkFnVERrNXZjblJvSUVOaGNtOXNhVzVoTVJRd0VnWURWUVFLRXd0SWVYQmxjbXhsWkdkbApjakVRTUE0R0ExVUVDeE1IYjNKa1pYSmxjakVRTUE0R0ExVUVBeE1IYjNKa1pYSmxjakJaTUJNR0J5cUdTTTQ5CkFnRUdDQ3FHU000OUF3RUhBMElBQkdGaFd3SllGbHR3clBVellIQ3loNTMvU3VpVU1ZYnVJakdGTWRQMW9FRzMKSkcrUlRSOFR4NUNYTXdpV05sZ285dU00a1NGTzBINURZUWZPQU5MU0o5NmpnZjB3Z2Zvd0RnWURWUjBQQVFILwpCQVFEQWdPb01CMEdBMVVkSlFRV01CUUdDQ3NHQVFVRkJ3TUJCZ2dyQmdFRkJRY0RBakFNQmdOVkhSTUJBZjhFCkFqQUFNQjBHQTFVZERnUVdCQlJ2M3lNUmh5cHc0Qi9Cc1NHTlVJL0VpU1lNN2pBZkJnTlZIU01FR0RBV2dCUzYKSFYyWEl1VE9rS213azVsNE1rcGpHVzFSbHpBZUJnTlZIUkVFRnpBVmdoTnZjbVJsY21WeUxtVjRZVzF3YkdVdQpZMjl0TUZzR0NDb0RCQVVHQndnQkJFOTdJbUYwZEhKeklqcDdJbWhtTGtGbVptbHNhV0YwYVc5dUlqb2lJaXdpCmFHWXVSVzV5YjJ4c2JXVnVkRWxFSWpvaWIzSmtaWEpsY2lJc0ltaG1MbFI1Y0dVaU9pSnZjbVJsY21WeUluMTkKTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSVFESHNrWUR5clNqeWpkTVVVWDNaT05McXJUNkdCcVNUdmZXN0dXMwpqVTg2cEFJZ0VIZkloVWxVV0VpN1hTb2Y4K2toaW9PYW5PWG80TWxQbGhlT0xjTGlqUzA9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
                  }
                ],
                "options": {
                  "election_tick": 10,
                  "heartbeat_tick": 1,
                  "max_inflight_blocks": 5,
                  "snapshot_interval_size": 16777216,
                  "tick_interval": "500ms"
                }
              },
              "state": "STATE_NORMAL",
              "type": "etcdraft"
            },
            "version": "0"
          }
        },
        "version": "0"
      }
    },
    "mod_policy": "Admins",
    "policies": {
      "Admins": {
        "mod_policy": "Admins",
        "policy": {
          "type": 3,
          "value": {
            "rule": "MAJORITY",
            "sub_policy": "Admins"
          }
        },
        "version": "0"
      },
      "Readers": {
        "mod_policy": "Admins",
        "policy": {
          "type": 3,
          "value": {
            "rule": "ANY",
            "sub_policy": "Readers"
          }
        },
        "version": "0"
      },
      "Writers": {
        "mod_policy": "Admins",
        "policy": {
          "type": 3,
          "value": {
            "rule": "ANY",
            "sub_policy": "Writers"
          }
        },
        "version": "0"
      }
    },
    "values": {
      "BlockDataHashingStructure": {
        "mod_policy": "Admins",
        "value": {
          "width": 4294967295
        },
        "version": "0"
      },
      "Capabilities": {
        "mod_policy": "Admins",
        "value": {
          "capabilities": {
            "V2_0": {}
          }
        },
        "version": "0"
      },
      "Consortium": {
        "mod_policy": "Admins",
        "value": {
          "name": "SampleConsortium"
        },
        "version": "0"
      },
      "HashingAlgorithm": {
        "mod_policy": "Admins",
        "value": {
          "name": "SHA256"
        },
        "version": "0"
      },
      "OrdererAddresses": {
        "mod_policy": "/Channel/Orderer/Admins",
        "value": {
          "addresses": [
            "orderer.example.com:7050"
          ]
        },
        "version": "0"
      }
    },
    "version": "0"
  },
  "sequence": "3"
}

配置文件在这种形式下可能看起来很吓人,但是一旦你研究它,你就会发现它有一个逻辑结构。

例如,让我们看一下关闭了几个选项卡的配置。

请注意,这是应用程序通道的配置,而不是排序程序系统通道。
示例配置
配置的结构现在应该更明显了。您可以看到配置分组:ChannelApplicationorder,以及与每个配置分组相关的配置参数(我们将在下一节中详细讨论这些参数),还可以看到代表组织的msp所在的位置。请注意,Channel 配置分组低于order 分组配置值。

关于这些参数的更多信息

在本节中,我们将深入了解可配置值在配置中的位置。

首先,配置的多个部分都有配置参数:

  • 策略。策略不仅仅是一个配置值(可以按照mod_策略中的定义进行更新),它定义了可以更改所有参数的环境。有关详细信息,请查看策略
  • 功能。确保网络和通道以相同的方式处理事情,为诸如通道配置更新和链码调用之类的事情创建确定的结果。如果没有确定的结果,通道上的一个节点可能会使交易无效,而另一个节点可能会验证它。有关更多信息,请查看功能

通道/应用

控制应用程序通道特有的配置参数(例如,添加或删除通道成员)。默认情况下,更改这些参数需要大多数应用程序组织管理员的签名。

  • 向通道添加组织。要将组织添加到通道,必须在此处生成并添加其MSP和其他组织参数(在Channel/Application/groups下)。
  • 组织相关参数。特定于一个组织的任何参数(例如,标识一个锚节点,或者组织管理员的证书)都可以更改。请注意,在默认情况下,更改这些值不需要大多数应用程序组织管理员,而只需要组织本身的管理员。

通道/排序节点

管理排序服务或排序者系统通道特有的配置参数,需要排序组织的大多数管理员(默认情况下只有一个排序组织,但可以添加更多组织,例如,当多个组织向排序服务提供节点时)。

  • 批量大小。这些参数决定了块中交易的数量和大小。任何块都不会出现大于absolute_max_bytes或块内交易数超过max_message_count的块。如果可以在preferred_max_bytes下构造块,那么块将被过早地剪切,大于此大小的交易将出现在它们自己的块中。
  • 批处理超时。在第一个交易到达后,在切割块之前等待其他交易的时间量。减小此值将改善延迟,但降低太多可能会因为不允许块填充到其最大容量而降低吞吐量。
  • 块验证。此策略指定将块视为有效的签名要求。默认情况下,它需要排序组织的某个成员的签名。
  • 共识型。为了能够将基于Kafka的排序服务迁移到基于Raft的排序服务,可以改变通道的共识类型。更多信息,请查看从Kafka迁移到Raft
  • Raft排序服务参数。要查看Raft排序服务特有的参数,请查看Raft configuration
  • Kafka经纪人(如适用)。当ConsensusType设置为kafka时,brokers列表将枚举kafka代理的某些子集(最好是全部),以便排序方在启动时首先连接到这些代理。

通道

管理节点组织和排序服务组织都需要同意的配置参数,需要大多数应用程序组织管理员和排序者组织管理员的同意。

  • 排序者地址。一个地址列表,客户端可以在其中调用排序方广播传递函数。节点端随机选择这些地址,并在它们之间进行故障切换以检索块。
  • 哈希结构。块数据是字节数组的数组。块数据的哈希计算为Merkle树。此值指定该Merkle树的宽度。目前,该值被固定为4294967295,该值对应于块数据字节的串联的简单平面哈希。
  • 哈希算法。用于计算编码到区块链块中的哈希值的算法。特别是,这会影响数据哈希和块的前一个块哈希字段。注意,此字段当前只有一个有效值(SHA256),不应更改。

系统通道配置参数

某些配置值对于排序节点系统通道是唯一的。

  • 通道创建策略。定义策略值,该值将被设置为在中定义的联盟的新通道应用程序组的mod_policy。附加到通道创建请求的签名集将根据新通道中此策略的实例化进行检查,以确保通道创建被授权。请注意,此配置值仅在orderer系统通道中设置。
  • 通道限制。仅可在排序节点系统通道中编辑。排序节点愿意分配的通道总数可以指定为max_count。这在具有弱联盟ChannelCreation策略的预生产环境中非常有用。

编辑配置

更新通道配置是一个概念上简单的三步操作:

  • 获取最新的通道配置
  • 创建修改的通道配置
  • 创建配置更新交易

然而,正如您将看到的,这种概念上的简单性被包装在一个有点复杂的过程中。因此,一些用户可能会选择编写提取、转换和确定配置更新范围的过程脚本。用户还可以选择如何修改通道配置本身,可以手动修改,也可以使用jq这样的工具。

我们有两个教程专门介绍如何编辑通道配置以达到特定目的:

在本主题中,我们将展示独立于配置更新的最终目标编辑通道配置的过程。

为配置更新设置环境变量

在尝试使用示例命令之前,请确保导出以下环境变量,这些变量将取决于构建部署的方式。请注意,由于通道配置更新仅适用于正在更新的通道的配置(排序系统通道除外,其配置默认复制到应用通道的配置中),因此必须为每个正在更新的通道设置通道名称和通道名称。

  • CH_NAME:正在更新的通道的名称。
  • TLS_ROOT_CA:提出更新的组织的TLS CA的根CA证书路径。
  • CORE_PEER_LOCALMSPID:您的MSP的名称。
  • CORE_PEER_MSPCONFIGPATH:组织的MSP的绝对路径。
  • ORDERER_CONTAINER:排序节点容器的名称。请注意,当以排序服务为目标时,您可以将排序服务中的任何活动节点作为目标。你的请求将自动转发给领导。

注意:本主题将为被拉取和修改的各种JSON和protobuf文件提供默认名称(config_block.pbconfig_block.json等等)。你可以随意使用任何你想要的名字。但是,请注意,除非您在每次配置更新结束时返回并删除这些文件,否则在进行其他更新时必须选择不同的文件。

步骤1:拉取并转换配置

更新通道配置的第一步是获取最新的配置块。这是一个三步走的过程。首先,我们将拉取protobuf格式的通道配置,创建一个名为config_block.pb的文件。

确保您在节点容器中。

现在提出:

peer channel fetch config config_block.pb -o $ORDERER_CONTAINER -c $CH_NAME --tls --cafile $TLS_ROOT_CA

接下来,我们将把protobuf版本的通道配置转换成一个名为config_block.json的JSON版本(JSON文件更易于人类阅读和理解):

configtxlator proto_decode --input config_block.pb --type common.Block --output config_block.json

最后,我们将从配置中找出所有不必要的元数据,这样更容易阅读。您可以随意调用此文件,但在本例中,我们将调用config.json

jq .data.data[0].payload.data.config config_block.json > config.json

现在让我们复制一个名为modified_config.jsonconfig.json不要直接编辑config.json,因为我们将在后面的步骤中使用它来计算config.jsonmodified_config.json之间的差异。

cp config.json modified_config.json

第2步:修改配置

此时,您有两个选项可以选择如何修改配置。

  • 打开modified_config.json使用您选择的文本编辑器并进行编辑。在线教程介绍了如何从没有编辑器的容器中复制文件、编辑文件并将其添加回容器。
  • 使用jq对配置应用编辑。

您选择手动编辑配置还是使用jq取决于您的用例。因为jq简洁且可编写脚本(当对多个通道进行相同的配置更新时,这是一个优势),因此它是执行通道更新的推荐方法。关于如何使用jq的示例,请查看updateding channel capabilities,其中显示了利用一个名为capabilities.json。 如果您要更新通道中的功能以外的内容,则必须相应地修改jq命令和JSON文件。

有关通道配置的内容和结构的更多信息,请查看上面的示例通道配置

步骤3:重新编码并提交配置

无论是手动更新配置还是使用jq之类的工具,现在都必须运行您运行的流程,以反向拉取和调整配置范围,同时还要执行一个步骤来计算旧配置和新配置之间的差异,然后再将配置更新提交给要批准的通道上的其他管理员。

首先,我们将config.json文件返回到protobuf格式,创建一个名为config.pb的文件。 然后我们会用我们的modified_config.json文件。然后,我们将计算这两个文件之间的差异,创建一个名为config_update.pb的文件。

configtxlator proto_encode --input config.json --type common.Config --output config.pb

configtxlator proto_encode --input modified_config.json --type common.Config --output modified_config.pb

configtxlator compute_update --channel_id $CH_NAME --original config.pb --updated modified_config.pb --output config_update.pb

现在我们已经计算了旧配置和新配置之间的差异,我们可以将更改应用于配置。

configtxlator proto_decode --input config_update.pb --type common.ConfigUpdate --output config_update.json

echo '{"payload":{"header":{"channel_header":{"channel_id":"'$CH_NAME'", "type":2}},"data":{"config_update":'$(cat config_update.json)'}}}' | jq . > config_update_in_envelope.json

configtxlator proto_encode --input config_update_in_envelope.json --type common.Envelope --output config_update_in_envelope.pb

提交配置更新交易:

peer channel update -f config_update_in_envelope.pb -c $CH_NAME -o $ORDERER_CONTAINER --tls --cafile $TLS_ROOT_CA

我们的配置更新交易表示原始配置和修改配置之间的差异,但是排序服务会将其转换为完整的通道配置。

得到必要的签名

一旦您成功地生成了新的配置协议文件,它将需要满足您试图更改的任何内容的相关策略,通常(尽管不总是)需要其他组织的签名。

注意:您可以编写签名集合的脚本,具体取决于您的应用程序。通常情况下,您可能会收集到比所需数量更多的签名。

获取这些签名的实际过程将取决于如何设置系统,但主要有两个实现。目前,Fabric命令行默认为“pass-it-along”系统。也就是说,提出配置更新的组织的管理员将更新发送给需要签名的其他人(通常是另一个管理员)。这个管理员签名(或者不签名)并将其传递给下一个管理员,以此类推,直到有足够的签名提交配置。

这有一个优点:当有足够的签名时,最后一个管理员可以简单地提交配置交易(在Fabric中,peer channel update命令默认包含一个签名)。然而,这一过程只适用于较小的通道,因为“传递”方法可能很耗时。

另一个选择是将更新提交给通道上的每个管理员,然后等待足够的签名返回。然后将这些签名缝合在一起并提交。这使得创建配置更新的管理员的生活变得更加困难(强迫他们为每个签名者处理一个文件),但是对于正在开发Fabric管理应用程序的用户来说,这是一个推荐的工作流。

一旦配置被添加到账本中,最好的做法是将其拉入并转换为JSON,以检查是否正确添加了所有配置。这也将作为最新配置的有用副本。


开发者链码

什么是链码?

链码是一个用GoNode.jsJava编写的实现指定接口的程序。链码运行在与节点不同的进程中,并通过应用程序提交的交易初始化和管理账本状态。

链码通常处理网络成员同意的业务逻辑,因此类似于“智能合约”。可以调用链码来更新或查询建议交易中的账本。给定适当的权限,链码可以调用同一通道或不同通道中的另一个链码来访问其状态。请注意,如果被调用链码与调用链码位于不同的通道上,则只允许读取查询。也就是说,不同通道上被调用的链码只是一个查询,它不参与后续提交阶段的状态验证检查。

在下面的部分中,我们将从应用程序开发人员的角度来探讨链码。我们将展示一个简单的链码示例应用程序,并详细介绍链码填充API中每个方法的用途。如果您是正在将链码部署到正在运行的网络的网络运营商,请访问将智能合约部署到通道教程和Fabric链码生命周期概念主题。


链码API

每个链码程序都必须实现chaincode接口,其方法被调用以响应接收到的交易。您可以在下面找到针对不同语言的链码Shim API的参考文档:

在每种语言中,客户机调用Invoke方法来提交交易提案。此方法允许您使用链码读取和写入通道账本上的数据。

您还需要包含一个Init方法,该方法将作为链码的初始化函数。将调用此方法,以便在启动或升级链码时初始化它。默认情况下,此函数从不执行。但是,您可以使用链码定义来请求执行Init函数。如果请求执行Init,fabric将确保Init在任何其他函数之前被调用,并且只被调用一次。此选项提供了对哪些用户可以初始化链码的附加控制,以及向账本添加初始数据的功能。如果使用节点CLI批准链码定义,请使用--init required标志请求执行init函数。然后使用peer chaincode invoke命令并传递--isInit标志来调用Init函数。有关详细信息,请参见Fabric链码生命周期

链码“shim”API中的另一个接口是ChaincodeStubInterface

用于访问和修改账本,以及在链码之间进行调用。

在本教程中使用Go链码,我们将通过实现一个管理简单“资产”的简单链码应用程序来演示这些APIs的使用。

简单资产链码

我们的应用程序是在账本上创建资产(键值对)的基本示例链码。

为代码选择位置

如果您没有在Go中进行编程,您可能需要确保已安装Go编程语言,并且系统配置正确。

现在,您需要为链码应用程序创建一个目录,作为$GOPATH/src/的子目录。

为了简单起见,让我们使用以下命令:

mkdir -p $GOPATH/src/sacc && cd $GOPATH/src/sacc

现在,让我们创建一个用代码填充的源文件:

touch sacc.go

Housekeeping

首先,让我们从一些housekeeping开始。与每个链码一样,它实现了链码接口,特别是InitInvoke函数。因此,让我们为链码添加必要的依赖项的Go import语句。我们将导入链码填充程序包和peer protobuf包。接下来,让我们添加一个struct SimpleAsset作为链码填充函数的接收器。

package main

import (
    "fmt"

    "github.com/hyperledger/fabric-chaincode-go/shim"
    "github.com/hyperledger/fabric-protos-go/peer"
)

// SimpleAsset implements a simple chaincode to manage an asset
type SimpleAsset struct {
}

初始化链码

接下来,我们将实现Init函数。

// Init is called during chaincode instantiation to initialize any data.
func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response {

}

请注意,链码升级也会调用此函数。在编写将升级现有链码的链码时,请确保适当地修改Init函数。特别是,如果升级过程中没有“migration”或没有要初始化的内容,请提供一个空的“Init”方法。

接下来,我们将使用ChaincodeStubInterface.GetStringArgs函数检索Init调用的参数并检查其有效性。在我们的例子中,我们需要一个键值对。

// Init is called during chaincode instantiation to initialize any
// data. Note that chaincode upgrade also calls this function to reset
// or to migrate data, so be careful to avoid a scenario where you
// inadvertently clobber your ledger's data!
func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response {
  // Get the args from the transaction proposal
  args := stub.GetStringArgs()
  if len(args) != 2 {
    return shim.Error("Incorrect arguments. Expecting a key and a value")
  }
}

下一步,既然我们已经确定调用是有效的,我们将在账本中存储初始状态。要做到这一点,我们会调用给ChaincodeStubInterface.PutState作为参数传入的键和值。假设一切顺利,返回到一个节点。回应对象,该对象指示初始化成功。

// Init is called during chaincode instantiation to initialize any
// data. Note that chaincode upgrade also calls this function to reset
// or to migrate data, so be careful to avoid a scenario where you
// inadvertently clobber your ledger's data!
func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response {
  // Get the args from the transaction proposal
  args := stub.GetStringArgs()
  if len(args) != 2 {
    return shim.Error("Incorrect arguments. Expecting a key and a value")
  }

  // Set up any variables or assets here by calling stub.PutState()

  // We store the key and the value on the ledger
  err := stub.PutState(args[0], []byte(args[1]))
  if err != nil {
    return shim.Error(fmt.Sprintf("Failed to create asset: %s", args[0]))
  }
  return shim.Success(nil)
}

调用链码

首先,让我们添加Invoke函数的签名。

// Invoke is called per transaction on the chaincode. Each transaction is
// either a 'get' or a 'set' on the asset created by Init function. The 'set'
// method may create a new asset by specifying a new key-value pair.
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {

}

与上面的Init函数一样,我们需要从ChaincodeStubInterface中提取参数。Invoke函数的参数将是要调用的链码应用程序函数的名称。在我们的例子中,我们的应用程序只有两个函数:setget,它们允许设置资产的值或检索其当前状态。我们先调用ChaincodeStubInterface.GetFunctionAndParameters将函数名和参数提取到该链码应用程序函数。

// Invoke is called per transaction on the chaincode. Each transaction is
// either a 'get' or a 'set' on the asset created by Init function. The Set
// method may create a new asset by specifying a new key-value pair.
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
    // Extract the function and args from the transaction proposal
    fn, args := stub.GetFunctionAndParameters()

}

接下来,我们将验证函数名是否为setget,并调用这些链码应用程序函数,通过shim.Success或者shim.Error将响应序列化为gRPC协议消息的函数。

// Invoke is called per transaction on the chaincode. Each transaction is
// either a 'get' or a 'set' on the asset created by Init function. The Set
// method may create a new asset by specifying a new key-value pair.
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
    // Extract the function and args from the transaction proposal
    fn, args := stub.GetFunctionAndParameters()

    var result string
    var err error
    if fn == "set" {
            result, err = set(stub, args)
    } else {
            result, err = get(stub, args)
    }
    if err != nil {
            return shim.Error(err.Error())
    }

    // Return the result as success payload
    return shim.Success([]byte(result))
}

实现链码应用程序

如前所述,我们的chaincode应用程序实现了两个可以通过Invoke函数调用的函数。现在让我们实现这些函数。注意,如上所述,要访问账本的状态,我们将利用ChaincodeStubInterface.PutState还有ChaincodeStubInterface.GetState链码填充程序API的函数。

// Set stores the asset (both key and value) on the ledger. If the key exists,
// it will override the value with the new one
func set(stub shim.ChaincodeStubInterface, args []string) (string, error) {
    if len(args) != 2 {
            return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value")
    }

    err := stub.PutState(args[0], []byte(args[1]))
    if err != nil {
            return "", fmt.Errorf("Failed to set asset: %s", args[0])
    }
    return args[1], nil
}

// Get returns the value of the specified asset key
func get(stub shim.ChaincodeStubInterface, args []string) (string, error) {
    if len(args) != 1 {
            return "", fmt.Errorf("Incorrect arguments. Expecting a key")
    }

    value, err := stub.GetState(args[0])
    if err != nil {
            return "", fmt.Errorf("Failed to get asset: %s with error: %s", args[0], err)
    }
    if value == nil {
            return "", fmt.Errorf("Asset not found: %s", args[0])
    }
    return string(value), nil
}

把一切都运行起来

最后,我们需要添加main函数,它将调用shim。启动功能。这是整个链码程序源代码。

package main

import (
    "fmt"

    "github.com/hyperledger/fabric-chaincode-go/shim"
    "github.com/hyperledger/fabric-protos-go/peer"
)

// SimpleAsset implements a simple chaincode to manage an asset
type SimpleAsset struct {
}

// Init is called during chaincode instantiation to initialize any
// data. Note that chaincode upgrade also calls this function to reset
// or to migrate data.
func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response {
    // Get the args from the transaction proposal
    args := stub.GetStringArgs()
    if len(args) != 2 {
            return shim.Error("Incorrect arguments. Expecting a key and a value")
    }

    // Set up any variables or assets here by calling stub.PutState()

    // We store the key and the value on the ledger
    err := stub.PutState(args[0], []byte(args[1]))
    if err != nil {
            return shim.Error(fmt.Sprintf("Failed to create asset: %s", args[0]))
    }
    return shim.Success(nil)
}

// Invoke is called per transaction on the chaincode. Each transaction is
// either a 'get' or a 'set' on the asset created by Init function. The Set
// method may create a new asset by specifying a new key-value pair.
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
    // Extract the function and args from the transaction proposal
    fn, args := stub.GetFunctionAndParameters()

    var result string
    var err error
    if fn == "set" {
            result, err = set(stub, args)
    } else { // assume 'get' even if fn is nil
            result, err = get(stub, args)
    }
    if err != nil {
            return shim.Error(err.Error())
    }

    // Return the result as success payload
    return shim.Success([]byte(result))
}

// Set stores the asset (both key and value) on the ledger. If the key exists,
// it will override the value with the new one
func set(stub shim.ChaincodeStubInterface, args []string) (string, error) {
    if len(args) != 2 {
            return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value")
    }

    err := stub.PutState(args[0], []byte(args[1]))
    if err != nil {
            return "", fmt.Errorf("Failed to set asset: %s", args[0])
    }
    return args[1], nil
}

// Get returns the value of the specified asset key
func get(stub shim.ChaincodeStubInterface, args []string) (string, error) {
    if len(args) != 1 {
            return "", fmt.Errorf("Incorrect arguments. Expecting a key")
    }

    value, err := stub.GetState(args[0])
    if err != nil {
            return "", fmt.Errorf("Failed to get asset: %s with error: %s", args[0], err)
    }
    if value == nil {
            return "", fmt.Errorf("Asset not found: %s", args[0])
    }
    return string(value), nil
}

// main function starts up the chaincode in the container during instantiate
func main() {
    if err := shim.Start(new(SimpleAsset)); err != nil {
            fmt.Printf("Error starting SimpleAsset chaincode: %s", err)
    }
}

链码访问控制

链码可以通过调用GetCreator()函数来利用客户端(提交者)证书来进行访问控制决策。此外,Go-shim还提供了扩展APIs,从提交者的证书中提取客户端标识,这些标识可用于访问控制决策,无论是基于客户端标识本身,还是基于组织标识,还是基于客户端标识属性。

例如,表示为键/值的资产可能包含客户机的标识作为值的一部分(例如,作为表示资产所有者的JSON属性),并且只有此客户机才有权在将来更新键/值。客户端标识库扩展APIs可以在链码中使用,以检索提交者信息,从而做出这样的访问控制决策。

有关更多详细信息,请参阅客户端标识(CID)库文档

要将客户端标识填充程序扩展作为依赖项添加到链码中,请参见管理在Go中编写的链码的外部依赖项


管理Go中编写的链码的外部依赖关系

Go链码需要不属于Go标准库的包(如链码填充程序)。这些包必须包含在链码包中。

有许多工具可用于管理(或“供应商”)这些依赖关系。下面演示如何使用govendor

govendor init
govendor add +external  // Add all external package, or
govendor add github.com/external/pkg // Add specific external package

这会将外部依赖项导入到本地vendor目录中。如果您是依赖Fabric shim或者shim扩展,在执行govendor命令之前,请将结构存储库克隆到您的$GOPATH/src/github.com/hyperledger目录。

一旦依赖项在您的链码目录中vendored,peer chaincode packagepeer chaincode install操作将把与依赖项关联的代码包含到链码包中。


建立你的第一个网络

注意:Build your first network(BYFN)教程已被弃用。如果您刚开始使用Hyperledger Fabric并希望部署基本网络,请参阅使用Fabric测试网络。如果要在生产中部署Fabric,请参阅《部署生产网络指南》。

构建您的第一个网络(BYFN)场景提供了一个由两个组织组成的示例Hyperledger Fabric网络,每个组织维护两个节点。它还将默认部署一个Raft排序服务。


安装必备组件

在我们开始之前,如果您还没有这样做,您可能希望检查您是否已在开发区块链应用程序和/或操作超级账本结构的平台上安装了所有必备组件

您还需要安装示例、二进制文件和Docker镜像。您将注意到fabric-sample存储库中包含许多示例。我们将使用first-network样本。现在打开这个子目录。

cd fabric-samples/first-network

本文档中提供的命令必须fabric-samples存储库克隆的first-network子目录运行。如果选择从其他位置运行命令,则提供的各种脚本将无法找到二进制文件。


想现在运行吗?

我们提供了一个完整的注释脚本-byfn.sh-它利用这些Docker镜像快速引导一个Hyperledger Fabric网络,该网络默认由代表两个不同组织的四个节点和一个Raft排序服务组成。它还将启动一个容器来运行脚本化的执行,该执行将把节点连接到一个通道,部署一个链码,并根据部署的链码驱动交易的执行。

以下是byfn.sh脚本:

Usage:
byfn.sh <mode> [-c <channel name>] [-t <timeout>] [-d <delay>] [-f <docker-compose-file>] [-s <dbtype>] [-l <language>] [-o <consensus-type>] [-i <imagetag>] [-v]"
  <mode> - one of 'up', 'down', 'restart', 'generate' or 'upgrade'"
    - 'up' - bring up the network with docker-compose up"
    - 'down' - clear the network with docker-compose down"
    - 'restart' - restart the network"
    - 'generate' - generate required certificates and genesis block"
    - 'upgrade'  - upgrade the network from version 1.3.x to 1.4.0"
  -c <channel name> - channel name to use (defaults to \"mychannel\")"
  -t <timeout> - CLI timeout duration in seconds (defaults to 10)"
  -d <delay> - delay duration in seconds (defaults to 3)"
  -f <docker-compose-file> - specify which docker-compose file use (defaults to docker-compose-cli.yaml)"
  -s <dbtype> - the database backend to use: goleveldb (default) or couchdb"
  -l <language> - the chaincode language: golang (default), javascript, or java"
  -a - launch certificate authorities (no certificate authorities are launched by default)
  -n - do not deploy chaincode (abstore chaincode is deployed by default)
  -i <imagetag> - the tag to be used to launch the network (defaults to \"latest\")"
  -v - verbose mode"
byfn.sh -h (print this message)"

Typically, one would first generate the required certificates and
genesis block, then bring up the network. e.g.:"

  byfn.sh generate -c mychannel"
  byfn.sh up -c mychannel -s couchdb"
  byfn.sh up -c mychannel -s couchdb -i 1.4.0"
  byfn.sh up -l javascript"
  byfn.sh down -c mychannel"
  byfn.sh upgrade -c mychannel"

Taking all defaults:"
      byfn.sh generate"
      byfn.sh up"
      byfn.sh down"

如果选择不提供标志,脚本将使用默认值。

生成网络工件

准备好试一试了吗?那好吧!执行以下命令:

./byfn.sh generate

您将看到关于将发生什么的简要描述,以及yes/no命令行提示符。用y或按回车键来执行所描述的操作。

Generating certs and genesis block for channel 'mychannel' with CLI timeout of '10' seconds and CLI delay of '3' seconds
Continue? [Y/n] y
proceeding ...
/Users/xxx/dev/fabric-samples/bin/cryptogen

##########################################################
##### Generate certificates using cryptogen tool #########
##########################################################
org1.example.com
2017-06-12 21:01:37.334 EDT [bccsp] GetDefault -> WARN 001 Before using BCCSP, please call InitFactories(). Falling back to bootBCCSP.
...

/Users/xxx/dev/fabric-samples/bin/configtxgen
##########################################################
#########  Generating Orderer Genesis block ##############
##########################################################
2017-06-12 21:01:37.558 EDT [common/configtx/tool] main -> INFO 001 Loading configuration
2017-06-12 21:01:37.562 EDT [msp] getMspConfig -> INFO 002 intermediate certs folder not found at [/Users/xxx/dev/byfn/crypto-config/ordererOrganizations/example.com/msp/intermediatecerts]. Skipping.: [stat /Users/xxx/dev/byfn/crypto-config/ordererOrganizations/example.com/msp/intermediatecerts: no such file or directory]
...
2017-06-12 21:01:37.588 EDT [common/configtx/tool] doOutputBlock -> INFO 00b Generating genesis block
2017-06-12 21:01:37.590 EDT [common/configtx/tool] doOutputBlock -> INFO 00c Writing genesis block

#################################################################
### Generating channel configuration transaction 'channel.tx' ###
#################################################################
2017-06-12 21:01:37.634 EDT [common/configtx/tool] main -> INFO 001 Loading configuration
2017-06-12 21:01:37.644 EDT [common/configtx/tool] doOutputChannelCreateTx -> INFO 002 Generating new channel configtx
2017-06-12 21:01:37.645 EDT [common/configtx/tool] doOutputChannelCreateTx -> INFO 003 Writing new channel tx

#################################################################
#######    Generating anchor peer update for Org1MSP   ##########
#################################################################
2017-06-12 21:01:37.674 EDT [common/configtx/tool] main -> INFO 001 Loading configuration
2017-06-12 21:01:37.678 EDT [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO 002 Generating anchor peer update
2017-06-12 21:01:37.679 EDT [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO 003 Writing anchor peer update

#################################################################
#######    Generating anchor peer update for Org2MSP   ##########
#################################################################
2017-06-12 21:01:37.700 EDT [common/configtx/tool] main -> INFO 001 Loading configuration
2017-06-12 21:01:37.704 EDT [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO 002 Generating anchor peer update
2017-06-12 21:01:37.704 EDT [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO 003 Writing anchor peer update

第一步为我们的各种网络实体生成所有证书和密钥,用于引导排序服务的创世区块,以及配置通道所需的配置交易集合。

启动网络

接下来,您可以使用以下命令之一启动网络:

./byfn.sh up

上面的命令将编译Golang链码镜像并启动相应的容器。Go是默认的链码语言,但是也支持node.js以及Java链码。如果要使用节点链码运行本教程,请改为传递以下命令:

# we use the -l flag to specify the chaincode language
# forgoing the -l flag will default to Golang

./byfn.sh up -l javascript

注意:有关node.js shim,请参考其文件。有关Java shim的更多信息,请参阅其文档

要使用Java链码运行示例,必须指定-l Java,如下所示:

./byfn.sh up -l java

注意:不要同时运行这两个命令。只有一种语言可以尝试,除非您关闭并重新创建之间的网络。

系统将提示您是要继续还是要中止。用y或按回车键:

Starting for channel 'mychannel' with CLI timeout of '10' seconds and CLI delay of '3' seconds
Continue? [Y/n]
proceeding ...
Creating network "net_byfn" with the default driver
Creating peer0.org1.example.com
Creating peer1.org1.example.com
Creating peer0.org2.example.com
Creating orderer.example.com
Creating peer1.org2.example.com
Creating cli


 ____    _____      _      ____    _____
/ ___|  |_   _|    / \    |  _ \  |_   _|
\___ \    | |     / _ \   | |_) |   | |
 ___) |   | |    / ___ \  |  _ <    | |
|____/    |_|   /_/   \_\ |_| \_\   |_|

Channel name : mychannel
Creating channel...

日志将从那里继续。这将启动所有容器,然后驱动一个完整的端到端应用程序场景。成功完成后,它应在终端窗口中报告以下内容:

Query Result: 90
2017-05-16 17:08:15.158 UTC [main] main -> INFO 008 Exiting.....
===================== Query successful on peer1.org2 on channel 'mychannel' =====================

===================== All GOOD, BYFN execution completed =====================


 _____   _   _   ____
| ____| | \ | | |  _ \
|  _|   |  \| | | | | |
| |___  | |\  | | |_| |
|_____| |_| \_| |____/

您可以在这些日志中滚动查看各种交易。如果您没有得到这个结果,那么跳到故障排除部分,让我们看看我们是否可以帮助您发现哪里出了问题。

关闭网络

最后,让我们把它全部放下,这样我们就可以一步一步地探索网络设置。以下操作将杀死您的容器,移除加密材料和四个工件,并从Docker注册表中删除链码镜像:

./byfn.sh down

再次提示您继续,输入y或按回车键:

Stopping with channel 'mychannel' and CLI timeout of '10'
Continue? [Y/n] y
proceeding ...
WARNING: The CHANNEL_NAME variable is not set. Defaulting to a blank string.
WARNING: The TIMEOUT variable is not set. Defaulting to a blank string.
Removing network net_byfn
468aaa6201ed
...
Untagged: dev-peer1.org2.example.com-mycc-1.0:latest
Deleted: sha256:ed3230614e64e1c83e510c0c282e982d2b06d148b1c498bbdcc429e2b2531e91
...

如果您想进一步了解底层工具和引导机制,请继续阅读。在接下来的几节中,我们将介绍构建一个功能齐全的Hyperledger Fabric网络的各种步骤和要求。

注意:下面概述的手动步骤假定cli容器中的FABRIC_LOGGING_SPEC设置为DEBUG。您可以通过修改first-network目录中的docker-compose-cli.yaml文件来设置它。例如

cli:
  container_name: cli
  image: hyperledger/fabric-tools:$IMAGE_TAG
  tty: true
  stdin_open: true
  environment:
    - GOPATH=/opt/gopath
    - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
    - FABRIC_LOGGING_SPEC=DEBUG
    #- FABRIC_LOGGING_SPEC=INFO

密码生成器

我们将使用cryptogen工具为各种网络实体生成加密材料(x509证书和签名密钥)。这些证书代表身份,它们允许在我们的实体通信和交易时进行签名/验证身份验证。

它是如何工作的?

Cryptogen使用一个文件-crypto-config.yaml-它包含网络拓扑,并允许我们为组织和属于这些组织的组件生成一组证书和密钥。每个组织都被提供一个唯一的根证书(ca-cert),该证书将特定组件(普通节点和排序节点)绑定到该组织。通过为每个组织分配一个唯一的CA证书,我们模拟了一个典型的网络,其中一个参与成员将使用自己的证书颁发机构。Hyperledger Fabric中的交易和通信由实体的私钥(keystore)签名,然后通过公钥(signcerts)进行验证。

您会注意到这个文件中有一个count变量。我们使用它来指定每个组织的节点数量;在我们的例子中,每个组织有两个节点。我们现在不会深入研究x.509证书和公钥基础设施的细节。如果你感兴趣,你可以在自己的时间阅读这些主题。

运行cryptogen工具后,生成的证书和密钥将保存到名为crypto-config的文件夹中。注意crypto-config.yaml文件列出了与排序组织绑定的五个排序节点。而cryptogen工具将为所有五个排序节点创建证书。这些order将在etcdraft排序服务实现中使用,并用于创建系统通道和mychannel


配置交易生成器

configtxgen工具用于创建四个配置构件:

  • orderer 创世区块
  • 通道配置交易
  • 两个锚节点交易-每个节点一个。

有关此工具功能的完整说明,请参阅configtxgen

order块是ordering服务的Genesis区块,在通道创建时,通道配置交易文件被广播给order。顾名思义,锚节点交易指定了这个通道上每个组织的锚节点。

它是如何工作的?

Configtxgen使用文件-configtx.yaml-包含示例网络的定义。有三个成员-一个OrderOrg(OrderOrg)和两个节点组织(Org1Org2),每个成员管理和维护两个节点。这个文件还指定了一个由两个节点组织组成的联盟SampleConsortium。请特别注意此文件底部的“Profiles”部分。你会注意到我们有几个独特的配置文件。值得注意的是:

  • SampleMultiNodeEtcdRaft:为Raft排序服务生成创世区块。仅在发出-o标志并指定etcdraft时使用。
  • Twoorkschannel:为我们的通道生成“创世”块,mychannel

这些头很重要,因为我们在创建工件时会将它们作为参数传入。

请注意,我们的SampleConsortium是在系统级概要文件中定义的,然后由我们的通道级概要文件引用。通道存在于联盟的权限内,所有联盟必须在整个网络范围内加以界定。

请注意,我们的SampleConsortium是在系统级概要文件中定义的,然后由我们的通道级概要文件引用。渠道存在于财团的权限内,所有财团必须在整个网络范围内加以界定。

该文件还包含两个值得注意的附加规范。首先,我们为每个节点组织(peer0.org1.example.com & peer0.org2.example.com)指定锚节点。其次,我们指向每个成员的MSP目录的位置,从而允许我们将每个组织的根证书存储在order genesis区块中。这是一个关键的概念。现在,任何与排序服务通信的网络实体都可以验证其数字签名。


运行工具

您可以使用configtxgencryptogen命令手动生成证书/密钥和各种配置构件。或者,您可以尝试调整byfn.sh编写完成目标的脚本。

手动生成工件

您可以参考byfn.sh脚本中的generateCerts函数来获取生成将用于crypto-config.yaml文件中定义的网络配置的证书所需的命令。不过,为了方便起见,我们也会在这里提供参考。

首先让我们运行cryptogen工具。我们的二进制文件在bin目录中,所以我们需要提供工具所在位置的相对路径。

../bin/cryptogen generate --config=./crypto-config.yaml

您应该在终端中看到以下内容:

org1.example.com
org2.example.com

证书和密钥(即MSP材料)将被输出到first-network根目录下的一个目录crypto-config

接下来,我们需要告诉configtxgen工具在哪里查找configtx.yaml它需要摄取的文件。我们将在我们目前的工作目录中告诉它:

export FABRIC_CFG_PATH=$PWD

然后,我们将调用configtxgen工具来创建排序节点的创世区块:

../bin/configtxgen -profile SampleMultiNodeEtcdRaft -channelID byfn-sys-channel -outputBlock ./channel-artifacts/genesis.block

注意:排序节点的创世区块和我们将要创建的后续构件将输出到first-network目录下的channel-artifacts目录中。上面命令中的channelID是系统通道的名称。

创建通道配置交易

接下来,我们需要创建通道交易工件。请确保替换$CHANNEL_NAME或将CHANNEL_NAME设置为可在以下说明中使用的环境变量:

# The channel.tx artifact contains the definitions for our sample channel

export CHANNEL_NAME=mychannel  && ../bin/configtxgen -profile TwoOrgsChannel -outputCreateChannelTx ./channel-artifacts/channel.tx -channelID $CHANNEL_NAME

请注意,Twoorkschannel概要文件将使用您在为网络创建创世区块时指定的排序服务配置。

您应该会在终端中看到类似于以下内容的输出:

2017-10-26 19:24:05.324 EDT [common/tools/configtxgen] main -> INFO 001 Loading configuration
2017-10-26 19:24:05.329 EDT [common/tools/configtxgen] doOutputChannelCreateTx -> INFO 002 Generating new channel configtx
2017-10-26 19:24:05.329 EDT [common/tools/configtxgen] doOutputChannelCreateTx -> INFO 003 Writing new channel tx

接下来,我们将在我们正在构建的通道上为Org1定义锚节点。同样,请确保替换$CHANNEL_NAME或为以下命令设置环境变量。终端输出将模拟通道交易工件的输出:

../bin/configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./channel-artifacts/Org1MSPanchors.tx -channelID $CHANNEL_NAME -asOrg Org1MSP

现在,我们将在同一个通道上为Org2定义锚节点:

../bin/configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./channel-artifacts/Org2MSPanchors.tx -channelID $CHANNEL_NAME -asOrg Org2MSP

启动网络

如果你运行byfn.sh上面的示例,请确保在继续之前已关闭测试网络(请参阅关闭网络)。

我们将利用一个脚本来启动我们的网络。docker-compose文件引用了我们之前下载的图像,并使用之前生成的镜像引导ordergenesis.block

我们希望手动查看这些命令,以便公开每个调用的语法和功能。

首先让我们开始我们的网络:

docker-compose -f docker-compose-cli.yaml -f docker-compose-etcdraft2.yaml up -d

如果要查看网络的实时日志,请不要提供-d标志。如果让日志流化,则需要打开第二个终端来执行CLI调用。

创建和加入频道

回想一下,我们在上面的Create a channel configuration transaction部分中使用configtxgen工具创建了通道配置交易。您可以重复该过程来创建其他通道配置事务,使用configtx.yaml中传递给configtxgen工具的相同或不同的配置文件。然后,您可以重复本节中定义的过程,在您的网络中建立其他通道。
我们将使用docker exec命令进入CLI容器:

docker exec -it cli bash

如果成功,您将看到以下内容:

bash-5.0#

为了让下面针对peer0.org1.example.com的CLI命令正常工作,我们需要在命令前面添加下面给出的四个环境变量。peer0.org1.example.com的这些变量被印到CLI容器中,因此我们可以在不传递它们的情况下进行操作。但是,如果要向其他普通节点或排序节点发送调用,请保持CLI容器的默认值以peer0.org1.example.com为目标,但在进行任何CLI调用时,将覆盖以下示例中所示的环境变量:

# Environment variables for PEER0

CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
CORE_PEER_ADDRESS=peer0.org1.example.com:7051
CORE_PEER_LOCALMSPID="Org1MSP"
CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt

接下来,我们将传递在create a channel configuration transaction部分中创建的生成的通道配置交易工件(我们称之为channel.tx)作为创建通道请求的一部分。

我们用-c标志指定通道名称,用-f标志指定通道配置交易。在这种情况下是这样的channel.tx,但是,您可以使用其他名称装载自己的配置交易。再次,我们将在CLI容器中设置CHANNEL_NAME环境变量,这样就不必显式地传递此参数。通道名称必须全部为小写,长度小于250个字符,并与正则表达式[a-z][a-z0-9.-]*匹配。

export CHANNEL_NAME=mychannel

# the channel.tx file is mounted in the channel-artifacts directory within your CLI container
# as a result, we pass the full path for the file
# we also pass the path for the orderer ca-cert in order to verify the TLS handshake
# be sure to export or replace the $CHANNEL_NAME variable appropriately

peer channel create -o orderer.example.com:7050 -c $CHANNEL_NAME -f ./channel-artifacts/channel.tx --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

请注意我们作为此命令的一部分传递的--cafile。它是排序者根证书的本地路径,允许我们验证TLS握手。

此命令返回genesis block-<CHANNEL_NAME.block>-我们将用它加入通道。它包含中指定的配置信息channel.tx如果您没有对默认通道名进行任何修改,那么命令将返回一个名为mychannel.block的proto。

对于这些手动命令的其余部分,您将保留在CLI容器中。当目标对象不是peer0.org1.example.com时,还必须记住在所有命令前面加上相应的环境变量。

现在让我们加入peer0.org1.example.com通道。

# By default, this joins ``peer0.org1.example.com`` only
# the <CHANNEL_NAME.block> was returned by the previous command
# if you have not modified the channel name, you will join with mychannel.block
# if you have created a different channel name, then pass in the appropriately named block

 peer channel join -b mychannel.block

您可以根据需要,通过在上面的Create & Join Channel部分中使用的四个环境变量进行适当的更改,使其他节点加入通道。

我们将简单地加入peer0.org2.example.com,而不是加入每一个节点,这样我们就可以正确地更新通道中的锚节点定义。由于我们要覆盖到CLI容器中的默认环境变量,因此此完整命令将如下所示:

CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp CORE_PEER_ADDRESS=peer0.org2.example.com:9051 CORE_PEER_LOCALMSPID="Org2MSP" CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt peer channel join -b mychannel.block

或者,可以选择单独设置这些环境变量,而不是传递整个字符串。一旦设置好它们,您只需再次发出peer channel join命令,CLI容器将代表peer0.org2.example.com执行操作。


更新锚节点

以下命令是通道更新,它们将传播到通道的定义中。本质上,我们在通道的genesis块上添加了额外的配置信息。请注意,我们没有修改genesis区块,只是简单地在定义锚节点的链中添加delta。

更新通道定义以将Org1的定位点对等定义为peer0.org1.example.com

peer channel update -o orderer.example.com:7050 -c $CHANNEL_NAME -f ./channel-artifacts/Org1MSPanchors.tx --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

现在更新通道定义,将Org2的锚节点定义为peer0.org2.example.com。与Org2节点的peer channel join命令相同,我们需要在调用之前添加适当的环境变量。

CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp CORE_PEER_ADDRESS=peer0.org2.example.com:9051 CORE_PEER_LOCALMSPID="Org2MSP" CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt peer channel update -o orderer.example.com:7050 -c $CHANNEL_NAME -f ./channel-artifacts/Org2MSPanchors.tx --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

安装并定义链码

注意:我们将使用一个简单的现有链码。要了解如何编写自己的链码,请参阅开发人员的链码教程

注意:这些说明使用v2.0版本中引入的Fabric链码生命周期。如果您想使用以前的生命周期来安装和实例化链码,请访问构建第一个网络教程的v1.4版本。

应用程序通过链码与区块链账本交互。因此,我们需要在每一个执行和背书我们的交易的节点上安装一个链码。然而,在我们可以与我们的链码交互之前,通道成员需要就建立链码治理的链码定义达成一致。

我们需要打包链码,然后才能将其安装到我们的对等机上。对于您创建的每个包,您需要提供一个链码包标签作为链码的描述。使用以下命令打包示例Go,node.js或Java链代码。

Golang

# before packaging Golang chaincode, vendoring Go dependencies is required like the following commands.
cd /opt/gopath/src/github.com/hyperledger/fabric-samples/chaincode/abstore/go
GO111MODULE=on go mod vendor
cd -

# this packages a Golang chaincode.
# make note of the --lang flag to indicate "golang" chaincode
# for go chaincode --path takes the relative path from $GOPATH/src
# The --label flag is used to create the package label
peer lifecycle chaincode package mycc.tar.gz --path github.com/hyperledger/fabric-samples/chaincode/abstore/go/ --lang golang --label mycc_1

Node.js

# this packages a Node.js chaincode
# make note of the --lang flag to indicate "node" chaincode
# for node chaincode --path takes the absolute path to the node.js chaincode
# The --label flag is used to create the package label
peer lifecycle chaincode package mycc.tar.gz --path /opt/gopath/src/github.com/hyperledger/fabric-samples/chaincode/abstore/javascript/ --lang node --label mycc_1

Java

# this packages a java chaincode
# make note of the --lang flag to indicate "java" chaincode
# for java chaincode --path takes the absolute path to the java chaincode
# The --label flag is used to create the package label
peer lifecycle chaincode package mycc.tar.gz --path /opt/gopath/src/github.com/hyperledger/fabric-samples/chaincode/abstore/java/ --lang java --label mycc_1

上面的每个命令都将创建一个名为mycc.tar.gz的链码包,我们可以用它在我们的节点上安装链码。发出以下命令在Org1的peer0上安装包。

# this command installs a chaincode package on your peer
peer lifecycle chaincode install mycc.tar.gz

成功的安装命令将返回链码包标识符。您将看到类似于以下内容的输出:

2019-03-13 13:48:53.691 UTC [cli.lifecycle.chaincode] submitInstallProposal -> INFO 001 Installed remotely: response:<status:200 payload:"\nEmycc_1:3a8c52d70c36313cfebbaf09d8616e7a6318ababa01c7cbe40603c373bcfe173" >
2019-03-13 13:48:53.691 UTC [cli.lifecycle.chaincode] submitInstallProposal -> INFO 002 Chaincode code package identifier: mycc_1:3a8c52d70c36313cfebbaf09d8616e7a6318ababa01c7cbe40603c373bcfe173

您还可以通过查询节点以获取有关已安装包的信息来找到链码包标识符。

# this returns the details of the chaincode packages installed on your peers
peer lifecycle chaincode queryinstalled

上面的命令将返回与install命令相同的包标识符。您将看到类似于以下内容的输出:

Get installed chaincodes on peer:
Package ID: mycc_1:3a8c52d70c36313cfebbaf09d8616e7a6318ababa01c7cbe40603c373bcfe173, Label: mycc_1

我们将需要包ID以用于将来的命令,所以让我们继续并将其保存为环境变量。将peer lifecycle chaincode queryinstalled命令返回的包ID粘贴到下面的命令中。对于所有用户,包ID可能不相同,因此您需要使用从控制台返回的包ID来完成此步骤。

# Save the package ID as an environment variable.

CC_PACKAGE_ID=mycc_1:3a8c52d70c36313cfebbaf09d8616e7a6318ababa01c7cbe40603c373bcfe173

mycc的背书策略将被设置为需要来自Org1和Org2中的节点的背书。因此,我们还需要在Org2中的节点上安装链码。
修改以下四个环境变量,以Org2的形式发出install命令:

# Environment variables for PEER0 in Org2

CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
CORE_PEER_ADDRESS=peer0.org2.example.com:9051
CORE_PEER_LOCALMSPID="Org2MSP"
CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt

现在将chaincode包安装到Org2的peer0上。下面的命令将安装链码并返回与我们作为Org1发出的install命令相同的标识符。

# this installs a chaincode package on your peer
peer lifecycle chaincode install mycc.tar.gz

安装包后,需要批准组织的链码定义。链码定义包括链码管理的重要参数,包括链码名称和版本。定义还包括包标识符,用于将安装在节点上的链码包与组织批准的链码定义相关联。

因为我们将环境变量设置为Org2操作,所以我们可以使用下面的命令来批准Org2的myccchaincode的定义。批准将分发给每个组织中的节点,因此命令不需要针对组织中的每个节点。

# this approves a chaincode definition for your org
# make note of the --package-id flag that provides the package ID
# use the --init-required flag to request the ``Init`` function be invoked to initialize the chaincode
peer lifecycle chaincode approveformyorg --channelID $CHANNEL_NAME --name mycc --version 1.0 --init-required --package-id $CC_PACKAGE_ID --sequence 1 --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

我们可以向上面的命令提供--signature policy--channel config policy参数来设置链码背书策略。背书策略指定属于不同通道成员的节点需要根据给定的链码验证交易。因为我们没有设置策略,mycc的定义将使用默认的背书策略,该策略要求在提交交易时,交易必须得到大多数通道成员的认可。这意味着,如果在通道中添加或删除了新组织,则会自动更新背书策略,以要求更多或更少的背书。在本教程中,默认策略将需要来自属于Org1Org2的节点的背书(即两个背书)。有关策略执行的更多详细信息,请参阅背书策略文档。

所有组织在使用链码之前需要就定义达成一致。修改以下四个环境变量以作为Org1操作:

# Environment variables for PEER0

CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
CORE_PEER_ADDRESS=peer0.org1.example.com:7051
CORE_PEER_LOCALMSPID="Org1MSP"
CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt

现在可以批准mycc链码的定义Org1。链码在组织级别得到批准。即使有多个节点,也可以发出一次命令。

# this defines a chaincode for your org
# make note of the --package-id flag that provides the package ID
# use the --init-required flag to request the Init function be invoked to initialize the chaincode
peer lifecycle chaincode approveformyorg --channelID $CHANNEL_NAME --name mycc --version 1.0 --init-required --package-id $CC_PACKAGE_ID --sequence 1 --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

一旦有足够数量的通道成员批准了链码定义,一个成员就可以将定义提交给通道。默认情况下,大多数通道成员需要在提交定义之前对其进行批准。通过发出以下查询,可以检查链码定义是否已准备好提交,并按组织查看当前审批:

# the flags used for this command are identical to those used for approveformyorg
# except for --package-id which is not required since it is not stored as part of
# the definition
peer lifecycle chaincode checkcommitreadiness --channelID $CHANNEL_NAME --name mycc --version 1.0 --init-required --sequence 1 --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 --output json

该命令将生成一个JSON映射作为输出,显示通道中的组织是否批准了checkcommitreadency命令中提供的链码定义。在这种情况下,鉴于两个组织都已批准,我们获得:

{
        "Approvals": {
                "Org1MSP": true,
                "Org2MSP": true
        }
}

由于两个通道成员都批准了该定义,我们现在可以使用以下命令将其提交给通道。您可以将此命令作为Org1或Org2发出。请注意,交易的目标是Org1和Org2中的节点来收集背书。

# this commits the chaincode definition to the channel
peer lifecycle chaincode commit -o orderer.example.com:7050 --channelID $CHANNEL_NAME --name mycc --version 1.0 --sequence 1 --init-required --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 --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt

调用链码

在将链码定义提交给通道后,我们就可以调用链码并开始与账本交互。我们使用--Init required标志请求执行链码定义中的Init函数。因此,我们需要将--isInit标志传递给它的第一次调用,并为Init函数提供参数。发出以下命令初始化链码并将初始数据放入账本。

# be sure to set the -C and -n flags appropriately
# use the --isInit flag if you are invoking an Init function
peer chaincode invoke -o orderer.example.com:7050 --isInit --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 $CHANNEL_NAME -n mycc --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"Args":["Init","a","100","b","100"]}' --waitForEvent

第一个调用将启动链码容器。我们可能需要等待容器启动。node.js镜像需要更长的时间。

查询

让我们查询链码,以确保容器已正确启动并且状态DB已填充。查询语法如下:

# be sure to set the -C and -n flags appropriately

peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["query","a"]}'

调用

现在让我们把10a移到b,这个交易将剪切一个新的块并更新状态DB。invoke的语法如下:

# be sure to set the -C and -n flags appropriately
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 $CHANNEL_NAME -n mycc --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"Args":["invoke","a","b","10"]}' --waitForEvent

查询

让我们确认之前的调用是否正确执行。我们用一个值100初始化了密钥a,并在上一次调用中删除了10。因此,对a的查询应该返回90。查询的语法如下。

# be sure to set the -C and -n flags appropriately

peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["query","a"]}'

我们应该看到以下几点:

Query Result: 90

在其他节点上安装链码

如果您希望其他节点与账本交互,则需要将它们连接到通道,并在节点上安装相同的链码包。您只需要从组织中批准一次链码定义。当每个节点尝试与特定的链码交互时,将为每个接单启动链码容器。再次,要认识到node.js在第一次调用时构建和启动镜像会比较慢。

我们将在第三个节点上安装链码,即Org2中的peer1。修改以下四个环境变量以针对Org2中的peer1发出install命令:

# Environment variables for PEER1 in Org2

CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
CORE_PEER_ADDRESS=peer1.org2.example.com:10051
CORE_PEER_LOCALMSPID="Org2MSP"
CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer1.org2.example.com/tls/ca.crt

现在在Org2的peer1上安装mycc包:

# this command installs a chaincode package on your peer
peer lifecycle chaincode install mycc.tar.gz

查询

让我们确认可以向Org2中的Peer1发出查询。我们用一个值100初始化了密钥a,并在上一次调用中删除了10。因此,对a的查询仍应返回90

Org2中的Peer1必须首先加入通道,然后才能响应查询。可以通过发出以下命令来连接通道:

CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp CORE_PEER_ADDRESS=peer1.org2.example.com:10051 CORE_PEER_LOCALMSPID="Org2MSP" CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer1.org2.example.com/tls/ca.crt peer channel join -b mychannel.block

join命令返回后,可以发出查询。查询的语法如下。

# be sure to set the -C and -n flags appropriately

peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["query","a"]}'

我们应该看到以下几点:

Query Result: 90

如果您收到错误,可能是因为节点加入并赶上当前区块链高度需要几秒钟的时间。您可以根据需要重新查询。也可以随意执行其他调用。

幕后发生了什么?

这些步骤描述了‘./byfn.sh up’运行script.sh的场景。使用./byfn.sh down清理网络并确保此命令处于活动状态。然后使用相同的docker-compose提示再次启动网络

  • 脚本-script.sh-在CLI容器中运行。脚本根据提供的通道名称驱动createChannel命令,并使用channel.tx文件进行通道配置。
  • createChannel的输出是一个创世区块-<your_channel_name>.block-它存储在节点的文件系统上,并包含从channel.tx指定的通道配置。
  • joinChannel命令用于所有四个节点,它将先前生成的创世区块作为输入。此命令指示节点加入<your_channel_name>并创建一个以<your_channel_name>.block开始的链。
  • 现在我们有一个由四个节点和两个组织组成的通道。这是我们的TwoOrgsChannel简介。
  • peer0.org1.example.compeer1.org1.example.com属于Org1;peer0.org2.example.com还有peer1.org2.example.com属于Org2
  • 这些关系是通过crypto-config.yaml定义的,MSP路径是在我们的docker compose中指定的。
  • Org1MSP(peer0.org1.example.com)和Org2MSP(peer0.org2.example.com)的锚节点随后更新。我们通过将Org1MSPanchors.txOrg2MSPanchors.tx工件与通道名称一起传递给排序服务来实现这一点。
  • peer0.org1.example.compeer0.org2.example.com上封装并安装了一个链码abstore
  • 然后,链码由Org1和Org2分别批准,然后提交到通道上。由于未指定背书策略,因此将利用大多数组织的通道默认背书策略,这意味着任何交易都必须由绑定到Org1和Org2的节点背书。
  • 然后调用chaincode Init来启动目标节点的容器,并初始化与链码关联的键值对。本例的初始值为[“a”,“100”“b”,“200”]。第一次调用会产生一个名为dev-peer0.org2.example.com-mycc-1.0的容器。
  • 对“A”值的查询被发送到peer0.org2.example.com。初始化链码时,启动了名为dev-peer0.org2.example.com-mycc-1.0的Org2 peer0容器。返回查询结果。没有执行写入操作,因此对“a”的查询仍将返回值“100”。
  • 调用被发送到peer0.org1.example.compeer0.org2.example.com以将“10”从“a”移到“b”
  • peer0.org2.example.com发送一个关于“A”值的查询。返回值90,正确地反映了前一个交易,在此期间键“A”的值被修改了10。
  • 链码abstore安装在peer1.org2.example.com上。
  • peer1.org2.example.com发送一个关于“A”值的查询。这将启动名为dev-peer1.org2.example.com-mycc-1.0的第三个链码容器。返回值90,正确地反映了前一个交易,在此期间键“A”的值被修改了10。

这说明了什么?

链码必须安装在节点上,才能成功对账本执行读/写操作。此外,链码容器不会为节点启动,直到对该链码执行init或传统交易-读/写(例如查询“a”的值)。交易导致容器启动。此外,一个通道中的所有节点都保留一份账本的精确副本,该账本由区块链组成,用于在区块中存储不可变、已排序的记录,以及一个状态数据库,以维护当前状态的快照。这包括那些没有安装链码的节点(如上面例子中的peer1.org1.example.com)。最后,链码在安装后是可以访问的(就像上面例子中的peer1.org2.example.com),因为它的定义已经在通道上提交。

我如何看待这些交易?

检查CLI Docker容器的日志。

docker logs -f cli

您将看到以下输出:

2017-05-16 17:08:01.366 UTC [msp] GetLocalMSP -> DEBU 004 Returning existing local MSP
2017-05-16 17:08:01.366 UTC [msp] GetDefaultSigningIdentity -> DEBU 005 Obtaining default signing identity
2017-05-16 17:08:01.366 UTC [msp/identity] Sign -> DEBU 006 Sign: plaintext: 0AB1070A6708031A0C08F1E3ECC80510...6D7963631A0A0A0571756572790A0161
2017-05-16 17:08:01.367 UTC [msp/identity] Sign -> DEBU 007 Sign: digest: E61DB37F4E8B0D32C9FE10E3936BA9B8CD278FAA1F3320B08712164248285C54
Query Result: 90
2017-05-16 17:08:15.158 UTC [main] main -> INFO 008 Exiting.....
===================== Query successful on peer1.org2 on channel 'mychannel' =====================

===================== All GOOD, BYFN execution completed =====================


 _____   _   _   ____
| ____| | \ | | |  _ \
|  _|   |  \| | | | | |
| |___  | |\  | | |_| |
|_____| |_| \_| |____/

您可以在这些日志中滚动查看各种交易。

如何查看链码日志?

您可以检查各个链码容器,以查看针对每个容器执行的单独交易。使用以下命令查找正在运行的容器列表,以查找链码容器:

$ docker ps -a
CONTAINER ID        IMAGE                                                                                                                                                                 COMMAND                  CREATED              STATUS              PORTS                                NAMES
7aa7d9e199f5        dev-peer1.org2.example.com-mycc_1-27ef99cb3cbd1b545063f018f3670eddc0d54f40b2660b8f853ad2854c49a0d8-2eba360c66609a3ba78327c2c86bc3abf041c78f5a35553191a1acf1efdd5a0d   "chaincode -peer.add…"   About a minute ago   Up About a minute                                        dev-peer1.org2.example.com-mycc_1-27ef99cb3cbd1b545063f018f3670eddc0d54f40b2660b8f853ad2854c49a0d8
82ce129c0fe6        dev-peer0.org2.example.com-mycc_1-27ef99cb3cbd1b545063f018f3670eddc0d54f40b2660b8f853ad2854c49a0d8-1297906045aa77086daba21aba47e8eef359f9498b7cb2b010dff3e2a354565a   "chaincode -peer.add…"   About a minute ago   Up About a minute                                        dev-peer0.org2.example.com-mycc_1-27ef99cb3cbd1b545063f018f3670eddc0d54f40b2660b8f853ad2854c49a0d8
eaef1a8f7acf        dev-peer0.org1.example.com-mycc_1-27ef99cb3cbd1b545063f018f3670eddc0d54f40b2660b8f853ad2854c49a0d8-00d8dbefd85a4aeb9428b7df95df9744be1325b2a60900ac7a81796e67e4280a   "chaincode -peer.add…"   2 minutes ago        Up 2 minutes                                             dev-peer0.org1.example.com-mycc_1-27ef99cb3cbd1b545063f018f3670eddc0d54f40b2660b8f853ad2854c49a0d8
da403175b785        hyperledger/fabric-tools:latest                                                                                                                                       "/bin/bash"              4 minutes ago        Up 4 minutes                                             cli
c62a8d03818f        hyperledger/fabric-peer:latest                                                                                                                                        "peer node start"        4 minutes ago        Up 4 minutes        7051/tcp, 0.0.0.0:9051->9051/tcp     peer0.org2.example.com
06593c4f3e53        hyperledger/fabric-peer:latest                                                                                                                                        "peer node start"        4 minutes ago        Up 4 minutes        0.0.0.0:7051->7051/tcp               peer0.org1.example.com
4ddc928ebffe        hyperledger/fabric-orderer:latest                                                                                                                                     "orderer"                4 minutes ago        Up 4 minutes        0.0.0.0:7050->7050/tcp               orderer.example.com
6d79e95ec059        hyperledger/fabric-peer:latest                                                                                                                                        "peer node start"        4 minutes ago        Up 4 minutes        7051/tcp, 0.0.0.0:10051->10051/tcp   peer1.org2.example.com
6aad6b40fd30        hyperledger/fabric-peer:latest                                                                                                                                        "peer node start"        4 minutes ago        Up 4 minutes        7051/tcp, 0.0.0.0:8051->8051/tcp     peer1.org1.example.com

链码容器是以dev peer开头的镜像。然后可以使用容器ID从每个链码容器中查找日志。

$ docker logs 7aa7d9e199f5
ABstore Init
Aval = 100, Bval = 100
ABstore Invoke
Aval = 90, Bval = 110

$ docker logs eaef1a8f7acf
ABstore Init
Aval = 100, Bval = 100
ABstore Invoke
Query Response:{"Name":"a","Amount":"100"}
ABstore Invoke
Aval = 90, Bval = 110
ABstore Invoke
Query Response:{"Name":"a","Amount":"90"}

您还可以查看节点日志以查看链码调用消息和阻止提交消息:

$ docker logs peer0.org1.example.com

了解Docker Compose拓扑

BYFN示例为我们提供了两种风格的Docker Compose文件,它们都是从docker-compose-base.yaml(位于base文件夹中)扩展而来的。我们的第一个风格docker-compose-cli.yaml为我们提供了一个CLI容器,以及一个order,四个节点。我们将此文件用于本页的全部说明。

注意:本节的其余部分将介绍为SDK设计的docker-compose文件。有关运行这些测试的详细信息,请参阅Node SDK repo。

第二种风格docker-compose-e2e.yaml被构造为使用node.js SDK。除了与SDK一起工作外,它的主要区别是fabric-ca服务器有容器。因此,我们能够将REST调用发送到组织ca以进行用户注册和注册。

如果要使用docker-compose-e2e.yaml而不首先运行byfn.sh脚本,那么我们需要做四个小的修改。我们需要指向我们组织CA的私钥。您可以在加密配置文件夹中找到这些值。例如,要定位Org1的私钥,我们将遵循以下路径:crypto-config/peerOrganizations/org1.example.com/ca/。 私钥是一个长哈希值,后跟_sk。Org2的路径是-crypto-config/peerOrganizations/org2.example.com/ca/

docker-compose-e2e.yaml中,更新ca0和ca1的FABRIC_CA_SERVER_TLS_KEYFILE变量。您还需要编辑命令中提供的路径以启动ca服务器。为每个CA容器提供相同的私钥两次。


使用CouchDB

状态数据库可以从默认的(goleveldb)切换到CouchDB。CouchDB也提供了相同的chaincode函数,但是,它还增加了对状态数据库数据内容执行丰富而复杂的查询的能力,这取决于将链码数据建模为JSON。

要使用CouchDB而不是默认数据库(goleveldb),请遵循前面概述的生成工件的相同过程,但启动网络跳过 docker-compose-couch.yaml时除外:

docker-compose -f docker-compose-cli.yaml -f docker-compose-couch.yaml -f docker-compose-etcdraft2.yaml up -d

abstore现在应该在下面使用CouchDB了。

注意:如果您选择实现fabric couchdb容器端口到主机端口的映射,请确保您了解安全问题。在开发环境中映射端口使CouchDB REST API可用,并允许通过CouchDB web接口(Fauxton)可视化数据库。为了限制外部对CouchDB容器的访问,生产环境可能不执行端口映射。

您可以使用上面概述的步骤对CouchDB状态数据库使用abstore链码,但是为了练习CouchDB查询功能,您需要使用一个将数据建模为JSON的链码。编写示例链代码marbles02是为了演示在使用CouchDB数据库时可以从链码发出的查询。您可以在fabric/examples/chaincode/go目录中找到marbles02链码。

我们将按照上面的创建和加入通道一节中概述的相同过程来创建和加入通道。将节点加入通道后,请使用以下步骤与marbles02链码进行交互:

  • 将链码打包安装在peer0.org1.example.com上:
# before packaging Golang chaincode, vendoring Go dependencies is required like the following commands.
cd /opt/gopath/src/github.com/hyperledger/fabric-samples/chaincode/marbles02/go
GO111MODULE=on go mod vendor
cd -

# packages and installs the Golang chaincode
peer lifecycle chaincode package marbles.tar.gz --path github.com/hyperledger/fabric-samples/chaincode/marbles02/go/ --lang golang --label marbles_1
peer lifecycle chaincode install marbles.tar.gz

install命令将返回一个链码package ID,您将使用它来批准链码定义。

2019-04-08 20:10:32.568 UTC [cli.lifecycle.chaincode] submitInstallProposal -> INFO 001 Installed remotely: response:<status:200 payload:"\nJmarbles_1:cfb623954827aef3f35868764991cc7571b445a45cfd3325f7002f14156d61ae\022\tmarbles_1" >
2019-04-08 20:10:32.568 UTC [cli.lifecycle.chaincode] submitInstallProposal -> INFO 002 Chaincode code package identifier: marbles_1:cfb623954827aef3f35868764991cc7571b445a45cfd3325f7002f14156d61ae
  • 将package ID另存为环境变量,以便将其传递给将来的命令:
CC_PACKAGE_ID=marbles_1:3a8c52d70c36313cfebbaf09d8616e7a6318ababa01c7cbe40603c373bcfe173
  • 批准链码定义为Org1:
# be sure to modify the $CHANNEL_NAME variable accordingly for the command

peer lifecycle chaincode approveformyorg --channelID $CHANNEL_NAME --name marbles --version 1.0 --package-id $CC_PACKAGE_ID --sequence 1 --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
  • peer0.org2.example.com上安装链码:
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
CORE_PEER_ADDRESS=peer0.org2.example.com:9051
CORE_PEER_LOCALMSPID="Org2MSP"
CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
peer lifecycle chaincode install marbles.tar.gz
  • 将链码定义批准为Org2,然后将定义提交给通道:
# be sure to modify the $CHANNEL_NAME variable accordingly for the command

peer lifecycle chaincode approveformyorg --channelID $CHANNEL_NAME --name marbles --version 1.0 --package-id $CC_PACKAGE_ID --sequence 1 --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
peer lifecycle chaincode commit -o orderer.example.com:7050 --channelID $CHANNEL_NAME --name marbles --version 1.0 --sequence 1 --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 --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
  • 我们现在可以制造一些弹珠了。第一次调用chaincode将启动chaincode容器。您可能需要等待容器启动。
# be sure to modify the $CHANNEL_NAME variable accordingly

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 $CHANNEL_NAME -n marbles --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"Args":["initMarble","marble1","blue","35","tom"]}'

容器启动后,可以发出其他命令来创建一些弹珠并移动它们:

# be sure to modify the $CHANNEL_NAME variable accordingly

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 $CHANNEL_NAME -n marbles --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"Args":["initMarble","marble2","red","50","tom"]}'
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 $CHANNEL_NAME -n marbles --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"Args":["initMarble","marble3","blue","70","tom"]}'
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 $CHANNEL_NAME -n marbles --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"Args":["transferMarble","marble2","jerry"]}'
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 $CHANNEL_NAME -n marbles --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"Args":["transferMarblesBasedOnColor","blue","jerry"]}'
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 $CHANNEL_NAME -n marbles --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"Args":["delete","marble1"]}'
  • 如果您选择在docker-compose中映射CouchDB端口,现在可以通过CouchDB web界面(Fauxton)查看状态数据库,方法是打开浏览器并导航到以下URL:
    http://localhost:5984/_utils

您应该会看到一个名为mychannel的数据库(或您唯一的频道名)以及其中的文档。

注意:对于以下命令,请确保适当更新$CHANNEL_NAME变量。

您可以从CLI运行常规查询(例如读取marble2):

peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["readMarble","marble2"]}'

输出应显示marble2的详细信息:

Query Result: {"color":"red","docType":"marble","name":"marble2","owner":"jerry","size":50}

您可以检索特定弹珠的历史记录-例如弹珠1:

peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["getHistoryForMarble","marble1"]}'

输出应显示marble1上的交易:

Query Result: [{"TxId":"1c3d3caf124c89f91a4c0f353723ac736c58155325f02890adebaa15e16e6464", "Value":{"docType":"marble","name":"marble1","color":"blue","size":35,"owner":"tom"}},{"TxId":"755d55c281889eaeebf405586f9e25d71d36eb3d35420af833a20a2f53a3eefd", "Value":{"docType":"marble","name":"marble1","color":"blue","size":35,"owner":"jerry"}},{"TxId":"819451032d813dde6247f85e56a89262555e04f14788ee33e28b232eef36d98f", "Value":}]

还可以对数据内容执行丰富的查询,例如按所有者jerry查询弹珠字段:

peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarblesByOwner","jerry"]}'

输出应显示jerry拥有的两个弹珠:

Query Result: [{"Key":"marble2", "Record":{"color":"red","docType":"marble","name":"marble2","owner":"jerry","size":50}},{"Key":"marble3", "Record":{"color":"blue","docType":"marble","name":"marble3","owner":"jerry","size":70}}]

为什么使用CouchDB?

CouchDB是一种NoSQL解决方案。它是一个面向文档的数据库,其中文档字段存储为键值映射。字段可以是简单的键值对、列表或映射。除了LevelDB支持的键控/复合键/键范围查询外,CouchDB还支持完整的数据丰富的查询功能,例如针对整个区块链数据的非键查询,因为其数据内容以JSON格式存储,并且完全可查询。因此,CouchDB可以满足LevelDB不支持的许多用例的链码、审计和报告需求。

CouchDB还可以增强区块链中的合规性和数据保护的安全性。因为它能够通过过滤和屏蔽交易中的单个属性来实现字段级安全,并且只在需要时授权只读权限。


关于数据持久性的一点注记

如果在节点容器或CouchDB容器上需要数据持久性,一个选项是将docker主机中的一个目录挂载到容器中的相关目录中。例如,您可以在docker-compose-base.yaml文件的节点容器规范中添加以下两行:

volumes:
 - /var/hyperledger/peer0:/var/hyperledger/production

对于CouchDB容器,可以在CouchDB容器规范中添加以下两行:

volumes:
 - /var/hyperledger/couchdb0:/opt/couchdb/data

故障排除

  • 始终以全新的方式启动网络。使用以下命令删除项目、加密、容器和链码镜像:
./byfn.sh down

注意:如果不删除旧的容器和镜像,您将看到错误。

  • 如果看到Docker错误,请首先检查Docker版本(必备组件),然后尝试重新启动Docker进程。Docker的问题常常无法立即识别。例如,您可能会看到由于无法访问容器中安装的加密材料而导致的错误。

如果它们持续存在,请删除镜像并从头开始:

docker rm -f $(docker ps -aq)
docker rmi -f $(docker images -q)
  • 如果在create、approve、commit、invoke或query命令中看到错误,请确保正确更新了通道名称和链码名称。提供的示例命令中有占位符值。

  • 如果您看到以下错误:

Error: Error endorsing chaincode: rpc error: code = 2 desc = Error installing chaincode code mycc:1.0(chaincode /var/hyperledger/production/chaincodes/mycc.1.0 exits)

您可能有先前运行的链码镜像(例如dev-peer1.org2.example.com-mycc-1.0dev-peer0.org1.example.com-mycc-1.0)。删除它们,然后再试一次。

docker rmi -f $(docker images | grep peer[0-9]-peer[0-9] | awk '{print $3}')
  • 如果您看到类似以下内容:
Error connecting: rpc error: code = 14 desc = grpc: RPC failed fast due to transport failure
Error: rpc error: code = 14 desc = grpc: RPC failed fast due to transport failure

确保您是针对重新标记为“最新”的“1.0.0”镜像运行网络。

  • 如果您看到以下错误:
[configtx/tool/localconfig] Load -> CRIT 002 Error reading configuration: Unsupported Config Type ""
panic: Error reading configuration: Unsupported Config Type ""

那么您没有正确设置FABRIC_CFG_PATH环境变量。configtxgen工具需要这个变量来定位configtx.yaml。返回并执行export FABRIC_CFG_PATH=$PWD,然后重新创建通道工件。

  • 要清理网络,请使用down选项:
./byfn.sh down
  • 如果你看到一个错误,说明你仍然有“活动端点”,那么修剪你的Docker网络。这将擦除您以前的网络,并从一个全新的环境开始:
docker network prune

您将看到以下消息:

WARNING! This will remove all networks not used by at least one container.
Are you sure you want to continue? [y/N]

选择y

  • 如果您看到类似于以下内容的错误:
/bin/bash: ./scripts/script.sh: /bin/bash^M: bad interpreter: No such file or directory

确保有问题的文件(script.sh在本例中)是以Unix格式编码的。这很可能是由于在Git配置中没有将core.autocrlf设置为false(请参阅Windows extras)。有几种方法可以解决这个问题。例如,如果您可以访问vim编辑器,请打开文件:

vim ./fabric-samples/first-network/scripts/script.sh

然后通过执行以下vim命令更改其格式:

:set ff=unix

如果您仍然看到错误,请在Hyperledger Rocket ChatStackOverflow上的fabric questions频道上共享您的日志。

参考自官方文档
如有侵权,请联系作者删除,谢谢!
If there is infringement, please contact the author to delete, thank you!

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值