返回

Hyperledger Fabric文档v2.2(五)

教程

教程

应用程序开发人员可以使用Fabric教程开始构建自己的解决方案。通过在本地计算机上部署测试网络,开始使用将智能合约部署到渠道教程中提供的步骤来部署和测试智能合约。编写你的第一个应用 介绍了如何使用Fabric SDK提供的API从客户端应用程序调用智能合约。要深入了解Fabric应用程序和智能合约如何协同工作,您可以访问 开发应用 话题

网络管理者可以使用将智能合约部署到通道教程和创建通道 教程系列,学习管理运行网络的重要方面。网络管理者和应用程序开发人员都可以使用关于私有数据CouchDB的教程来探索重要的Fabric特性。当您准备好在生产环境中部署Hyperledger fabric时,请参阅部署一个生产网络.

有两个更新通道的教程: 更新通道配置Updating the capability level of a channelUpgrading your components 展示了如何升级组件,如 peer节点、排序节点、SDK等。

使用Fabric的测试网络⭐

下载Hyperledger Fabric Docker镜像和示例后,您将可以使用以fabric-samples代码库中提供的脚本来部署测试网络。 您可以通过在本地计算机上运行节点来使用测试网络以了解Fabric。更有经验的开发人员可以使用 网络测试其智能合约和应用程序。该网络工具仅用作教育与测试目的。它不应该用作部署产品网络的模板。 该测试网络在Fabric v2.0中被引入作为first-network示例的长期替代。该示例网络使用Docker Compose部署了一个Fabric网络。 因为这些节点是隔离在Docker Compose网络中的,所以测试网络不配置为连接到其他正在运行的fabric节点。

注意: 这些指导已基于最新的稳定版Docker镜像和提供的tar文件中的预编译的安装软件进行验证。 如果您使用当前的master分支的镜像或工具运行这些命令,则可能会遇到错误。

开始之前

在运行测试网络之前,您需要克隆fabric-samples代码库并下载Fabric镜像。确保已安装 的 准备阶段安装示例、二进制和 Docker 镜像.

启动测试网络

您可以在fabric-samples代码库的test-network目录中找到启动网络的脚本。 使用以下命令导航至测试网络目录:

1
cd fabric-samples/test-network

在此目录中,您可以找到带注释的脚本network.sh,该脚本在本地计算机上使用Docker镜像建立Fabric网络。 你可以运行./network.sh -h打印脚本帮助文本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Usage:
  network.sh <Mode> [Flags]
    Modes:
      up - bring up fabric orderer and peer nodes. No channel is created
      up createChannel - bring up fabric network with one channel
      createChannel - create and join a channel after the network is created
      deployCC - deploy the asset transfer basic chaincode on the channel or specify
      down - clear the network with docker-compose down
      restart - restart the network

    Flags:
    -ca <use CAs> -  create Certificate Authorities to generate the crypto material
    -c <channel name> - channel name to use (defaults to "mychannel")
    -s <dbtype> - the database backend to use: goleveldb (default) or couchdb
    -r <max retry> - CLI times out after certain number of attempts (defaults to 5)
    -d <delay> - delay duration in seconds (defaults to 3)
    -ccn <name> - the short name of the chaincode to deploy: basic (default),ledger, private, secured
    -ccl <language> - the programming language of the chaincode to deploy: go (default), java, javascript, typescript
    -ccv <version>  - chaincode version. 1.0 (default)
    -ccs <sequence>  - chaincode definition sequence. Must be an integer, 1 (default), 2, 3, etc
    -ccp <path>  - Optional, chaincode path. Path to the chaincode. When provided the -ccn will be used as the deployed name and not the short name of the known chaincodes.
    -cci <fcn name>  - Optional, chaincode init required function to invoke. When provided this function will be invoked after deployment of the chaincode and will define the chaincode as initialization required.
    -i <imagetag> - the tag to be used to launch the network (defaults to "latest")
    -cai <ca_imagetag> - the image tag to be used for CA (defaults to "latest")
    -verbose - verbose mode
    -h - print this message

 Possible Mode and flag combinations
   up -ca -c -r -d -s -i -verbose
   up createChannel -ca -c -r -d -s -i -verbose
   createChannel -c -r -d -verbose
   deployCC -ccn -ccl -ccv -ccs -ccp -cci -r -d -verbose

 Taking all defaults:
   network.sh up

 Examples:
   network.sh up createChannel -ca -c mychannel -s couchdb -i 2.0.0
   network.sh createChannel -c channelName
   network.sh deployCC -ccn basic -ccl javascript

test-network目录中,运行以下命令删除先前运行的所有容器或工程

1
./network.sh down

然后,您可以通过执行以下命令来启动网络。如果您尝试从另一个目录运行脚本,则会遇到问题:

1
./network.sh up

此命令创建一个由两个peer节点和一个orderer节点组成的Fabric网络。 运行./network.sh up时没有创建任何channel, 虽然我们将在后面的步骤实现。 如果命令执行成功,您将看到已创建的节点的日志:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Creating network "net_test" with the default driver
Creating volume "net_orderer.example.com" with default driver
Creating volume "net_peer0.org1.example.com" with default driver
Creating volume "net_peer0.org2.example.com" with default driver
Creating orderer.example.com    ... done
Creating peer0.org2.example.com ... done
Creating peer0.org1.example.com ... done
CONTAINER ID        IMAGE                               COMMAND             CREATED             STATUS                  PORTS                              NAMES
8d0c74b9d6af        hyperledger/fabric-orderer:latest   "orderer"           4 seconds ago       Up Less than a second   0.0.0.0:7050->7050/tcp             orderer.example.com
ea1cf82b5b99        hyperledger/fabric-peer:latest      "peer node start"   4 seconds ago       Up Less than a second   0.0.0.0:7051->7051/tcp             peer0.org1.example.com
cd8d9b23cb56        hyperledger/fabric-peer:latest      "peer node start"   4 seconds ago       Up 1 second             7051/tcp, 0.0.0.0:9051->9051/tcp   peer0.org2.example.com

如果未得到此结果,请跳至故障排除 寻求可能出现问题的帮助。 默认情况下,网络使用 cryptogen工具来建立网络。 但是,您也可以 通过证书颁发机构建立网络

测试网络的组成部分

部署测试网络后,您可能需要一些时间来检查其网络组件。 运行以下命令以列出所有正在您的计算机上运行的Docker容器。 您应该看到由network.sh脚本创建的三个节点:

1
docker ps -a

与Fabric网络互动的每个节点用户都必须属于一个网络成员的组织。 Fabric网络成员的所有组织通常称为联盟(consortium)。 测试网络有两个联盟成员,Org1和Org2。 该网络还包括一个维护网络排序服务的排序组织。

Peer 节点 是任何Fabric网络的基本组件。 peer节点存储区块链账本并在进行交易之前对其进行验证。 peer运行智能合约用于管理区块链账本的上的业务逻辑。

网络中的每个peer都必须属于该联盟的成员。 在测试网络里,每个组织各自运营一个peer节点, peer0.org1.example.compeer0.org2.example.com.

每个Fabric网络还包括一个排序服务。 虽然peer节点验证交易并将交易块添加到区块链账本,但是他们不决定交易顺序或产生区块。 在分布式网络上,peer可能运行得很远彼此之间没有什么共同点,并且对何时创建事务没有共同的看法。 在交易顺序上达成共识是一个代价高昂的过程,为peer增加开销。

排序服务允许peer节点专注于验证交易并将它们提交到账本。 排序节点从客户那里收到认可的交易后,他们就交易顺序达成共识,然后添加区块。 这些区块之后被分配给添加这些区块到账本的peer节点。 排序节点还可以操作定义Fabric网络的功能的系统通道,例如如何制作块以及节点可以使用的Fabric版本。 系统通道定义了哪个组织是该联盟的成员。

该示例网络使用一个单节点Raft排序服务,该服务由排序组织运行。 您可以看到在您机器上正在运行的排序节点orderer.example.com。 虽然测试网络仅使用单节点排序服务,但是一个真实的网络将有多个排序节点,由一个或多个多个排序组织操作。 不同的排序节点将使用Raft共识算法达成跨交易顺序的共识网络。

创建一个通道

现在我们的机器上正在运行peer节点和排序节点, 我们可以使用脚本创建用于在Org1和Org2之间进行交易的Fabric通道。 通道是特定网络成员之间的专用通信层。通道只能由被邀请加入通道的组织使用,并且对网络的其他成员不可见。 每个通道都有一个单独的区块链账本。被邀请的组织“加入”他们的peer节点来存储其通道账本并验证交易。

您可以使用network.sh脚本在Org1和Org2之间创建通道并加入他们的peer节点。 运行以下命令以创建一个默认名称为“ mychannel”的通道:

1
./network.sh createChannel

如果命令成功执行,您将看到以下消息打印在您的日志:

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

您也可以使用channel标志创建具有自定义名称的通道。 作为一个例子,以下命令将创建一个名为channel1的通道:

1
./network.sh createChannel -c channel1

通道标志还允许您创建多个不同名称的多个通道。 创建mychannelchannel1之后,您可以使用下面的命令创建另一个名为channel2的通道:

1
./network.sh createChannel -c channel2

如果您想一步建立网络并创建频道,则可以使用upcreateChannel模式一起:

1
./network.sh up createChannel

在通道启动一个链码

创建通道后,您可以开始使用智能合约与通道账本交互。 智能合约包含管理区块链账本上资产的业务逻辑。 由成员运行的应用程序网络可以在账本上调用智能合约创建,更改和转让这些资产。 应用程序还通过智能合约查询,以在分类帐上读取数据。

为确保交易有效,使用智能合约创建的交易通常需要由多个组织签名才能提交到通道账本。 多个签名是Fabric信任模型不可或缺的一部分。 一项交易需要多次背书,以防止一个通道上的单一组织使用通道不同意的业务逻辑篡改其对等节点的分类账本。 要签署交易,每个组织都需要调用并在其对等节点上执行智能合约,然后签署交易的输出。 如果输出是一致的并且已经有足够的组织签名,则可以将交易提交到账本。 该政策被称为背书政策,指定需要执行智能交易的通道上的已设置组织合同,针对每个链码设置为链码定义的一部分。

在Fabric中,智能合约作为链码以软件包的形式部署在网络上。 链码安装在组织的peer节点上,然后部署到某个通道,然后可以在该通道中用于认可交易和区块链账本交互。 在将链码部署到通道前,该频道的成员需要就链码定义达成共识,建立链码治理。 何时达到要求数量的组织同意后,链码定义可以提交给通道,并且可以使用链码了。

使用network.sh创建通道后,您可以使用以下命令在通道上启动链码:

1
./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-go -ccl go

deployCC子命令将在peer0.org1.example.compeer0.org2.example.com上安装 asset-transfer (basic) 链码。 然后在使用通道标志(或mychannel如果未指定通道)的通道上部署指定的通道的链码。 如果您第一次部署一套链码,脚本将安装链码的依赖项。默认情况下,脚本安装Go版本的 asset-transfer (basic) 链码。 但是您可以使用语言便签 -l,用于安装 Java 或 javascript 版本的链码。 您可以在 fabric-samples 目录的 asset-transfer-basic 文件夹中找到 asset-transfer (basic) 链码。 此目录包含作为案例和用来突显 Fabric 特征的样本链码。

BUG:无法下载mod

解决:更换go代理

1
2
go env -w GOPROXY=https://goproxy.cn
go env -w GO111MODULE=on

与网络交互

在您启用测试网络后,可以使用peer CLI与您的网络进行交互。 peer CLI允许您调用已部署的智能合约,更新通道,或安装和部署新的智能合约。

确保您正在从test-network目录进行操作。 如果你按照说明安装示例,二进制文件和Docker映像, 您可以在fabric-samples代码库的bin文件夹中找到peer二进制文件。 使用以下命令将这些二进制文件添加到您的CLI路径:

1
export PATH=${PWD}/../bin:$PATH

您还需要将fabric-samples代码库中的FABRIC_CFG_PATH设置为指向其中的core.yaml文件:

1
export FABRIC_CFG_PATH=$PWD/../config/

现在,您可以设置环境变量,以允许您作为Org1操作peer CLI:

1
2
3
4
5
6
7
# Environment variables for Org1

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

CORE_PEER_TLS_ROOTCERT_FILECORE_PEER_MSPCONFIGPATH环境变量指向Org1的organizations文件夹中的的加密材料。

如果您使用 ./network.sh deployCC -ccl go 安装和启动 asset-transfer (basic) 链码,您可以调用链码(Go)的 InitLedger 方法来赋予一些账本上的初始资产(如果使用 typescript 或者 javascript,例如 ./network.sh deployCC -l javascript,你会调用相关链码的 initLedger 功能)。 运行以下命令用一些资产来初始化账本

1
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 basic --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"InitLedger","Args":[]}'

如果命令成功,您将观察到类似以下的输出:

1
-> INFO 001 Chaincode invoke successful. result: status:200

现在你可以用你的 CLI 工具来查询账本。运行以下指令来获取添加到通道账本的资产列表

1
peer chaincode query -C mychannel -n basic -c '{"Args":["GetAllAssets"]}'

如果成功,您将看到以下输出:

1
2
3
4
5
6
7
8
[
  {"ID": "asset1", "color": "blue", "size": 5, "owner": "Tomoko", "appraisedValue": 300},
  {"ID": "asset2", "color": "red", "size": 5, "owner": "Brad", "appraisedValue": 400},
  {"ID": "asset3", "color": "green", "size": 10, "owner": "Jin Soo", "appraisedValue": 500},
  {"ID": "asset4", "color": "yellow", "size": 10, "owner": "Max", "appraisedValue": 600},
  {"ID": "asset5", "color": "black", "size": 15, "owner": "Adriana", "appraisedValue": 700},
  {"ID": "asset6", "color": "white", "size": 15, "owner": "Michel", "appraisedValue": 800}
]

当一个网络成员希望在账本上转一些或者改变一些资产,链码会被调用。使用以下的指令来通过调用 asset-transfer (basic) 链码改变账本上的资产所有者

1
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 basic --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"TransferAsset","Args":["asset6","Christopher"]}'

如果命令成功,您应该看到以下响应:

1
2019-12-04 17:38:21.048 EST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200

因为 asset-transfer (basic) 链码的背书策略需要交易同时被 Org1 和 Org2 签名,链码调用指令需要使用 --peerAddresses 标签来指向 peer0.org1.example.compeer0.org2.example.com。因为网络的 TLS 被开启,指令也需要用 --tlsRootCertFiles 标签指向每个 peer 节点的 TLS 证书

调用链码之后,我们可以使用另一个查询来查看调用如何改变了区块链账本的资产。因为我们已经查询了 Org1 的 peer,我们可以把这个查询链码的机会通过 Org2 的 peer 来运行。设置以下的环境变量来操作 Org2

1
2
3
4
5
6
7
# Environment variables for Org2

export CORE_PEER_TLS_ENABLED=true
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

你可以查询运行在 peer0.org2.example.com asset-transfer (basic) 链码:

1
peer chaincode query -C mychannel -n basic -c '{"Args":["ReadAsset","asset6"]}'

结果显示 "asset6" 转给了 Christopher:

1
{"ID":"asset6","color":"white","size":15,"owner":"Christopher","appraisedValue":800}

关停网络

使用完测试网络后,您可以使用以下命令关闭网络:

1
./network.sh down

该命令将停止并删除节点和链码容器,删除组织加密材料,并从Docker Registry移除链码镜像。 该命令还删除之前运行的通道项目和docker卷。如果您遇到任何问题,还允许您再次运行./ network.sh up

下一步

既然您已经使用测试网络在您的本地计算机上部署了Hyperledger Fabric,您可以使用教程来开始开发自己的解决方案:

您可以在教程页上找到Fabric教程的完整列表。

使用认证机构建立网络

Hyperledger Fabric使用公钥基础设施(PKI)来验证所有网络参与者的行为。 每个节点,网络管理员和用户提交的交易需要具有公共证书私钥以验证其身份。 这些身份必须具有有效的信任根源,该证书是由作为网络中的成员组织颁发的。 network.sh脚本在创建对等和排序节点之前创建所有部署和操作网络所有需要的加密材料

默认情况下,脚本使用cryptogen工具创建证书密钥。 该工具用于开发和测试,并且可以快速为具有有效根信任的Fabric组织创建所需的加密材料。 当您运行./network.sh up时,您会看到cryptogen工具正在创建Org1,Org2和Orderer Org的证书和密钥

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
creating Org1, Org2, and ordering service organization with crypto from 'cryptogen'

/Usr/fabric-samples/test-network/../bin/cryptogen

##########################################################
##### Generate certificates using cryptogen tool #########
##########################################################

##########################################################
############ Create Org1 Identities ######################
##########################################################
+ cryptogen generate --config=./organizations/cryptogen/crypto-config-org1.yaml --output=organizations
org1.example.com
+ res=0
+ set +x
##########################################################
############ Create Org2 Identities ######################
##########################################################
+ cryptogen generate --config=./organizations/cryptogen/crypto-config-org2.yaml --output=organizations
org2.example.com
+ res=0
+ set +x
##########################################################
############ Create Orderer Org Identities ###############
##########################################################
+ cryptogen generate --config=./organizations/cryptogen/crypto-config-orderer.yaml --output=organizations
+ res=0
+ set +x

测试网络脚本network.sh还提供了使用证书颁发机构(CA)的网络的启动选项。 在生产网络中,每个组织操作一个CA(或多个中间CA)来创建属于他们的组织身份。 所有由该组织运行的CA创建的身份享有相同的组织信任根源。 虽然花费的时间比使用cryptogen多,但是使用CA建立测试网络,提供了在生产环境中部署网络的指导。 部署CA还可以让您注册Fabric SDK的客户端身份,并为您的应用程序创建证书和私钥。

如果您想使用Fabric CA建立网络,请首先运行以下命令关停所有正在运行的网络:

1
./network.sh down

然后,您可以使用CA标志启动网络:

1
./network.sh up -ca

执行命令后,您可以看到脚本启动了三个CA,网络中的每个组织一个。

1
2
3
4
5
6
7
##########################################################
##### Generate certificates using Fabric CA's ############
##########################################################
Creating network "net_default" with the default driver
Creating ca_org2    ... done
Creating ca_org1    ... done
Creating ca_orderer ... done

值得花一些时间检查/network.sh脚本部署CA之后生成的日志。 测试网络使用Fabric CA客户端为每个组织的CA注册节点和用户身份。 之后这个脚本使用enroll命令为每个身份生成一个MSP文件夹。 MSP文件夹包含每个身份的证书和私钥,以及在CA运营的组织中建立身份的角色和成员身份。 您可以使用以下命令来检查Org1管理员用户的MSP文件夹:

1
tree organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/

该命令将显示MSP文件夹的结构和配置文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/
└── msp
    ├── IssuerPublicKey
    ├── IssuerRevocationPublicKey
    ├── cacerts
    │   └── localhost-7054-ca-org1.pem
    ├── config.yaml
    ├── keystore
    │   └── 58e81e6f1ee8930df46841bf88c22a08ae53c1332319854608539ee78ed2fd65_sk
    ├── signcerts
    │   └── cert.pem
    └── user

您可以在signcerts文件夹中找到管理员用户的证书,然后在keystore文件夹中找到私钥。 要了解有关MSP的更多信息,请参阅成员服务提供者概念主题。

cryptogen和Fabric CA都为每个组织在organizations文件夹中生成加密材料。 您可以在organizations/fabric-ca目录中的registerEnroll.sh脚本中找到用于设置网络的命令。 要了解更多有关如何使用Fabric CA部署Fabric网络的信息,请访问Fabric CA操作指南。 您可以通过访问identitymembership概念主题了解有关Fabric如何使用PKI的更多信息。

幕后发生了什么⭐⭐⭐

如果您有兴趣了解有关示例网络的更多信息,则可以调查test-network目录中的文件和脚本。 下面的步骤提供了有关在您发出./network.sh up命令时会发生什么情况的导览。

  1. ./ network.sh为两个peer组织和排序组织创建证书和密钥。 默认情况下,脚本利用cryptogen工具使用位于organizations/cryptogen文件夹中的配置文件。 如果使用-ca标志创建证书颁发机构,则脚本使用Fabric CA服务器配置文件和位于organizations/fabric-ca文件夹的registerEnroll.sh脚本。 cryptogen和Fabric CA均会在organisations文件夹创建所有三个组织中的加密资料和MSP文件夹

  1. 该脚本使用configtxgen工具创建系统通道生成块Configtxgen使用了通道配置文件configtx/configtx.yaml中的TwoOrgsOrdererGenesis轮廓(profiles)信息创建创世区块。 区块被存储在system-genesis-block文件夹中。

  1. 一旦组织的加密材料和系统通道的创始块生成后,network.sh就可以启动网络的节点。 脚本使用docker文件夹中的docker-compose-test-net.yaml文件创建peer节点和排序节点docker文件夹还包含 docker-compose-ca.yaml文件启动网络节点三个Fabric CA。 该文件旨在用于Fabric SDK 运行端到端测试。 请参阅Node SDK代码库有关运行这些测试的详细信息。

  1. 如果您使用createChannel子命令,则./network.sh使用提供的通道名称, 运行在scripts文件夹中的createChannel.sh脚本来创建通道。 该脚本使用configtx.yaml文件来中的TwoOrgsChannel轮廓创建通道,以及设置两个锚节点。 该脚本使用peer节点cli创建通道,加入peer0.org1.example.compeer0.org2.example.com 到通道, 以及使两个peer节点都成为锚节点

  • 创建通道区块:

  • 成功背书

  • 将两个peer节点加入通道

  • 设置org1的锚节点

  • 设置org2的锚节点

  1. 如果执行deployCC命令,./network.sh会运行deployCC.sh脚本在两个 peer 节点上安装链码asset-transfer (basic), 然后定义通道上的链码。 一旦将链码定义提交给通道,peer节点cli则可以使用Init初始化链码并调用链码将初始数据放入账本。

  • 安装go vendor依赖包

  • 打包链码并将链码安装到两个peer节点上

  • 查询链码是否成功安装到peer节点

  • Approve the chaincode definition for my organization.(每个组织批准链码定义)

  • Check whether a chaincode definition is ready to be committed on a channel.(根据通道成员同意的状况,来判断提交是否可能成功)

  • Commit the chaincode definition on the channel.(提交链码到通道上)
  • Query the committed chaincode definitions by channel on a peer. Optional: provide a chaincode name to query a specific definition.(确认操作)
  • 输入命令时没要求初始化

故障排除

如果您对本教程有任何疑问,请查看以下内容:

  • 您应该始终重新启动网络。 您可以使用以下命令删除先前运行的工件,加密材料,容器,卷和链码镜像:

    1
    
    ./network.sh down
    

    如果您不删除旧的容器,镜像和卷,将看到报错

  • 如果您看到Docker错误,请先检查您的Docker版本(Prerequisites), 然后尝试重新启动Docker进程。 Docker的问题是经常无法立即识别的。 例如,您可能会看到您的节点无法访问挂载在容器内的加密材料导致的错误。

    如果问题仍然存在,则可以删除镜像并从头开始

    1
    2
    
     docker rm -f $(docker ps -aq)
     docker rmi -f $(docker images -q)
    
  • 如果您在创建,批准,提交,调用或查询命令时发现错误,确保您已正确更新通道名称和链码名称。 提供的示例命令中有占位符值。

  • 如果您看到以下错误:

    1
    
    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-asset-transfer-1.0dev-peer0.org1.example.com-asset-transfer-1.0)。 删除它们并再次尝试。

    1
    
    docker rmi -f $(docker images | grep dev-peer[0-9] | awk '{print $3}')
    
  • 如果您看到以下错误:

    1
    2
    
    [configtx/tool/localconfig] Load -> CRIT 002 Error reading configuration: Unsupported Config Type ""
    panic: Error reading configuration: Unsupported Config Type ""
    

    那么您没有正确设置环境变量FABRIC_CFG_PATHconfigtxgen工具需要此变量才能找到configtx.yaml。 返回执行export FABRIC_CFG_PATH=$PWD/configtx/configtx.yaml,然后重新创建您的通道工件。

  • 如果看到错误消息指出您仍然具有“active endpoints”,请清理您的Docker网络。 这将清除您以前的网络,并以全新环境开始:

    1
    
    docker network prune
    

    您将看到一下信息:

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

    y

  • 如果您看到类似下面的错误:

    1
    
    /bin/bash: ./scripts/createChannel.sh: /bin/bash^M: bad interpreter: No such file or directory
    

    确保有问题的文件(在此示例中为createChannel.sh)为以Unix格式编码。 这很可能是由于未在Git配置中将core.autocrlf设置为false(查看Windows Extras)。 有几种解决方法。 如果你有例如vim编辑器,打开文件:

    1
    
    vim ./fabric-samples/test-network/scripts/createChannel.sh
    

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

    1
    
    :set ff=unix
    
  • 如果您的orderer在创建时退出,或者您看到由于无法连接到排序服务创建通道命令失败, 请使用docker logs命令从排序节点读取日志。 你可能会看到以下消息:

    1
    
    PANI 007 [channel system-channel] config requires unsupported orderer capabilities: Orderer capability V2_0 is required but not supported: Orderer capability V2_0 is required but not supported
    

    当您尝试使用Fabric 1.4.x版本docker镜像运行网络时,会发生这种情况。 测试网络需要使用Fabric 2.x版本运行

如果您仍然发现错误,请在fabric-questions上共享您的日志 Hyperledger Rocket chatStackOverflow

将智能合约部署到通道

最终用户通过调用智能合约与区块链分类账进行交互。在 Hyperledger Fabric 中,智能合约部署在称为链代码的包中。想要验证交易或查询分类帐的组织需要在其peer上安装链代码。在加入通道的节点上安装链码后,通道成员可以将链码部署到通道中,并使用链码中的智能合约在通道账本上创建或更新资产

使用称为 Fabric 链代码生命周期的过程将链代码部署到通道。Fabric 链代码生命周期允许多个组织在链代码用于创建交易之前就如何操作达成一致。例如,虽然背书策略指定哪些组织需要执行链代码来验证交易,但通道成员需要使用 Fabric 链代码生命周期来就链代码背书策略达成一致。有关如何在通道上部署和管理链代码的更深入概述,请参阅Fabric 链代码生命周期

您可以使用本教程了解如何使用peer lifecycle chaincode 命令将链代码部署到 Fabric 测试网络的通道。了解命令后,您可以使用本教程中的步骤将您自己的链代码部署到测试网络,或将链代码部署到生产网络。在本教程中,您将部署编写您的第一个应用程序教程使用的 Fabcar 链代码。

**注意:**这些说明使用 v2.0 版本中引入的 Fabric 链代码生命周期。如果您想使用以前的生命周期来安装和实例化链码,请访问 fabric文档的 v1.4 版本

启动网络

我们将从部署 Fabric 测试网络的实例开始。在开始之前,请确保您已经安装了先决条件并安装了示例、二进制文件和 Docker 映像。使用以下命令导航到fabric-samples存储库本地克隆中的测试网络目录:

1
cd fabric-samples/test-network

为了本教程,我们希望从已知的初始状态开始操作。以下命令将杀死任何活动的或陈旧的 docker 容器并删除以前生成的工件。

1
./network.sh down

然后您可以使用以下命令启动测试网络:

1
./network.sh up createChannel

createChannel命令创建一个以mychannel两个通道成员 Org1 和 Org2 命名的通道。该命令还将属于每个组织的peer节点加入通道。如果网络和通道创建成功,您可以在日志中看到以下消息:

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

我们现在可以使用 Peer CLI 通过以下步骤将 Fabcar 链代码部署到通道:

  • 第一步:打包智能合约
  • 第二步:安装链码包
  • 第三步:批准链码定义
  • 第四步:将链代码定义提交到通道

设置Logspout(可选)

此步骤不是必需的,但对于排查链代码非常有用。要监控智能合约的日志,管理员可以使用该logspout 工具查看一组 Docker 容器的聚合输出。该工具将来自不同 Docker 容器的输出流收集到一个地方,从而可以轻松地从单个窗口查看正在发生的情况。这可以帮助管理员在安装智能合约时或开发人员在调用智能合约时调试问题。因为一些容器纯粹是为了启动智能合约而创建的,并且只存在很短的时间,所以从您的网络中收集所有日志是有帮助的。

安装和配置 Logspout 的脚本monitordocker.sh已包含在commercial-paperFabric 样本的样本中。我们也将在本教程中使用相同的脚本。Logspout 工具会持续将日志流式传输到您的终端,因此您需要使用一个新的终端窗口。打开一个新终端并导航到test-network目录。

1
cd fabric-samples/test-network

您可以monitordocker.sh从任何目录运行脚本。为了便于使用,我们会将示例中的monitordocker.sh脚本复制到您的工作目录commercial-paper

1
2
3
cp ../commercial-paper/organization/digibank/configuration/cli/monitordocker.sh .
# if you're not sure where it is
find . -name monitordocker.sh

然后,您可以通过运行以下命令来启动 Logspout:

1
./monitordocker.sh fabric_test

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

1
2
3
4
5
6
7
8
9
Starting monitoring on all containers on the network net_basic
Unable to find image 'gliderlabs/logspout:latest' locally
latest: Pulling from gliderlabs/logspout
4fe2ade4980c: Pull complete
decca452f519: Pull complete
ad60f6b6c009: Pull complete
Digest: sha256:374e06b17b004bddc5445525796b5f7adb8234d64c5c5d663095fccafb6e4c26
Status: Downloaded newer image for gliderlabs/logspout:latest
1f99d130f15cf01706eda3e1f040496ec885036d485cb6bcc0da4a567ad84361

一开始您不会看到任何日志,但是当我们部署我们的链代码时,这会发生变化。使此终端窗口变宽字体变小可能会有所帮助。

打包智能合约

我们需要先打包链代码,然后才能将它安装到我们的对等节点上。如果您想安装用GoJavaJavaScript编写的智能合约,则步骤会有所不同。

Go

在我们打包链码之前,我们需要安装链码依赖项。导航到包含 Fabcar 链代码的 Go 版本的文件夹。

1
cd fabric-samples/chaincode/fabcar/go

该示例使用 Go 模块来安装链代码依赖项。依赖项列在目录中的go.mod文件中fabcar/go。您应该花点时间检查一下这个文件。

1
2
3
4
5
6
$ cat go.mod
module github.com/hyperledger/fabric-samples/chaincode/fabcar/go

go 1.13

require github.com/hyperledger/fabric-contract-api-go v1.1.0

go.mod文件将 Fabric 合约 API 导入到智能合约包中。你可以在文本编辑器中打开,看看智能合约开头fabcar.go是如何使用合约API来定义类型的:SmartContract

1
2
3
4
// SmartContract provides functions for managing a car
type SmartContract struct {
    contractapi.Contract
}

然后该SmartContract类型用于为智能合约中定义的函数创建事务上下文,这些函数将数据读取和写入区块链分类账。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// CreateCar adds a new car to the world state with given details
func (s *SmartContract) CreateCar(ctx contractapi.TransactionContextInterface, carNumber string, make string, model string, colour string, owner string) error {
    car := Car{
        Make:   make,
        Model:  model,
        Colour: colour,
        Owner:  owner,
    }

    carAsBytes, _ := json.Marshal(car)

    return ctx.GetStub().PutState(carNumber, carAsBytes)
}

您可以通过访问API 文档智能合约处理主题了解有关 Go 合约 API 的更多信息。

fabcar/go安装智能合约依赖项,请从目录运行以下命令。

1
GO111MODULE=on go mod vendor

如果命令成功,go 包将安装在一个vendor文件夹中。

现在我们有了我们的依赖,我们可以创建链代码包。导航回test-network文件夹中的工作目录,以便我们可以将链代码与其他网络工件打包在一起。

1
cd ../../../test-network

您可以使用peerCLI 以所需格式创建链代码包。peer二进制文件位于存储库的bin文件夹中。fabric-samples使用以下命令将这些二进制文件添加到您的 CLI 路径:

1
export PATH=${PWD}/../bin:$PATH

您还需要设置FABRIC_CFG_PATH指向存储库中的core.yaml文件fabric-samples

1
export FABRIC_CFG_PATH=$PWD/../config/

要确认您能够使用peerCLI,请检查二进制文件的版本。二进制文件需要是版本2.0.0或更高版本才能运行本教程。

1
peer version

您现在可以使用peer lifecycle chaincode package命令创建链代码包

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

此命令将在您的当前目录中创建一个名为fabcar.tar.gz的包。该--lang标志用于指定链代码语言,该--path标志提供您的智能合约代码的位置。该路径必须是绝对路径或相对于您当前工作目录的路径。该--label标志用于指定链代码标签,该标签将在安装后标识您的链代码。建议您的标签包含链码名称和版本

现在我们已经创建了链代码包,我们可以在测试网络的节点上安装链代码

JavaScript

暂时不用

Java

暂时不用

安装链码包

在我们打包 Fabcar 智能合约后,我们可以在我们的节点上安装链代码。链代码需要安装在每个将认可交易的节点上。因为我们要将背书策略设置为需要来自 Org1 和 Org2 的背书,所以我们需要在两个组织运营的peer节点上安装链代码:

  • peer0.org1.example.com
  • peer0.org2.example.com

让我们先在 Org1 节点上安装链码。设置以下环境变量以 Org1 管理员用户操作peer 。将CORE_PEER_ADDRESS被设置为指向 Org1 的peer,peer0.org1.example.com

1
2
3
4
5
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

发出peer lifecycle chaincode install命令以在peer节点上安装链代码:

1
peer lifecycle chaincode install fabcar.tar.gz

如果命令成功,peer将生成并返回包标识符。此包 ID 将用于在下一步中批准链代码。您应该看到类似于以下内容的输出:

1
2
2020-02-12 11:40:02.923 EST [cli.lifecycle.chaincode] submitInstallProposal -> INFO 001 Installed remotely: response:<status:200 payload:"\nIfabcar_1:69de748301770f6ef64b42aa6bb6cb291df20aa39542c3ef94008615704007f3\022\010fabcar_1" >
2020-02-12 11:40:02.925 EST [cli.lifecycle.chaincode] submitInstallProposal -> INFO 002 Chaincode code package identifier: fabcar_1:69de748301770f6ef64b42aa6bb6cb291df20aa39542c3ef94008615704007f3

我们现在可以在 Org2 节点上安装链码。设置以下环境变量以作为 Org2 管理员和在目标peer运行,peer0.org2.example.com.

1
2
3
4
5
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_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
peer lifecycle chaincode install fabcar.tar.gz

链码是在安装链码时由peer构建的。如果智能合约代码有问题,安装命令将从链代码返回构建错误。

批准链码定义

安装链代码包后,您需要为您的组织批准链代码定义。该定义包括链代码治理的重要参数,例如名称、版本和链代码背书策略

在部署之前需要批准链代码的一组通道成员由该Application/Channel/lifeycleEndorsement策略管理。默认情况下,此策略要求大多数通道成员需要批准链码才能在通道上使用。因为我们在通道上只有两个组织,并且 2 的多数是 2,所以我们需要批准 Fabcar 的链代码定义为 Org1 和 Org2。

如果一个组织已经在他们的peer上安装了链代码,他们需要在他们的组织批准的链代码定义中包含 packageID。包 ID 用于将安装在节点上的链代码批准的链代码定义相关联,并允许组织使用链代码来背书交易。您可以使用peer lifecycle chaincode queryinstalled命令查询您的peer节点,从而找到链代码的包 ID

1
peer lifecycle chaincode queryinstalled

包 ID 是链代码标签链代码二进制文件的哈希值的组合。

Package ID = label : hash

每个peer都将生成相同的包 ID。您应该看到类似于以下内容的输出:

1
2
Installed chaincodes on peer:
Package ID: fabcar_1:69de748301770f6ef64b42aa6bb6cb291df20aa39542c3ef94008615704007f3, Label: fabcar_1

我们将在批准链代码时使用包 ID,因此让我们继续并将其保存为环境变量。将返回的包 ID 粘贴到下面的命令中。**注意:**所有用户的包 ID 都不相同,因此您需要使用上一步从命令窗口返回的包 ID 完成此步骤。peer lifecycle chaincode queryinstalled

1
export CC_PACKAGE_ID=fabcar_1:69de748301770f6ef64b42aa6bb6cb291df20aa39542c3ef94008615704007f3

由于环境变量已设置为以 Org2 管理员身份运行 CLI,因此我们可以为 Org2 批准 Fabcar 的链代码定义。Chaincode 在组织级别获得批准的命令只需要一个peer节点。该批准被分发给该组织内其他peers通过gossip。使用peer lifecycle chaincode approveformyorg命令批准链代码定义:

1
peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name fabcar --version 1.0 --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

上面的命令使用--package-id标志将包标识符包含在链码定义中。该--sequence参数是一个整数,用于跟踪定义或更新链代码的次数。因为链码是第一次部署到通道,所以序列号是 1。当 Fabcar 链码升级时,序列号将增加到 2。如果您使用的是 Fabric Chaincode Shim 提供的低级API,您可以将--init-required标志传递给上面的命令以请求执行 Init 函数来初始化链代码。链码的第一次调用需要以 Init 函数为目标并包含--isInit标志,然后您才能使用链码中的其他函数与账本进行交互。

我们--signature-policy可以--channel-config-policyapproveformyorg命令指定链代码背书策略。背书策略指定有多少属于不同通道成员的节点需要根据给定的链代码验证交易。因为我们没有设置策略,所以Fabcar的定义会使用默认的背书策略,即交易提交时需要得到在场的过半数通道成员的背书。这意味着如果在通道中添加或删除新组织,背书策略会自动更新以要求更多或更少的背书。在本教程中,默认策略将需要 2 中的多数,因此交易需要由来自 Org1 和 Org2 的peer节点背书。如果要指定自定义背书策略,可以使用背书策略操作指南以了解策略语法。

您需要使用具有管理员角色的身份来批准链代码定义。因此,该CORE_PEER_MSPCONFIGPATH变量需要指向包含管理员身份的 MSP 文件夹。您不能批准客户端用户的链代码定义。批准需要提交给订购服务,它将验证管理员签名,然后将批准分发给peer。

我们仍然需要为 Org1批准链码定义。设置以下环境变量以作为 Org1 管理员运行:

1
2
3
4
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_ADDRESS=localhost:7051

您现在可以为 Org1批准链代码定义。

1
peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name fabcar --version 1.0 --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

我们现在拥有将 Fabcar 链代码部署到通道所需的大部分内容。虽然只有大多数组织需要批准链代码定义(使用默认策略),但所有组织都需要批准链代码定义才能在其peer上启动链代码。如果您在通道成员批准链码之前提交定义,组织将无法为交易背书。因此,建议所有通道成员在提交链代码定义之前批准链代码。

将链码定义提交到通道

在足够数量的组织批准链代码定义后,一个组织可以将链代码定义提交到通道。如果大多数通道成员批准了定义,则提交交易将成功,并且链代码定义中约定的参数将在通道上实现。

您可以使用peer lifecycle chaincode checkcommitreadiness命令来检查通道成员是否已批准相同的链代码定义。用于该checkcommitreadiness命令的标志与用于为您的组织批准链代码的标志相同。但是,您不需要包括--package-id标志。

1
peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name fabcar --version 1.0 --sequence 1 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --output json

该命令将生成一个 JSON 映射,如果通道成员批准了checkcommitreadiness命令中指定的参数,该映射就会显示:

1
2
3
4
5
6
{
    "Approvals": {
        "Org1MSP": true,
        "Org2MSP": true
    }
}

由于作为通道成员的两个组织都批准了相同的参数,因此链代码定义已准备好提交给通道。您可以使用peer lifecycle chaincode commit命令将链代码定义提交到通道。提交命令也需要由组织管理员提交。

1
peer lifecycle chaincode commit -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name fabcar --version 1.0 --sequence 1 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt

上面的交易使用--peerAddresses标记来自Org1的peer0.org1.example.com 和来自Org2的peer0.org2.example.comcommit交易被提交给加入通道的peer节点,以查询链码定义是否被组织的peer批准。该命令需要来自足够数量组织的peer节点为目标,以满足部署链代码的策略。因为批准分布在每个组织内,所以您可以将属于通道成员的任何peer节点作为目标。

通道成员背书的链代码定义被提交给排序服务添加到区块分发到通道。然后通道上的节点验证是否有足够数量的组织批准了链码定义。peer lifecycle chaincode commit命令将在返回响应之前等待来自peer的验证。

您可以使用peer lifecycle chaincode querycommitted命令来确认链代码定义已提交到通道。

1
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

如果链码成功提交到通道,该querycommitted命令将返回链码定义的序列和版本:

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

调用链码

在将链代码定义提交到通道后,链代码将在加入安装链代码的通道的peer节点上启动。Fabcar 链代码现在已准备好供客户端应用程序调用。使用以下命令在分布式账本上创建一组初始汽车。请注意,invoke 命令需要以足够数量的peer节点为目标,以满足链代码背书策略。

1
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:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"initLedger","Args":[]}'

如果命令成功,您应该能够收到类似于以下内容的响应:

1
2020-02-12 18:22:20.576 EST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200

我们可以使用查询函数来读取由链代码创建的汽车集合:

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

对查询的响应应该是以下汽车列表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[{"Key":"CAR0","Record":{"make":"Toyota","model":"Prius","colour":"blue","owner":"Tomoko"}},
{"Key":"CAR1","Record":{"make":"Ford","model":"Mustang","colour":"red","owner":"Brad"}},
{"Key":"CAR2","Record":{"make":"Hyundai","model":"Tucson","colour":"green","owner":"Jin Soo"}},
{"Key":"CAR3","Record":{"make":"Volkswagen","model":"Passat","colour":"yellow","owner":"Max"}},
{"Key":"CAR4","Record":{"make":"Tesla","model":"S","colour":"black","owner":"Adriana"}},
{"Key":"CAR5","Record":{"make":"Peugeot","model":"205","colour":"purple","owner":"Michel"}},
{"Key":"CAR6","Record":{"make":"Chery","model":"S22L","colour":"white","owner":"Aarav"}},
{"Key":"CAR7","Record":{"make":"Fiat","model":"Punto","colour":"violet","owner":"Pari"}},
{"Key":"CAR8","Record":{"make":"Tata","model":"Nano","colour":"indigo","owner":"Valeria"}},
{"Key":"CAR9","Record":{"make":"Holden","model":"Barina","colour":"brown","owner":"Shotaro"}}]

升级智能合约

您可以使用相同的 Fabric 链码生命周期流程来升级已部署到通道的链码。通道成员可以通过安装新的链代码包来升级链代码,然后使用新的包 ID、新的链代码版本和序列号加一来批准链代码定义。将链码定义提交到通道后,可以使用新的链码。此过程允许通道成员在升级链代码时进行协调,并确保在将新链代码部署到通道之前有足够数量的通道成员准备好使用新链代码。

通道成员还可以使用升级过程来更改链代码背书策略。通过使用新背书策略批准链代码定义并将链代码定义提交到通道,通道成员可以更改管理链代码的背书策略,而无需安装新的链代码包。

为了提供升级我们刚刚部署的 Fabcar 链码的场景,我们假设 Org1 和 Org2 想要安装以另一种语言编写的链码版本。他们将使用 Fabric 链代码生命周期来更新链代码版本,并确保两个组织都已安装新的链代码,然后它才能在通道上激活。

我们假设 Org1 和 Org2 最初安装了 Fabcar 链代码的 GO 版本,但使用 JavaScript 编写的链代码会更舒服。第一步是打包 Fabcar 链代码的 JavaScript 版本。如果您在学习本教程时使用了 JavaScript 指令来打包链代码,则可以按照打包以GoJava编写的链代码的步骤安装新的链代码二进制文件。

test-network目录发出以下命令以安装链代码依赖项。

1
2
3
cd ../chaincode/fabcar/javascript
npm install
cd ../../../test-network

然后,您可以发出以下命令从test-network目录中打包 JavaScript 链代码。如果您关闭终端,我们将需要再次设置peer CLI 所需的环境变量。

1
2
3
4
export PATH=${PWD}/../bin:$PATH
export FABRIC_CFG_PATH=$PWD/../config/
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
peer lifecycle chaincode package fabcar_2.tar.gz --path ../chaincode/fabcar/javascript/ --lang node --label fabcar_2

运行以下命令以peerOrg1 管理员身份操作 CLI:

1
2
3
4
5
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

我们现在可以使用以下命令在 Org1 节点上安装新的链代码包。

1
peer lifecycle chaincode install fabcar_2.tar.gz

新的链代码包将创建一个新的包 ID。我们可以通过查询来找到新的包 ID。

1
peer lifecycle chaincode queryinstalled

queryinstalled命令将返回已安装在您的对等方上的链码列表。

1
2
3
Installed chaincodes on peer:
Package ID: fabcar_1:69de748301770f6ef64b42aa6bb6cb291df20aa39542c3ef94008615704007f3, Label: fabcar_1
Package ID: fabcar_2:1d559f9fb3dd879601ee17047658c7e0c84eab732dca7c841102f20e42a9e7d4, Label: fabcar_2

您可以使用包标签找到新链代码的包 ID,并将其保存为新的环境变量

1
export NEW_CC_PACKAGE_ID=fabcar_2:1d559f9fb3dd879601ee17047658c7e0c84eab732dca7c841102f20e42a9e7d4

Org1 现在可以批准新的链代码定义

1
peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name fabcar --version 2.0 --package-id $NEW_CC_PACKAGE_ID --sequence 2 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem

新的链码定义使用 JavaScript 链码包的包 ID 并更新链码版本。因为 Fabric 链代码生命周期使用序列参数跟踪链代码升级,所以 Org1 还需要将序列号从 1 递增到 2。您可以使用peer lifecycle chaincode querycommitted命令来查找最后一个提交到通道的链代码的序列。

我们现在需要为 Org2 安装链码包批准链码定义,以便升级链码。运行以下命令以Org2 管理员身份操作peer CLI:

1
2
3
4
5
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_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=localhost:9051

我们现在可以使用以下命令在 Org2 的peer节点上安装新的链代码包。

1
peer lifecycle chaincode install fabcar_2.tar.gz

您现在可以为Org2批准新链代码定义

1
peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name fabcar --version 2.0 --package-id $NEW_CC_PACKAGE_ID --sequence 2 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem

使用peer lifecycle chaincode checkcommitreadiness命令检查序列为 2 的链码定义是否已准备好提交到通道

1
peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name fabcar --version 2.0 --sequence 2 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --output json

如果命令返回以下 JSON,则链代码已准备好升级:

1
2
3
4
5
6
{
    "Approvals": {
        "Org1MSP": true,
        "Org2MSP": true
    }
}

提交新的链代码定义后,链代码将在通道上升级。在此之前,之前的链代码将继续在两个组织的peer节点上运行。Org2 可以使用以下命令升级链码:

1
peer lifecycle chaincode commit -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name fabcar --version 2.0 --sequence 2 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt

成功的提交事务将立即启动新的链代码。如果链代码定义更改了背书策略,则新策略将生效。

您可以使用docker ps命令来验证新链代码是否已在您的对等节点上启动:

1
2
3
4
5
6
7
$docker ps
CONTAINER ID        IMAGE                                                                                                                                                                   COMMAND                  CREATED             STATUS              PORTS                              NAMES
197a4b70a392        dev-peer0.org1.example.com-fabcar_2-1d559f9fb3dd879601ee17047658c7e0c84eab732dca7c841102f20e42a9e7d4-d305a4e8b4f7c0bc9aedc84c4a3439daed03caedfbce6483058250915d64dd23   "docker-entrypoint.s…"   2 minutes ago       Up 2 minutes                                           dev-peer0.org1.example.com-fabcar_2-1d559f9fb3dd879601ee17047658c7e0c84eab732dca7c841102f20e42a9e7d4
b7e4dbfd4ea0        dev-peer0.org2.example.com-fabcar_2-1d559f9fb3dd879601ee17047658c7e0c84eab732dca7c841102f20e42a9e7d4-9de9cd456213232033c0cf8317cbf2d5abef5aee2529be9176fc0e980f0f7190   "docker-entrypoint.s…"   2 minutes ago       Up 2 minutes                                           dev-peer0.org2.example.com-fabcar_2-1d559f9fb3dd879601ee17047658c7e0c84eab732dca7c841102f20e42a9e7d4
8b6e9abaef8d        hyperledger/fabric-peer:latest                                                                                                                                          "peer node start"        About an hour ago   Up About an hour    0.0.0.0:7051->7051/tcp             peer0.org1.example.com
429dae4757ba        hyperledger/fabric-peer:latest                                                                                                                                          "peer node start"        About an hour ago   Up About an hour    7051/tcp, 0.0.0.0:9051->9051/tcp   peer0.org2.example.com
7de5d19400e6        hyperledger/fabric-orderer:latest                                                                                                                                       "orderer"                About an hour ago   Up About an hour    0.0.0.0:7050->7050/tcp             orderer.example.com

如果您使用了该--init-required标志,则需要先调用 Init 函数,然后才能使用升级后的链码。因为我们没有请求执行 Init,所以我们可以通过创建新汽车来测试我们新的 JavaScript 链码:

1
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:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"createCar","Args":["CAR11","Honda","Accord","Black","Tom"]}'

您可以再次查询分类账上的所有汽车以查看新车:

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

您应该从 JavaScript 链代码中看到以下结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[{"Key":"CAR0","Record":{"make":"Toyota","model":"Prius","colour":"blue","owner":"Tomoko"}},
{"Key":"CAR1","Record":{"make":"Ford","model":"Mustang","colour":"red","owner":"Brad"}},
{"Key":"CAR11","Record":{"color":"Black","docType":"car","make":"Honda","model":"Accord","owner":"Tom"}},
{"Key":"CAR2","Record":{"make":"Hyundai","model":"Tucson","colour":"green","owner":"Jin Soo"}},
{"Key":"CAR3","Record":{"make":"Volkswagen","model":"Passat","colour":"yellow","owner":"Max"}},
{"Key":"CAR4","Record":{"make":"Tesla","model":"S","colour":"black","owner":"Adriana"}},
{"Key":"CAR5","Record":{"make":"Peugeot","model":"205","colour":"purple","owner":"Michel"}},
{"Key":"CAR6","Record":{"make":"Chery","model":"S22L","colour":"white","owner":"Aarav"}},
{"Key":"CAR7","Record":{"make":"Fiat","model":"Punto","colour":"violet","owner":"Pari"}},
{"Key":"CAR8","Record":{"make":"Tata","model":"Nano","colour":"indigo","owner":"Valeria"}},
{"Key":"CAR9","Record":{"make":"Holden","model":"Barina","colour":"brown","owner":"Shotaro"}}]

清理

当您使用完链码后,您还可以使用以下命令删除 Logspout 工具。

1
2
docker stop logspout
docker rm logspout

然后,您可以通过从test-network目录中发出以下命令来关闭测试网络:

1
./network.sh down

下一步

编写智能合约并将其部署到通道后,您可以使用 Fabric SDK 提供的 API 从客户端应用程序调用智能合约。这允许用户与区块链分类账上的资产进行交互。要开始使用 Fabric SDK,请参阅编写您的第一个应用程序教程

故障排除

该组织不同意链码

问题:当我尝试向通道提交新的链码定义时,命令失败并出现以下错误:peer lifecycle chaincode commit

1
Error: failed to create signed transaction: proposal response was not successful, error code 500, msg failed to invoke backing implementation of 'CommitChaincodeDefinition': chaincode definition not agreed to by this org (Org1MSP)

解决方案:您可以尝试通过使用命令peer lifecycle chaincode checkcommitreadiness检查哪些通道成员已批准您尝试提交的链代码定义来解决此错误。如果任何组织对链代码定义的任何参数使用了不同的值,提交事务将失败。peer lifecycle chaincode checkcommitreadiness将显示哪些组织不批准您尝试提交的链码定义

1
2
3
4
5
6
{
    "approvals": {
        "Org1MSP": false,
        "Org2MSP": true
    }
}
调用失败

问题:peer lifecycle chaincode commit交易成功,但当我第一次尝试调用链代码时,它失败并出现以下错误:``

1
Error: endorsement failure during invoke. response: status:500 message:"make sure the chaincode fabcar has been successfully defined on channel mychannel and try again: chaincode definition for 'fabcar' exists, but chaincode is not installed"

解决方案:您批准链代码定义时可能没有设置正确--package-id。因此,提交到通道的链代码定义未与您安装的链代码包相关联,并且链代码未在您的peer节点上启动。如果你正在运行基于 docker 的网络,你可以使用docker ps命令来检查你的链代码是否正在运行:

1
2
3
4
5
docker ps
CONTAINER ID        IMAGE                               COMMAND             CREATED             STATUS              PORTS                              NAMES
7fe1ae0a69fa        hyperledger/fabric-orderer:latest   "orderer"           5 minutes ago       Up 4 minutes        0.0.0.0:7050->7050/tcp             orderer.example.com
2b9c684bd07e        hyperledger/fabric-peer:latest      "peer node start"   5 minutes ago       Up 4 minutes        0.0.0.0:7051->7051/tcp             peer0.org1.example.com
39a3e41b2573        hyperledger/fabric-peer:latest      "peer node start"   5 minutes ago       Up 4 minutes        7051/tcp, 0.0.0.0:9051->9051/tcp   peer0.org2.example.com

如果您没有看到列出任何链代码容器,请使用命令peer lifecycle chaincode approveformyorg 证明链码定义使用了正确的package ID。

背书政策失败

问题:当我尝试将链代码定义提交到通道时,交易失败并出现以下错误:

1
2
2020-04-07 20:08:23.306 EDT [chaincodeCmd] ClientWait -> INFO 001 txid [5f569e50ae58efa6261c4ad93180d49ac85ec29a07b58f576405b826a8213aeb] committed with status (ENDORSEMENT_POLICY_FAILURE) at localhost:7051
Error: transaction invalidated with status (ENDORSEMENT_POLICY_FAILURE)

解决方案:此错误是提交事务未收集到足够的背书以满足生命周期背书策略的结果。此问题可能是由于您的交易没有针对足够数量的同行来满足政策。这也可能是某些同行组织未在其文件中Endorsement:包含默认策略引用/Channel/Application/Endorsementconfigtx.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Readers:
        Type: Signature
        Rule: "OR('Org2MSP.admin', 'Org2MSP.peer', 'Org2MSP.client')"
Writers:
        Type: Signature
        Rule: "OR('Org2MSP.admin', 'Org2MSP.client')"
Admins:
        Type: Signature
        Rule: "OR('Org2MSP.admin')"
Endorsement:
        Type: Signature
        Rule: "OR('Org2MSP.peer')"

当您启用 Fabric 链代码生命周期V2_0时,除了将您的通道升级到该功能之外,您还需要使用新的 Fabric 2.0 通道策略。您的频道需要包含/Channel/Application/LifecycleEndorsement政策/Channel/Application/Endorsement

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Policies:
        Readers:
                Type: ImplicitMeta
                Rule: "ANY Readers"
        Writers:
                Type: ImplicitMeta
                Rule: "ANY Writers"
        Admins:
                Type: ImplicitMeta
                Rule: "MAJORITY Admins"
        LifecycleEndorsement:
                Type: ImplicitMeta
                Rule: "MAJORITY Endorsement"
        Endorsement:
                Type: ImplicitMeta
                Rule: "MAJORITY Endorsement"

如果您不在通道配置中包含新通道策略,则在为您的组织批准链代码定义时会出现以下错误:

1
Error: proposal failed with status: 500 - failed to invoke backing implementation of 'ApproveChaincodeDefinitionForMyOrg': could not set defaults for chaincode definition in channel mychannel: policy '/Channel/Application/Endorsement' must be defined for channel 'mychannel' before chaincode operations can be attempted

编写你的第一个应用

本教程将介绍Fabric应用程序如何与已部署的区块链网络交互。 该教程使用Fabric SDKs构建的示例程序——详细阐述在 应用 专题中——调用一个智能合约, 该合约使用智能合约API——详细阐述在 智能合约处理 中——查询和更新账本。 我们还将使用我们的示例程序和已部署的证书颁发机构来生成X.509证书, 应用程序需要这些证书才能与许可性的区块链交互。

关于 FabCar

FabCar例子演示了如何查询保存在账本上的Car(我们业务对象例子),以及如何更新账本(向账本添加新的Car)。 它包含以下两个组件:

  1. 示例应用程序:调用区块链网络,调用智能合约中实现的交易。
  2. 智能合约:实现了涉及与账本交互的交易。

我们将按照以下三个步骤进行:

1. 搭建开发环境。 我们的应用程序需要和网络交互,所以我们需要一个智能合约和应用程序使用的基础网络。

2. 浏览一个示例智能合约。 我们将查看示例智能合约 Fabcar 来学习他们的交易,还有应用程序是怎么使用他们来进行查询和更新账本的。

3. 使用示例应用程序和智能合约交互。 我们的应用程序将使用 FabCar 智能合约来查询和更新账本上的汽车资产。我们将进入到应用程序的代码和他们创建的交易,包括查询一辆汽车,查询一批汽车和创建一辆新车。

在完成这个教程之后,你将基本理解一个应用是如何通过编程关联智能合约来和 Fabric 网络上的多个节点的账本的进行交互的。

准备工作

除了Fabric的标准 准备阶段 之外,本教程还利用了Node.js对应的Hyperledger Fabric SDK。 有关最新的预备知识列表,请参阅Node.js SDK README

  • 如果您使用的是Linux,您需要安装 Python v2.7make,和C/C++编译器工具链,如 GCC。可以执行如下命令安装其他工具:
1
sudo apt install build-essential

设置区块链网络

如果你已经学习了 使用Fabric的测试网络 而且已经运行起来了一个网络,本教程将在启动一个新网络之前关闭正在运行的网络。

启动网络

这个教程演示了 Javascript 版本的 FabCar 智能合约和应用程序,但是 fabric-samples 仓库也包含 Go、Java 和 TypeScript 版本的样例。想尝试 Go、Java 或者 TypeScript 版本,改变下边的 ./startFabric.shjavascript 参数为 gojava 或者 typescript,然后跟着介绍写到终端中。

进入你克隆到本地的 fabric-samples 仓库的 fabcar 子目录。

1
cd fabric-samples/fabcar

使用 startFabric.sh 脚本启动网络。

1
./startFabric.sh go

此命令将部署两个peer节点和一个排序节点以部署Fabric测试网络。 我们将使用证书颁发机构(Fabric-CA)启动测试网络,而不是使用cryptogen工具。 我们将使用这些CA的其中一个来创建证书以及一些key, 这些加密资料将在之后的步骤中被我们的应用程序使用。startFabric.sh 脚本还将部署初始化mychannel 通道上的 FabCar智能合约的Go版本,然后调用智能合约来把初始数据存储在帐本上。

示例应用

FabCar的第一个组件:示例应用程序,适用于以下几种语言:

在本教程中,我们将阐释用 javascriptnode.js编写的示例。

fabric-samples/fabcar 目录,进入到 javascript 文件夹。

1
cd javascript

该目录包含使用Node.js对应的Fabric SDK 开发的示例程序。运行以下命令安装应用程序依赖项。 这大约需要1分钟完成:

1
npm install

这个指令将安装应用程序的主要依赖,这些依赖定义在 package.json 中。其中最重要的是 fabric-network 类;它使得应用程序可以使用身份、钱包和连接到通道的网关,以及提交交易和等待通知。本教程也将使用 fabric-ca-client 类来注册用户以及他们的授权证书,生成一个 fabric-network 在后边会用到的合法身份。

完成 npm install ,运行应用程序所需要的一切就准备好了。让我们来看一眼教程中使用的示例 JavaScript 应用文件:

1
ls

你会看到下边的文件:

1
2
enrollAdmin.js  node_modules       package.json  registerUser.js
invoke.js       package-lock.json  query.js      wallet

里边也有一些其他编程语言的文件,比如在 fabcar/java 目录中。当你使用过 JavaScript 示例之后,你可以看一下它们,主要的内容都是一样的。

登记管理员用户

下边会执行和证书授权服务器的通讯。在运行下边的程序时,打开一个新终端,并运行 docker logs -f ca_org1 来查看 CA 的日志流,会很有帮助。

当我们创建网络的时候,一个管理员用户( admin)被证书授权服务器(CA)创建成了 注册员 。我们第一步要使用 enroll.js 程序为 admin 生成私钥、公钥和 x.509 证书。这个程序使用一个 证书签名请求 (CSR)——先在本地生成公钥和私钥,然后把公钥发送到 CA ,CA 会发布会一个让应用程序使用的证书。这三个证书会保存在钱包中,以便于我们以管理员的身份使用 CA 。

我们登记一个 admin 用户:

1
node enrollAdmin.js

这个命令将 CA 管理员的证书保存在 wallet 目录。 您可以在 wallet/admin.id 文件中找到管理员的证书和私钥

注册和登记应用程序用户

既然我们的 admin 是用来与CA一起工作的。 我们也已经在钱包中有了管理员的凭据, 那么我们可以创建一个新的应用程序用户,它将被用于与区块链交互。 运行以下命令注册和记录一个名为 appUser 的新用户

1
node registerUser.js

与admin注册类似,该程序使用CSR注册 appUser 并将其凭证与 admin 凭证一起存储在钱包中。 现在,我们有了两个独立用户的身份—— adminappUser ——它们可以被我们的应用程序使用。

查询账本

区块链网络中的每个节点都拥有一个 账本 的副本,应用程序可以通过执行智能合约查询账本上最新的数据来实现来查询账本,并将查询结果返回给应用程序。

这里是一个查询工作如何进行的简单说明:

最常用的查询是查寻账本中询当前的值,也就是 世界状态 。世界状态是一个键值对的集合,应用程序可以根据一个键或者多个键来查询数据。而且,当键值对是以 JSON 值模式组织的时候,世界状态可以通过配置使用数据库(如 CouchDB )来支持富查询。这对于查询所有资产来匹配特定的键的值是很有用的,比如查询一个人的所有汽车。

首先,我们来运行我们的 query.js 程序来返回账本上所有汽车的侦听。这个程序使用我们的第二个身份——user1——来操作账本。

1
node query.js

输入结果应该类似下边:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Wallet path: ...fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result is:
[{"Key":"CAR0","Record":{"color":"blue","docType":"car","make":"Toyota","model":"Prius","owner":"Tomoko"}},
{"Key":"CAR1","Record":{"color":"red","docType":"car","make":"Ford","model":"Mustang","owner":"Brad"}},
{"Key":"CAR2","Record":{"color":"green","docType":"car","make":"Hyundai","model":"Tucson","owner":"Jin Soo"}},
{"Key":"CAR3","Record":{"color":"yellow","docType":"car","make":"Volkswagen","model":"Passat","owner":"Max"}},
{"Key":"CAR4","Record":{"color":"black","docType":"car","make":"Tesla","model":"S","owner":"Adriana"}},
{"Key":"CAR5","Record":{"color":"purple","docType":"car","make":"Peugeot","model":"205","owner":"Michel"}},
{"Key":"CAR6","Record":{"color":"white","docType":"car","make":"Chery","model":"S22L","owner":"Aarav"}},
{"Key":"CAR7","Record":{"color":"violet","docType":"car","make":"Fiat","model":"Punto","owner":"Pari"}},
{"Key":"CAR8","Record":{"color":"indigo","docType":"car","make":"Tata","model":"Nano","owner":"Valeria"}},
{"Key":"CAR9","Record":{"color":"brown","docType":"car","make":"Holden","model":"Barina","owner":"Shotaro"}}]

让我们仔细看看 query.js 程序如何使用 Fabric Node SDK 提供的API与我们的Fabric网络交互。 使用一个编辑器(比如, atom 或 visual studio)打开 query.js

应用程序首先从 fabric-network 模块 引入两个key类:WalletsGateway 到scope中。 这些类将用于定位钱包中的 appUser 身份, 并使用它连接到网络:

1
const { Gateway, Wallets } = require('fabric-network');

首先,程序使用Wallet类从我们的文件系统获取应用程序用户。

1
const identity = await wallet.get('appUser');

一旦程序有了身份标识,它便会使用Gateway类连接到我们的网络

1
2
const gateway = new Gateway();
await gateway.connect(ccpPath, { wallet, identity: 'appUser', discovery: { enabled: true, asLocalhost: true } });

ccpPath 描述了连接配置文件的路径, 我们的应用程序将使用该配置文件连接到我们的网络。 连接配置文件从 fabric-samples/test-network 目录中被加载进来, 并解析为JSON文件:

1
const ccpPath = path.resolve(__dirname, '..', '..', 'test-network','organizations','peerOrganizations','org1.example.com', 'connection-org1.json');

如果你想了解更多关于连接配置文件的结构,和它是怎么定义网络的,请查阅 连接配置文件主题

一个网络可以被差分成很多通道,代码中下一个很重要的一行是将应用程序连接到网络中特定的通道 mychannel

1
const network = await gateway.getNetwork('mychannel');

在这个通道中,我们可以通过 FabCar 智能合约来和账本进行交互:

1
const contract = network.getContract('fabcar');

fabcar 中有许多不同的 交易 ,我们的应用程序先使用 queryAllCars 交易来查询账本世界状态的值:

1
const result = await contract.evaluateTransaction('queryAllCars');

evaluateTransaction 方法代表了一种区块链网络中和智能合约最简单的交互。它只是根据配置文件中的定义连接一个节点,然后向节点发送请求,请求内容将在节点中执行。智能合约查询节点账本上的所有汽车,然后把结果返回给应用程序。这次交互没有导致账本的更新。

FabCar智能合约

FabCar智能合约示例有以下几种语言版本:

让我们来看看用JavaScript编写的FabCar智能合约中的交易。 打开一个新终端,并导航到 fabric-samples 仓库里 JavaScript版本的FabCar智能合约:

1
cd fabric-samples/chaincode/fabcar/javascript/lib

在文本编辑器中打开 fabcar.js 文件。

看看我们的智能合约是如何使用 Contract 类定义的

1
class FabCar extends Contract {...

在这个类结构中,你将看到定义了以下交易: initLedger, queryCar, queryAllCars, createCarchangeCarOwner 。例如:

1
2
async queryCar(ctx, carNumber) {...}
async queryAllCars(ctx) {...}

让我们更进一步看一下 queryAllCars ,看一下它是怎么和账本交互的。

1
2
3
4
5
6
async queryAllCars(ctx) {

  const startKey = '';
  const endKey = '';

  const iterator = await ctx.stub.getStateByRange(startKey, endKey);

这段代码展示了如何使用 getStateByRange 在一个key范围内从账本中检索所有的汽车。 给出的空startKey和endKey将被解释为从起始到结束的所有key。 另一个例子是,如果您使用 startKey = 'CAR0', endKey = 'CAR999' , 那么 getStateByRange 将以字典顺序检索在CAR0和CAR999之间key的汽车。 其余代码遍历查询结果,并将结果封装为JSON,以供示例应用程序使用。

下面展示了应用程序如何调用智能合约中的不同交易。每一个交易都使用一组 API 比如 getStateByRange 来和账本进行交互。了解更多 API 请阅读 detail.

_images/RunningtheSample.png

你可以看到我们的 queryAllCars 交易,还有另一个叫做 createCar 。我们稍后将在教程中使用他们来更新账本和添加新的区块。

但是在那之前,返回到应用程序中的 query.js 程序,更改 evaluateTransaction 的请求来查询 CAR4query 程序现在看起来应该是这个样子:

1
const result = await contract.evaluateTransaction('queryCar', 'CAR4');

保存程序,然后返回到 fabcar/javascript 目录。现在,再次运行 query 程序:

1
node query.js

你应该会看到如下:

1
2
3
Wallet path: ...fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result is:
{"color":"black","docType":"car","make":"Tesla","model":"S","owner":"Adriana"}

如果你回头去看一下 queryAllCars 的交易结果,你会看到 CAR4 是 Adriana 的黑色 Tesla model S,也就是这里返回的结果。

我们可以使用 queryCar 交易来查询任意汽车,使用它的键 (比如 CAR0 )得到车辆的制造商、型号、颜色和车主等相关信息。

很棒。现在你应该已经了解了智能合约中基础的查询交易,也手动修改了查询程序中的参数。

更新账本

现在我们已经完成一些账本的查询和添加了一些代码,我们已经准备好更新账本了。有很多的更新操作我们可以做,但是我们从创建一个 车开始。

从一个应用程序的角度来说,更新一个账本很简单。应用程序向区块链网络提交一个交易,当交易被验证和提交后,应用程序会收到一个交易成功的提醒。但是在底层,区块链网络中各组件中不同的 共识 程序协同工作,来保证账本的每一个更新提案都是合法的,而且有一个大家一致认可的顺序。

_images/write_first_app.diagram.2.png

上图中,我们可以看到完成这项工作的主要组件。同时,多个节点中每一个节点都拥有一份账本的副本,并可选的拥有一份智能合约的副本,网络中也有一个排序服务。排序服务保证网络中交易的一致性;它也将连接到网络中不同的应用程序的交易以定义好的顺序生成区块

我们对账本的的第一个更新是创建一辆新车。我们有一个单独的程序叫做 invoke.js ,用来更新账本。和查询一样,使用一个编辑器打开程序定位到我们构建和提交交易到网络的代码段:

1
await contract.submitTransaction('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom');

看一下应用程序如何调用智能合约的交易 createCar 来创建一量车主为 Tom 的黑色 Honda Accord 汽车。我们使用 CAR12 作为这里的键,这也说明了我们不必使用连续的键。

保存并运行程序:

1
node invoke.js

如果执行成功,你将看到类似输出:

1
2
Wallet path: ...fabric-samples/fabcar/javascript/wallet
Transaction has been submitted

注意 inovke 程序是怎样使用 submitTransaction API 和区块链网络交互的,而不是 evaluateTransaction

1
await contract.submitTransaction('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom');

submitTransactionevaluateTransaction 更加复杂。除了跟一个单独的 peer 进行互动外,SDK 会将 submitTransaction 提案发送给在区块链网络中的每个需要的组织的 peer。其中的每个 peer 将会使用这个提案来执行被请求的智能合约,以此来产生一个建议的回复,它会为这个回复签名并将其返回给 SDK。SDK 搜集所有签过名的交易反馈到一个单独的交易中,这个交易会被发送给排序节点。排序节点从每个应用程序那里搜集并将交易排序,然后打包进一个交易的区块中。接下来它会将这些区块分发给网络中的每个 peer,在那里每笔交易会被验证并提交。最后,SDK 会被通知,这允许它能够将控制返回给应用程序。

submitTransaction 也包含一个监听者,它会检查来确保交易被验证并提交到账本中。应用程序应该使用一个提交监听者,或者使用像 submitTransaction 这样的 API 来给你做这件事情。如果不做这个,你的交易就可能没有被成功地排序、验证以及提交到账本。

应用程序中的这些工作由 submitTransaction 完成!应用程序、智能合约、节点和排序服务一起工作来保证网络中账本一致性的程序被称为共识,它的详细解释在这里 section

为了查看这个被写入账本的交易,返回到 query.js 并将参数 CAR4 更改为 CAR12

就是说,将:

1
const result = await contract.evaluateTransaction('queryCar', 'CAR4');

改为:

1
const result = await contract.evaluateTransaction('queryCar', 'CAR12');

再次保存,然后查询:

1
node query.js

应该返回这些:

1
2
3
Wallet path: ...fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result is:
{"color":"Black","docType":"car","make":"Honda","model":"Accord","owner":"Tom"}

恭喜。你创建了一辆汽车并验证了它记录在账本上!

现在我们已经完成了,我们假设 Tom 很大方,想把他的 Honda Accord 送给一个叫 Dave 的人。

为了完成这个,返回到 invoke.js 然后利用输入的参数,将智能合约的交易从 createCar 改为 changeCarOwner

1
await contract.submitTransaction('changeCarOwner', 'CAR12', 'Dave');

第一个参数 CAR12 表示将要易主的车。第二个参数 Dave 表示车的新主人。

再次保存并执行程序:

1
node invoke.js

现在我们来再次查询账本,以确定 Dave 和 CAR12 键已经关联起来了:

1
node query.js

将返回如下结果:

1
2
3
Wallet path: ...fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result is:
{"color":"Black","docType":"car","make":"Honda","model":"Accord","owner":"Dave"}

CAR12 的主人已经从 Tom 变成了 Dave。

在真实世界中的一个应用程序里,智能合约应该有一些访问控制逻辑。比如,只有某些有权限的用户能够创建新车,并且只有车辆的拥有者才能够将车辆交换给其他人。

清除数据

当你完成FabCar示例的尝试后,您就可以使用 networkDown.sh 脚本关闭测试网络。

1
./networkDown.sh

该命令将关闭我们创建的网络的CA、peer节点和排序节点。 它还将删除保存在 wallet 目录中的 adminappUser 加密资料。 请注意,帐本上的所有数据都将丢失。 如果您想再次学习本教程,您将会以初始状态的形式启动网络。

总结

现在我们完成了一些查询和更新,你应该已经比较了解如何通过智能合约和区块链网络进行交互来查询和更新账本。我们已经看过了查询和更新的基本角智能合约、API 和 SDK ,你也应该对如何在其他的商业场景和操作中使用不同应用有了一些认识。

其他资源

就像我们在介绍中说的,我们有一整套文章在 开发应用 包含了关于智能合约、程序和数据设计的更多信息,一个更深入的使用 商业票据的教程 和大量应用开发的相关资料。

商业票据教程

本教程将向您展示如何安装和使用商业票据样例应用程序和智能合约。该主题是以任务为导向的, 因此它更侧重的是流程而不是概念。如果您想更深入地了解这些概念,可以阅读开发应用主题。

在本教程中,MagnetoCorp 和 DigiBank 这两个组织使用 Hyperledger Fabric 区块链网络 PaperNet 相互交易商业票据。

一旦搭建了一个基本的网络,您就将扮演 MagnetoCorp 的员工 Isabella,她将代表公司发行商业票据。然后,您将转换角色,担任 DigiBank 的员工 Balaji,他将购买此商业票据,持有一段时间,然后向 MagnetoCorp 赎回该商业票据,以获取小额利润。

您将扮演开发人员最终用户管理员,这些角色位于不同组织中,都将执行以下步骤,这些步骤旨在帮助您了解作为两个不同组织独立工作,但要根据Hyperledger Fabric 网络中双方共同商定的规则来进行协作是什么感觉。

本教程已经在 MacOS 和 Ubuntu 上进行了测试,应该可以在其他 Linux 发行版上运行。Windows版本的教程正在开发中。

准备阶段

在开始之前,您必须安装本教程所需的一些必备工具。我们将必备工具控制在最低限度,以便您能快速开始。

必须安装以下软件:

  • Node, Node.js SDK的README文档包含准备阶段中 对应最新版本的清单。

发现安装以下软件很有帮助:

  • 源码编辑器,如 Visual Studio Code 版本 1.28,或更高版本。VS Code 将会帮助您开发和测试您的应用程序和智能合约。安装 VS Code 看这里

    许多优秀的代码编辑器都可以使用,包括 Atom, Sublime TextBrackets

可能会发现,随着您在应用程序和智能合约开发方面的经验越来越丰富,安装以下软件会很有帮助。首次运行教程时无需安装这些:

  • Node Version Manager。NVM 帮助您轻松切换不同版本的 node——如果您同时处理多个项目的话,那将非常有用。安装 NVM 看这里

下载示例

商业票据教程是fabric-samples仓库中的示例之一。 在开始本教程之前, 确保您已经按照说明安装了Fabric 准备阶段必备工具下载示例、二进制文件和Docker映像。 当您完成后,您便克隆了包含教程脚本、 智能合约和应用程序文件的 fabric-samples 仓库。

commercialpaper.download 下载 fabric-samples GitHub仓库到您的本地机器。

下载后,可随意查看fabric-samples的目录结构:

1
2
3
4
5
6
7
8
9
$ cd fabric-samples
$ ls

CODEOWNERS              SECURITY.md                 first-network
CODE_OF_CONDUCT.md      chaincode                   high-throughput
CONTRIBUTING.md         chaincode-docker-devmode    interest_rate_swaps
LICENSE                 ci                          off_chain_data
MAINTAINERS.md          commercial-paper            test-network
README.md               fabcar

注意 commercial-paper 目录,我们的示例就在这里!

现在您已经完成了教程的第一个阶段!随着您继续操作,您将为不同用户和组件打开多个命令窗口。例如:

  • 为了显示来自您的网络的peer节点、排序服务节点和CA日志输出。
  • 作为MagnetoCorp组织和DigiBank组织的管理员, 审批同意链码。
  • 代表Isabella和Balaji运行应用程序, 他们将使用智能合约彼此交易商业票据。

当您应该从特定命令窗口运行一项命令时,我们将详细说明这一点。例如:

1
(isabella)$ ls

这表示您应该在 Isabella 的窗口中执行 ls 命令。

创建网络

本教程将使用Fabric测试网络部署智能合约。 测试网络由两个peer组织和一个排序服务组织组成。 两个组织各自操作一个peer节点, 而排序服务组织操作单个Raft排序服务节点。 我们还将使用测试网络创建一个名为mychannel的单一通道, 两个peer组织都将是该通道的成员。

commercialpaper.network The Hyperledger Fabric 基础网络的组成部分包括一个节点及该节点的账本数据库,一个排序服务和一个证书授权中心(CA)。以上每个组件都在一个 Docker 容器中运行

每个组织运行自己的证书颁发机构。 两个peer节点、状态数据库、排序服务节点 和每个组织CA都在各自的Docker容器中运行。 在生产环境中,组织通常使用与其他系统共享的现有CA; 它们不是专用于Fabric网络的。

测试网络的两个组织允许我们以两个独立的组织形式, 操作不同的peer节点与一个区块链帐本交互。 在本教程中,我们将作为DigiBank操作测试网络中的Org1 以及作为MagnetoCorp操作Org2。

您可以启动测试网络, 并使用商业票据目录中提供的脚本创建通道。 进入到fabric-samples中的commercial-paper目录:

1
cd fabric-samples/commercial-paper

然后使用脚本启动测试网络:

1
./network-starter.sh

当脚本运行时,您将看到部署测试网络的日志。 当脚本完成后,您可以使用docker ps命令 查看Fabric节点在您的本地机器上的运行情况:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ docker ps

CONTAINER ID        IMAGE                               COMMAND                  CREATED              STATUS              PORTS                                        NAMES
a86f50ca1907        hyperledger/fabric-peer:latest      "peer node start"        About a minute ago   Up About a minute   7051/tcp, 0.0.0.0:9051->9051/tcp             peer0.org2.example.com
77d0fcaee61b        hyperledger/fabric-peer:latest      "peer node start"        About a minute ago   Up About a minute   0.0.0.0:7051->7051/tcp                       peer0.org1.example.com
7eb5f64bfe5f        hyperledger/fabric-couchdb          "tini -- /docker-ent…"   About a minute ago   Up About a minute   4369/tcp, 9100/tcp, 0.0.0.0:5984->5984/tcp   couchdb0
2438df719f57        hyperledger/fabric-couchdb          "tini -- /docker-ent…"   About a minute ago   Up About a minute   4369/tcp, 9100/tcp, 0.0.0.0:7984->5984/tcp   couchdb1
03373d116c5a        hyperledger/fabric-orderer:latest   "orderer"                About a minute ago   Up About a minute   0.0.0.0:7050->7050/tcp                       orderer.example.com
6b4d87f65909        hyperledger/fabric-ca:latest        "sh -c 'fabric-ca-se…"   About a minute ago   Up About a minute   7054/tcp, 0.0.0.0:8054->8054/tcp             ca_org2
7b01f5454832        hyperledger/fabric-ca:latest        "sh -c 'fabric-ca-se…"   About a minute ago   Up About a minute   7054/tcp, 0.0.0.0:9054->9054/tcp             ca_orderer
87aef6062f23        hyperledger/fabric-ca:latest        "sh -c 'fabric-ca-se…"   About a minute ago   Up About a minute   0.0.0.0:7054->7054/tcp                       ca_org1

看看您是否可以将这些容器映射到基本网络上(可能需要横向移动才能找到信息):

  • Org1的peer, peer0.org1.example.com, 在容器a86f50ca1907上运行
  • Org2的peer, peer0.org2.example.com, 在容器77d0fcaee61b上运行
  • Org1的peer对应的CouchDB数据库, couchdb0, 在容器7eb5f64bfe5f上运行
  • Org2的peer对应的CouchDB数据库, couchdb1, 在容器2438df719f57上运行
  • 排序服务节点, orderer.example.com, 在容器03373d116c5a上运行
  • Org1的CA, ca_org1, 在容器87aef6062f23上运行
  • Org2的CA, ca_org2, 在容器6b4d87f65909上运行
  • 排序服务Org的CA, ca_orderer, 在容器7b01f5454832上运行

所有这些容器构成了被称作 fabric_testdocker 网络。您可以使用 docker network 命令查看该网络:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
$ docker network inspect fabric_test

  [
      {
          "Name": "fabric_test",
          "Id": "f4c9712139311004b8f7acc14e9f90170c5dcfd8cdd06303c7b074624b44dc9f",
          "Created": "2020-04-28T22:45:38.525016Z",
          "Containers": {
              "03373d116c5abf2ca94f6f00df98bb74f89037f511d6490de4a217ed8b6fbcd0": {
                  "Name": "orderer.example.com",
                  "EndpointID": "0eed871a2aaf9a5dbcf7896aa3c0f53cc61f57b3417d36c56747033fd9f81972",
                  "MacAddress": "02:42:c0:a8:70:05",
                  "IPv4Address": "192.168.112.5/20",
                  "IPv6Address": ""
              },
              "2438df719f57a597de592cfc76db30013adfdcfa0cec5b375f6b7259f67baff8": {
                  "Name": "couchdb1",
                  "EndpointID": "52527fb450a7c80ea509cb571d18e2196a95c630d0f41913de8ed5abbd68993d",
                  "MacAddress": "02:42:c0:a8:70:06",
                  "IPv4Address": "192.168.112.6/20",
                  "IPv6Address": ""
              },
              "6b4d87f65909afd335d7acfe6d79308d6e4b27441b25a829379516e4c7335b88": {
                  "Name": "ca_org2",
                  "EndpointID": "1cc322a995880d76e1dd1f37ddf9c43f86997156124d4ecbb0eba9f833218407",
                  "MacAddress": "02:42:c0:a8:70:04",
                  "IPv4Address": "192.168.112.4/20",
                  "IPv6Address": ""
              },
              "77d0fcaee61b8fff43d33331073ab9ce36561a90370b9ef3f77c663c8434e642": {
                  "Name": "peer0.org1.example.com",
                  "EndpointID": "05d0d34569eee412e28313ba7ee06875a68408257dc47e64c0f4f5ef4a9dc491",
                  "MacAddress": "02:42:c0:a8:70:08",
                  "IPv4Address": "192.168.112.8/20",
                  "IPv6Address": ""
              },
              "7b01f5454832984fcd9650f05b4affce97319f661710705e6381dfb76cd99fdb": {
                  "Name": "ca_orderer",
                  "EndpointID": "057390288a424f49d6e9d6f788049b1e18aa28bccd56d860b2be8ceb8173ef74",
                  "MacAddress": "02:42:c0:a8:70:02",
                  "IPv4Address": "192.168.112.2/20",
                  "IPv6Address": ""
              },
              "7eb5f64bfe5f20701aae8a6660815c4e3a81c3834b71f9e59a62fb99bed1afc7": {
                  "Name": "couchdb0",
                  "EndpointID": "bfe740be15ec9dab7baf3806964e6b1f0b67032ce1b7ae26ac7844a1b422ddc4",
                  "MacAddress": "02:42:c0:a8:70:07",
                  "IPv4Address": "192.168.112.7/20",
                  "IPv6Address": ""
              },
              "87aef6062f2324889074cda80fec8fe014d844e10085827f380a91eea4ccdd74": {
                  "Name": "ca_org1",
                  "EndpointID": "a740090d33ca94dd7c6aaf14a79e1cb35109b549ee291c80195beccc901b16b7",
                  "MacAddress": "02:42:c0:a8:70:03",
                  "IPv4Address": "192.168.112.3/20",
                  "IPv6Address": ""
              },
              "a86f50ca19079f59552e8674932edd02f7f9af93ded14db3b4c404fd6b1abe9c": {
                  "Name": "peer0.org2.example.com",
                  "EndpointID": "6e56772b4783b1879a06f86901786fed1c307966b72475ce4631405ba8bca79a",
                  "MacAddress": "02:42:c0:a8:70:09",
                  "IPv4Address": "192.168.112.9/20",
                  "IPv6Address": ""
              }
          },
          "Options": {},
          "Labels": {}
      }
  ]

看看这八个容器如何在作为单个 Docker 网络一部分的同时使用不同的 IP 地址。(为了清晰起见,我们对输出进行了缩写。)

由于我们是以DigiBank和MagnetoCorp的身份来操作测试网络的, peer0.org1.example.com将属于DigiBank组织, 而peer0.org2.example.com将由MagnetoCorp操作。 现在测试网络已经启动并运行, 从现在开始我们可以将我们的网络称为PaperNet。

回顾一下: 您已经从 GitHub 下载了 Hyperledger Fabric 示例仓库,并且已经在本地机器上运行了基本的网络。现在让我们开始扮演 MagnetoCorp 的角色来交易商业票据。

以MagentoCorp身份管理网络

商业票据教程允许您通过为DigiBank和MagnetoCorp 提供两个单独的文件夹来充当两个组织。 这两个文件夹包含每个组织的智能合约客户端应用程序文件。 由于这两个组织在商业票据交易中有不同的角色, 所以每个组织的应用程序文件也不同。 在fabric-samples仓库下打开一个新窗口, 并使用以下命令切换到MagnetoCorp目录:

1
cd commercial-paper/organization/magnetocorp

我们要做的第一件事就是以 MagnetoCorp 的角色监控 PaperNet 网络中的组件。管理员可以使用 logspout 工具 。该工具可以将不同输出流采集到一个地方,从而在一个窗口中就可以轻松看到正在发生的事情。比如,对于正在安装智能合约的管理员或者正在调用智能合约的开发人员来说,这个工具确实很有帮助。

在MagnetoCorp目录下,运行下列命令以运行monitordocker.sh脚本, 并为运行在fabric_test上的与PaperNet相关联的容器 启动logspout工具:

1
2
3
4
5
6
7
8
(magnetocorp admin)$ ./configuration/cli/monitordocker.sh fabric_test
...
latest: Pulling from gliderlabs/logspout
4fe2ade4980c: Pull complete
decca452f519: Pull complete
(...)
Starting monitoring on all containers on the network fabric_test
b7f3586e5d0233de5a454df369b8eadab0613886fc9877529587345fc01a3582

注意,如果 monitordocker.sh 中的默认端口已经在使用,您可以传入一个端口号

1
(magnetocorp admin)$ ./monitordocker.sh fabric_test <port_number>

现在,这个窗口将为本教程剩余部分显示Docker容器的日志输出, 那么继续并打开另一个命令窗口。 我们要做的下一件事是 检查MagnetoCorp将用于发行商业票据的智能合约。

检查商业票据智能合约

issue, buyredeem 是 PaperNet 智能合约的三个核心功能。应用程序使用这些功能来提交交易,相应地,在账本上会发行、购买和赎回商业票据。我们接下来的任务就是检查这个智能合约。

打开一个新的终端窗口来代表 MagnetoCorp 开发人员

1
cd commercial-paper/organization/magnetocorp

然后,您可以使用您选择的编辑器(本教程中的VS Code) 在contract目录中查看智能合约:

1
(magnetocorp developer)$ code contract

在这个文件夹的 lib 目录下,您将看到 papercontract.js 文件,其中包含了商业票据智能合约!

commercialpaper.vscode1 一个示例代码编辑器在 papercontract.js 文件中展示商业票据智能合约

papercontract.js 是一个在 Node.js 环境中运行的 JavaScript 程序。注意下面的关键程序行:

  • const { Contract, Context } = require('fabric-contract-api');

    这个语句引入了两个关键的 Hyperledger Fabric 类ContractContext,它们被智能合约广泛使用。您可以在 fabric-shim JSDOCS 中了解到这些类的更多信息。

  • class CommercialPaperContract extends Contract {

    这里基于内置的 Fabric Contract 类定义了智能合约类 CommercialPaperContract 。实现了 issue, buyredeem 商业票据关键交易的方法被定义在该类中

  • async issue(ctx, issuer, paperNumber, issueDateTime, maturityDateTime...) {

    这个方法为 PaperNet 定义了商业票据 issue 交易。传入的参数用于创建新的商业票据。

    找到并检查智能合约内的 buyredeem 交易。

  • let paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime...);

    issue 交易内部,这个语句根据提供的交易输入使用 CommercialPaper在内存中创建了一个新的商业票据。检查 buyredeem 交易看如何类似地使用该类。

  • await ctx.paperList.addPaper(paper);

    这个语句使用 ctx.paperList **在账本上添加了新的商业票据,**其中 ctx.paperListPaperList 类的一个实例,当智能合约上下文 CommercialPaperContext 被初始化时,就会创建出一个 ctx.paperList。再次检查 buyredeem 方法,以了解这些方法是如何使用这一类的。

  • return paper;

    该语句返回一个二进制缓冲区,作为来自 issue 交易的响应,供智能合约的调用者处理。

欢迎检查 contract 目录下的其他文件来理解智能合约是如何工作的,请仔细阅读智能合约处理主题中 papercontract.js 是如何设计的。

将智能合约部署到通道

在应用程序调用papercontract之前, 必须将它安装到测试网络中合适的peer节点上, 然后在通道上使用 Fabric链码生命周期定义它。 Fabric链码生命周期允许多个组织在链码被部署到通道之前同意链码的参数。 因此,我们需要以MagnetoCorp和DigiBank的管理员的身份来安装和审批同意链码。

commercialpaper.installMagnetoCorp 的管理员将 papercontract 的一个副本安装在 MagnetoCorp 的节点上。

智能合约是应用开发的重点,它被包含在一个名为链码的 Hyperledger Fabric 构件中。在一个链码中可以定义一个或多个智能合约,安装链码就使得 PaperNet 中的不同组织可以使用其中的智能合约。这意味着只有管理员需要关注链码其他人都只需关注智能合约

以MagentoCorp身份安装和批准智能合约

我们将首先以MagnetoCorp管理员的身份安装并同意智能合约。 确保您正在magnetocorp文件夹里操作, 或使用以下命令浏览至该文件夹:

1
cd commercial-paper/organization/magnetocorp

MagnetoCorp管理员可以通过使用peerCLI与PaperNet交互。 然而,管理员需要在其命令窗口中设置某些环境变量, 以使用正确的peer二进制文件集, 向MagnetoCorp的peer节点的地址发送命令和使用正确的加密资料对请求进行签名。

您可以使用示例提供的脚本在命令窗口中设置环境变量。 在magnetocorp目录中执行如下命令:

1
source magnetocorp.sh

您将在窗口中看到被打印出来的环境变量的完整列表。 我们现在可以使用这个命令窗口来以 MagnetoCorp管理员的身份与PaperNet交互。

第一步是安装papercontract智能合约。 可以使用peer lifecycle chaincode package命令将智能合约打包成链码。 在MagnetoCorp管理员的命令窗口中, 执行如下命令创建链码包:

1
(magnetocorp admin)$ peer lifecycle chaincode package cp.tar.gz --lang node --path ./contract --label cp_0

MagnetoCorp管理员现在可以使用peer lifecycle chaincode install命令 在MagnetoCorp的peer节点上安装链码:

1
(magnetocorp admin)$ peer lifecycle chaincode install cp.tar.gz

在安装了链码包后, 您会在您的终端上看到类似如下的消息:

1
2
2020-01-30 18:32:33.762 EST [cli.lifecycle.chaincode] submitInstallProposal -> INFO 001 Installed remotely: response:<status:200 payload:"\nEcp_0:ffda93e26b183e231b7e9d5051e1ee7ca47fbf24f00a8376ec54120b1a2a335c\022\004cp_0" >
2020-01-30 18:32:33.762 EST [cli.lifecycle.chaincode] submitInstallProposal -> INFO 002 Chaincode code package identifier: cp_0:ffda93e26b183e231b7e9d5051e1ee7ca47fbf24f00a8376ec54120b1a2a335c

因为MagnetoCorp管理员已经设置了CORE_PEER_ADDRESS=localhost:9051 来将peer0.org2.example.com作为指令的目标, 所以INFO 001 Installed remotely...表示 papercontract已被成功安装在此peer节点上。

在安装智能合约之后,我们需要以MagnetoCorp的管理员身份 同意papercontract的链码定义。 第一步是找到我们安装在我们的peer上的链码的packageID。 我们可以使用peer lifecycle chaincode queryinstalled 命令查询packageID:

1
peer lifecycle chaincode queryinstalled

该命令将返回与安装命令相同的包标识符。 您应该会看到类似如下的输出:

1
2
Installed chaincodes on peer:
Package ID: cp_0:ffda93e26b183e231b7e9d5051e1ee7ca47fbf24f00a8376ec54120b1a2a335c, Label: cp_0

在下一步中,我们将需要package ID, 因此我们将其保存为一个环境变量。 对于所有用户,package ID可能不相同, 因此您需要使用从命令窗口返回的package ID来完成这个步骤。

1
export PACKAGE_ID=cp_0:ffda93e26b183e231b7e9d5051e1ee7ca47fbf24f00a8376ec54120b1a2a335c

管理员现在可以使用peer lifecycle chaincode approveformyorg命令 为MagnetoCorp组织同意链码定义

1
(magnetocorp admin)$ peer lifecycle chaincode approveformyorg --orderer localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name papercontract -v 0 --package-id $PACKAGE_ID --sequence 1 --tls --cafile $ORDERER_CA

为了使用链码定义,通道成员需要同意的最重要的链码参数之一 是链码背书策略。 背书策略描述了在确定交易有效之前必须背书(执行和签署)的组织集合。 通过不指定--policy标志而同意papercontract链码, MagnetoCorp管理员将同意使用通道的默认Endorsement策略, 这在mychannel测试通道的实例下 要求通道上的大多数组织来背书交易。 所有的交易,无论有效还是无效,都将被记录在 区块链账本上, 但只有有效的交易才会更新世界状态

以DigiBank身份安装和批准智能合约

基于mychannelLifecycleEndorsement策略, Fabric链码生命周期将要求通道上的大多数组织在将链码提交到通道之前同意链码的定义。 这意味着我们需要以MagnetoCorp和DigiBank的身份同意papernet链码, 以达成所需的多数即2/2的要求。 在fabric-samples文件夹下打开一个新的终端窗口, 并浏览到包含DigiBank智能合约和应用程序文件的文件夹:

1
(digibank admin)$ cd commercial-paper/organization/digibank/

使用DigiBank文件夹中的脚本设置环境变量, 这将允许您作为DigiBank管理员操作:

1
source digibank.sh

我们现在可以以DigiBank的管理员身份安装和同意papercontract。 执行如下命令打包链码:

1
(digibank admin)$ peer lifecycle chaincode package cp.tar.gz --lang node --path ./contract --label cp_0

管理员现在可以在DigiBank的peer节点上安装链码:

1
(digibank admin)$ peer lifecycle chaincode install cp.tar.gz

然后我们需要查询并保存刚刚安装的 链码的packageID:

1
(digibank admin)$ peer lifecycle chaincode queryinstalled

将package ID保存为环境变量。 使用从控制台返回的package ID完成此步骤。

1
export PACKAGE_ID=cp_0:ffda93e26b183e231b7e9d5051e1ee7ca47fbf24f00a8376ec54120b1a2a335c

Digibank管理员现在可以同意papercontract的链码定义:

1
(digibank admin)$ peer lifecycle chaincode approveformyorg --orderer localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name papercontract -v 0 --package-id $PACKAGE_ID --sequence 1 --tls --cafile $ORDERER_CA
将链码定义提交到通道

现在,DigiBank和MagnetoCorp都同意了papernet链码, 我们有了我们需要的大多数(2/2)组织的条件来提交链码定义到通道。 一旦在通道上成功定义了链码, 通道上的客户端应用程序就可以调用 papercontract链码中的CommercialPaper智能合约。 由于任何一个机构都可以提交链码到通道, 我们将继续以DigiBank管理员的身份操作:

commercialpaper.commit在DigiBank管理员将papercontract链码的定义提交到通道后,将创建一个新的Docker链码容器,以便在PaperNet的两个peer节点上运行papercontract

DigiBank管理员使用peer lifecycle chaincode commit命令 papercontract的链码定义提交到mychannel:

1
(digibank admin)$ peer lifecycle chaincode commit -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --peerAddresses localhost:7051 --tlsRootCertFiles ${PEER0_ORG1_CA} --peerAddresses localhost:9051 --tlsRootCertFiles ${PEER0_ORG2_CA} --channelID mychannel --name papercontract -v 0 --sequence 1 --tls --cafile $ORDERER_CA --waitForEvent

链码容器将在链码定义提交到通道后启动。 您可以使用docker ps命令在两个peer节点上 看到papercontract容器的启动。

1
2
3
4
5
(digibank admin)$ docker ps

CONTAINER ID        IMAGE                                                                                                                                                               COMMAND                  CREATED             STATUS              PORTS                                        NAMES
d4ba9dc9c55f        dev-peer0.org1.example.com-cp_0-ebef35e7f1f25eea1dcc6fcad5019477cd7f434c6a5dcaf4e81744e282903535-05cf67c20543ee1c24cf7dfe74abce99785374db15b3bc1de2da372700c25608   "docker-entrypoint.s…"   30 seconds ago      Up 28 seconds                                                    dev-peer0.org1.example.com-cp_0-ebef35e7f1f25eea1dcc6fcad5019477cd7f434c6a5dcaf4e81744e282903535
a944c0f8b6d6        dev-peer0.org2.example.com-cp_0-1487670371e56d107b5e980ce7f66172c89251ab21d484c7f988c02912ddeaec-1a147b6fd2a8bd2ae12db824fad8d08a811c30cc70bc5b6bc49a2cbebc2e71ee   "docker-entrypoint.s…"   31 seconds ago      Up 28 seconds                                                    dev-peer0.org2.example.com-cp_0-1487670371e56d107b5e980ce7f66172c89251ab21d484c7f988c02912ddeaec

注意,容器的命名指出了启动它的peer节点, 以及它正在运行**papercontract版本**的实际情况。

现在我们已经将papercontract链代码部署到通道, 我们可以使用MagnetoCorp应用程序来发行商业票据。 让我们花点时间检查一下应用程序的结构。

应用结构

包含在 papercontract 中的智能合约由 MagnetoCorp 的应用程序 issue.js 调用。Isabella 使用该程序向发行商业票据 00001 的账本提交一项交易。让我么来快速检验一下 issue 应用是怎么工作的。

commercialpaper.application gateway允许应用程序专注于交易的生成、提交和响应。它协调不同网络组件之间的交易提案、排序和通知处理

issue 应用程序代表 Isabella 提交交易,它通过从 Isabella 的 钱包索取其 X.509 证书来开始运行,此证书可能储存在本地文件系统中或一个硬件安全模块 HSM 里。随后,issue 应用程序就能够利用gateway在通道上提交交易。Hyperledger Fabric的软件开发包(SDK)提供了一个 gateway 抽象,因此应用程序在将网络交互托管给网关时能够专注于应用逻辑。网关和钱包使得编写 Hyperledger Fabric 应用程序变得很简单。

让我们来检验一下 Isabella 将要使用的 issue 客户端应用程序,为其打开一个终端窗口,在 fabric-samples 中找到 MagnetoCorp 的 /application 文件夹:

1
2
3
4
(isabella)$ cd commercial-paper/organization/magnetocorp/application/
(isabella)$ ls

enrollUser.js       issue.js        package.json

addToWallet.js 是 Isabella 将用来把自己的身份装进钱包的程序,而 issue.js使用这一身份通过调用 papercontract 来代表 MagnetoCorp 生成商业票据 00001

切换至包含 MagnetoCorp 的 issue.js 应用程序副本的目录,并且使用您的代码编辑器检查此目录:

1
2
(isabella)$ cd commercial-paper/organization/magnetocorp/application
(isabella)$ code issue.js

检查该目录;目录包含了 issue 应用程序和其所有依赖项。

commercialpaper.vscode2 一个展示了商业票据应用程序目录内容的代码编辑器。

注意以下在 issue.js 中的关键程序行:

  • const { Wallets, Gateway } = require('fabric-network');

    该语句把两个关键的 Hyperledger Fabric 软件开发包(SDK)类引入了 WalletGateway

  • const wallet = await Wallets.newFileSystemWallet('../identity/user/isabella/wallet');

    该语句表明了应用程序在连接到区块链网络通道上时将使用 Isabella 钱包。因为 Isabella 的 X.509 证书位于本地文件系统中,所以应用程序创建了一个 FileSystemWallet。应用程序会在 isabella 钱包中选择一个特定的身份。

  • await gateway.connect(connectionProfile, connectionOptions);

    此行代码使用 connectionProfile 识别的网关来连接到网络使用 ConnectionOptions 当中引用的身份

    看看 ../gateway/networkConnection.yamlUser1@org1.example.com 是如何分别被用于这些值的。

  • const network = await gateway.getNetwork('mychannel');

    该语句是将应用程序连接到网络通道 mychannel 上, papercontract 之前就已经在该通道上部署过了。

  • const contract = await network.getContract('papercontract');

    该语句是让应用程序可以访问由 papercontract 中的 org.papernet.commercialpaper 命名空间定义的智能合约。一旦应用程序请求了 getContract,那么它就能提交任意在其内实现的交易。

  • const issueResponse = await contract.submitTransaction('issue', 'MagnetoCorp', '00001', ...);

    该行代码是使用在智能合约中定义的 issue 交易来向网络提交一项交易MagnetoCorp00001… 都是被 issue 交易用来生成一个新的商业票据的值。

  • let paper = CommercialPaper.fromBuffer(issueResponse);

    此语句是处理 issue 交易发来的响应。该响应需要从缓冲区被反序列化成 paper ,这是一个能够被应用程序准确解释的 CommercialPaper 对象。

欢迎检查 /application 目录下的其他文档来了解 issue.js 是如何工作的,并仔细阅读应用程序 主题 中关于如何实现 issue.js 的内容。

应用程序依赖

issue.js 客户端应用程序是用 JavaScript 编写的,旨在作为 PaperNet 网络的客户端来在 node.js 环境中运行。按照惯例,会在多个网络外部的节点上建立 MagnetoCorp 的应用程序,以此来提升开发的质量和速度。考虑一下 issue.js 是如何纳入 js-yaml 来处理 YAML 网关连接配置文件的,或者 issue.js 是如何纳入 fabric-network 来访问 GatewayWallet 类的:

1
2
const yaml = require('js-yaml');
const { Wallets, Gateway } = require('fabric-network');

需要使用 npm install 命令来将这些包从 npm 下载到本地文件系统中。按照惯例,必须将包安装进一个相对于应用程序的 /node_modules 目录中,以供运行时使用。

检查 package.json 文件来看看 issue.js 是如何通过识别包来下载自己的准确版本的:

npm 版本控制功能非常强大;点击这里可以了解更多相关信息。

让我们使用 npm install 命令来安装这些包,安装过程可能需要一分钟:

1
2
3
4
5
6
(isabella)$ cd commercial-paper/organization/magnetocorp/application/
(isabella)$ npm install

(           ) extract:lodash: sill extract ansi-styles@3.2.1
(...)
added 738 packages in 46.701s

看看这个命令是如何更新目录的:

1
2
3
4
(isabella)$ ls

enrollUser.js       node_modules            package.json
issue.js            package-lock.json

检查 node_modules 目录,查看已经安装的包。能看到很多已经安装了的包,这是因为 js-yamlfabric-network 本身都被搭建在其他 npm 包中! package-lock.json 文件 能准确识别已安装的版本,如果您想用于生产环境的话,那么这一点对您来说就很重要。例如,测试、排查问题或者分发已验证的应用。

钱包

Isabella 马上就能够运行 issue.js 来发行 MagnetoCorp 商业票票据 00001 了;现在还剩最后一步!因为 issue.js 代表 Isabella,所以也就代表 MagnetoCorp, issue.js 将会使用 Isabella 钱包中反应以上事实的身份。现在我们需要执行这个一次性的活动,向 Isabella 的钱包中添 X.509 证书

运行在PaperNet上的MagnetoCorp证书颁发机构ca_org2, 有一个在部署网络时便注册的应用程序用户。 Isabella可以使用身份名和secretissue.js应用程序生成X.509加密材料。 使用CA生成客户端加密资料的过程称为enrollment。 在实际的应用场景中,网络运营者将向客户端应用程序开发人员 提供使用CA所注册的客户端身份的名和secret。 然后,开发人员将使用证书凭据注册他们的客户端应用程序并与网络交互。

enrollUser.js程序使用fabric-ca-client类生成私有、公共密钥对, 然后发起一个Certificate Signing RequestCA。 如果Isabella提交的身份名称和secret匹配CA中注册过的证书凭据, CA将发行并签名一个编码了公钥的证书, 证明Isabella属于Isabella签名。签名请求完成后, enrollUser.js私钥和签名证书存储在Isabella的钱包中。 您可以查看enrollUser.js文件, 了解更多关于Node SDK如何使用fabric-ca-client类来完成这些任务的信息。

在 Isabella 的终端窗口中运行 addToWallet.js 程序来把身份信息添加到她的钱包中:

1
2
3
4
(isabella)$ node enrollUser.js

Wallet path: /Users/nikhilgupta/fabric-samples/commercial-paper/organization/magnetocorp/identity/user/isabella/wallet
Successfully enrolled client user "isabella" and imported it into the wallet

现在我们可以把焦点转向这个程序的结果—— 将用于提交交易到PaperNet的钱包内容:

1
2
3
(isabella)$ ls ../identity/user/isabella/wallet/

isabella.id

Isabella可以在她的钱包中存储多个身份, 但在我们的示例中,她只使用一个。 wallet文件夹里有一个isabella.id文件, 该文件提供Isabella连接到网络所需的信息。 Isabella使用的其他身份都有自己对应的文件。 您可以打开这个文件, 查看JSON文件中issue.js将使用的代表Isabella的身份标识信息。 为清晰起见,输出已经进行了格式化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(isabella)$  cat ../identity/user/isabella/wallet/*

{
  "credentials": {
    "certificate": "-----BEGIN CERTIFICATE-----\nMIICKTCCAdCgAwIBAgIQWKwvLG+sqeO3LwwQK6avZDAKBggqhkjOPQQDAjBzMQsw\nCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZy\nYW5jaXNjbzEZMBcGA1UEChMQb3JnMi5leGFtcGxlLmNvbTEcMBoGA1UEAxMTY2Eu\nb3JnMi5leGFtcGxlLmNvbTAeFw0yMDAyMDQxOTA5MDBaFw0zMDAyMDExOTA5MDBa\nMGwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T\nYW4gRnJhbmNpc2NvMQ8wDQYDVQQLEwZjbGllbnQxHzAdBgNVBAMMFlVzZXIxQG9y\nZzIuZXhhbXBsZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT4TnTblx0k\ngfqX+NN7F76Me33VTq3K2NUWZRreoJzq6bAuvdDR+iFvVPKXbdORnVvRSATcXsYl\nt20yU7n/53dbo00wSzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADArBgNV\nHSMEJDAigCDOCdm4irsZFU3D6Hak4+84QRg1N43iwg8w1V6DRhgLyDAKBggqhkjO\nPQQDAgNHADBEAiBhzKix1KJcbUy9ey5ulWHRUMbqdVCNHe/mRtUdaJagIgIgYpbZ\nXf0CSiTXIWOJIsswN4Jp+ZxkJfFVmXndqKqz+VM=\n-----END CERTIFICATE-----\n",
    "privateKey": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQggs55vQg2oXi8gNi8\nNidE8Fy5zenohArDq3FGJD8cKU2hRANCAAT4TnTblx0kgfqX+NN7F76Me33VTq3K\n2NUWZRreoJzq6bAuvdDR+iFvVPKXbdORnVvRSATcXsYlt20yU7n/53db\n-----END PRIVATE KEY-----\n"
  },
  "mspId": "Org2MSP",
  "type": "X.509",
  "version": 1
}

在文件中您可以注意到以下内容:

  • "privateKey": 用来代表Isabella签名交易, 但不能被分发到她的直接控制范围之外。
  • "certificate": 它包含Isabella的公钥和证书颁发机构在 创建证书时添加的其他X.509属性。该证书被分发到网络中, 以便不同的参与者可以在不同的时间 以加密方式验证由Isabella的私钥加密过的信息。

点击此处获取更多关于证书信息。在实践中,证书文档还包含一些 Fabric 专门的元数据,例如 Isabella 的组织和角色——在钱包主题阅读更多内容。

发行应用

Isabella 现在可以用 issue.js 来提交一项交易,该交易将发行 MagnetoCorp 商业票据 00001

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(isabella)$ node issue.js

Connect to Fabric gateway.
Use network channel: mychannel.
Use org.papernet.commercialpaper smart contract.
Submit commercial paper issue transaction.
Process issue transaction response.{"class":"org.papernet.commercialpaper","key":"\"MagnetoCorp\":\"00001\"","currentState":1,"issuer":"MagnetoCorp","paperNumber":"00001","issueDateTime":"2020-05-31","maturityDateTime":"2020-11-30","faceValue":"5000000","owner":"MagnetoCorp"}
MagnetoCorp commercial paper : 00001 successfully issued for value 5000000
Transaction complete.
Disconnect from Fabric gateway.
Issue program complete.

node 命令初始化一个 node.js 环境,并运行 issue.js。从程序输出我们能看到,系统发行了一张 MagnetoCorp 商业票据 00001,面值为 500 万美元

如您所见,为实现这一点,应用程序调用了 papercontract.jsCommercialPaper 智能合约里定义的 issue 交易。MagnetoCorp 管理员已经在网络上安装并实例化了 CommercialPaper 智能合约。在世界状态里作为一个矢量状态来代表新的商业票据的是通过 Fabric 应用程序编码端口(API)来与账本交互的智能合约,其中最显著的 API 是 putState()getState()。我们即将看到该矢量状态在随后是如何被 buyredeem 交易来操作的,这两项交易同样也是定义在那个智能合约中。

潜在的 Fabric 软件开发包(SDK)一直都在处理交易的背书、排序和通知流程,使得应用程序的逻辑变得简单明了; SDK 用网关提取出网络细节信息和连接选项 ,以此来声明更先进的流程策略,如交易重试。

现在让我们将重点转换到 DigiBank(将购买商业票据),以遵循 MagnetoCorp 00001 的生命周期。

DigiBank应用

Balaji 使用 DigiBank 的 buy 应用程序来向账本提交一项交易,该账本将商业票据 00001 的所属权从 MagnetoCorp 转向 DigiBankCommercialPaper 智能合约与 MagnetoCorp 应用程序使用的相同,但是此次的交易不同,是 buy 交易而不是 issue 交易。让我们检查一下 DigiBank 的客户端应用程序是怎样工作的。

为 Balaji 打开一个终端窗口。 在 fabric-samples 中,切换到包含 buy.js 应用程序的 DigiBank 应用程序目录,并用编辑器打开该目录:

1
2
(balaji)$ cd commercial-paper/organization/digibank/application/
(balaji)$ code buy.js

如您所见,该目录同时包含了 Balaji 将使用的 buyredeem 应用程序 。

commercialpaper.vscode3 DigiBank 的商业票据目录包含 buy.jsredeem.js 客户端应用程序。

DigiBank 的 buy.js 应用程序在结构上与 MagnetoCorp的 issue.js 十分相似,但存在两个重要的差异:

  • 身份:用户是 DigiBank 的用户 Balaji 而不是 MagnetoCorp 的 Isabella
1
const wallet = await Wallets.newFileSystemWallet('../identity/user/balaji/wallet');

看看应用程序在连接到 PaperNet 网络上时是如何使用 balaji 钱包的。buy.jsbalaji 钱包里选择一个特定的身份。

  • 交易:被调用的交易是 buy 而不是 issue
1
const buyResponse = await contract.submitTransaction('buy', 'MagnetoCorp', '00001', ...);

提交一项 buy 交易,其值为 MagnetoCorp, 00001…, CommercialPaper 智能合约类使用这些值来将商业票据 00001所属权转换成 DigiBank。

欢迎检查 application 目录下的其他文档来理解应用程序的工作原理,并仔细阅读应用程序主题中关于如何实现 buy.js 的内容。

以DigiBank的身份运行

负责购买和赎回商业票据的 DigiBank 客户端应用程序的结构和 MagnetoCorp 的发行交易十分相似。所以,我们来安装这些应用程序 的依赖项,并搭建 Balaji 的钱包,这样一来,Balaji 就能使用这些应用程序购买赎回商业票据。

和 MagnetoCorp 一样, Digibank 必须使用 npm install 命令来安装规定的应用包,同时,安装时间也很短。

在 DigiBank 管理员窗口安装应用程序依赖项:

1
2
3
4
5
6
(digibank admin)$ cd commercial-paper/organization/digibank/application/
(digibank admin)$ npm install

(            ) extract:lodash: sill extract ansi-styles@3.2.1
(...)
added 738 packages in 46.701s

在 Balaji 的终端窗口运行 addToWallet.js 程序,把身份信息添加到他的钱包中:

addToWallet.js 程序为 balaji 将其身份信息添加到他的钱包中, buy.jsredeem.js 将使用这些身份信息来向 PaperNet 提交交易。

1
2
3
4
(balaji)$ node enrollUser.js

Wallet path: /Users/nikhilgupta/fabric-samples/commercial-paper/organization/digibank/identity/user/balaji/wallet
Successfully enrolled client user "balaji" and imported it into the wallet

addToWallet.js程序已经balaji的身份信息添加到他的钱包中, 该钱包将被buy.jsredeem.js用于向PaperNet提交交易。

与Isabella一样,Balaji可以在他的钱包中存储多个身份, 但在我们的示例中,他只使用一个。 他在digibank/identity/user/balaji/wallet/balaji.id对应的id文件 和Isabella的非常相似——请随意查看。

购买应用

Balaji 现在可以使用 buy.js 来提交一项交易,该交易将会把 MagnetoCorp 商业票据 00001 的所属权转换成 DigiBank

在 Balaji 的窗口运行 buy 应用程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(balaji)$ node buy.js

Connect to Fabric gateway.
Use network channel: mychannel.
Use org.papernet.commercialpaper smart contract.
Submit commercial paper buy transaction.
Process buy transaction response.
MagnetoCorp commercial paper : 00001 successfully purchased by DigiBank
Transaction complete.
Disconnect from Fabric gateway.
Buy program complete.

您可看到程序输出为:Balaji 已经代表 DigiBank 成功购买了 MagnetoCorp 商业票据 00001。 buy.js 调用了 CommercialPaper 智能合约中定义的 buy 交易,该智能合约使用 Fabric 应用程序编程接口(API) putState()getState() 在世界状态中更新了商业票据 00001 。如您所见,就智能合约的逻辑来说,购买和发行商业票据的应用程序逻辑彼此十分相似。

赎回应用

商业票据 00001 生命周期的最后一步交易是 DigiBank 从 MagnetoCorp 那里赎回商业票据。Balaji 使用 redeem.js 提交一项交易来执行智能合约中的赎回逻辑。

在Balaji的窗口运行 redeem 交易:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(balaji)$ node redeem.js

Connect to Fabric gateway.
Use network channel: mychannel.
Use org.papernet.commercialpaper smart contract.
Submit commercial paper redeem transaction.
Process redeem transaction response.
MagnetoCorp commercial paper : 00001 successfully redeemed with MagnetoCorp
Transaction complete.
Disconnect from Fabric gateway.
Redeem program complete.

同样地,看看当 redeem.js 调用了 CommercialPaper 中定义的 redeem 交易时,商业票据 00001 是如何被成功赎回的。 redeem 交易在世界状态中更新了商业票据 00001 ,以此来反映商业票据的所属权又归回其发行方 MagnetoCorp。

清理

在您完成商业票据教程后, 您可以使用一个脚本来清理您的环境。 打开一个命令窗口浏览回商业票据示例的根目录:

1
cd fabric-samples/commercial-paper

然后您可以使用以下命令关闭网络:

1
./network-clean.sh

除了logspout工具之外, 这个命令还将关闭peer节点、CouchDB容器和网络中的排序节点。 它还会移除我们为Isabella和Balaji创造的身份标识。 请注意,账本上的所有数据都将丢失。 如果你想再次学习教程,你将从一个干净的初始状态开始。

下一步

要想更深入地理解以上教程中所介绍的应用程序和智能合约的工作原理,可以参照 开发应用程序。该主题将为您详细介绍商业票据场景、PaperNet 商业网络,网络操作者以及它们所使用的应用程序和智能合约的工作原理。

欢迎使用该样本来开始创造您自己的应用程序和智能合约

在 Fabric 中使用私有数据

本教程将演示如何使用集合在区块链网络中授权的 Peer 节点上存储和检索私有数据。

本教程需要你已经掌握私有数据存储及其使用方法。更多信息,请查看 私有数据

  1. 创建集合定义的 JSON 文件
  2. 使用链码 API 读写私有数据
  3. 安装并定义一个带集合的链码
  4. 存储私有数据
  5. 授权节点查询私有数据
  6. 未授权节点查询私有数据
  7. 清除私有数据
  8. 使用私有数据索引
  9. 其他资源

本教程将会在 Fabric Building Your First Network (BYFN)教程网络中使用 弹珠私有数据示例 来演示如何创建、部署以及 使用私有数据集合。你应该先完成 fabric 的安装 安装示例、二进制和 Docker 镜像

创建集合定义的JSON文件

在通道中使用私有数据的第一步是定义集合决定私有数据的访问权限

集合的定义描述了谁可以保存数据数据要分发给多少个节点需要多少个节点来进行数据分发,以及私有数据在私有数据库中的保存时间。之后,我们将会展示链码的接口:PutPrivateDataGetPrivateData 将集合映射到私有数据以确保其安全。

集合定义由以下几个属性组成:

  • name: 集合的名称。
  • policy: 定义了哪些组织中的 Peer 节点能够存储集合数据
  • requiredPeerCount: 私有数据要分发到的节点数,这是链码背书成功的条件之一。
  • maxPeerCount: 为了数据冗余,当前背书节点将尝试向其他节点分发数据的数量。如果当前背书节点发生故障,其他的冗余节点可以承担私有数据查询的任务。
  • blockToLive: 对于非常敏感的信息,比如价格或者个人信息,这个值代表数据可以在私有数据库中保存的时间。数据会在私有数据库中保存 blockToLive 个区块,之后就会被清除。如果要永久保留,将此值设置为 0 即可。
  • memberOnlyRead: 设置为 true 时,节点会自动强制集合中定义的成员组织内的客户端对私有数据仅拥有只读权限

为了说明私有数据的用法,弹珠私有数据示例包含两个私有数据集合定义collectionMarbles和collectionMarblePrivateDetailscollectionMarbles 定义中的 policy 属性允许通道的所有成员(Org1 和 Org2)在私有数据库中保存私有数据collectionMarblesPrivateDetails 集合仅允许 Org1 的成员在其私有数据库中保存私有数据

关于 policy 属性的更多相关信息,请查看 背书策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// collections_config.json

[
  {
       "name": "collectionMarbles",
       "policy": "OR('Org1MSP.member', 'Org2MSP.member')",
       "requiredPeerCount": 0,
       "maxPeerCount": 3,
       "blockToLive":1000000,
       "memberOnlyRead": true
  },

  {
       "name": "collectionMarblePrivateDetails",
       "policy": "OR('Org1MSP.member')",
       "requiredPeerCount": 0,
       "maxPeerCount": 3,
       "blockToLive":3,
       "memberOnlyRead": true
  }
]

由这些策略保护的数据将会在链码中映射出来,在本教程后半段将有说明。

当链码被使用 peer lifecycle chaincode commit 命令 提交到通道中时,集合定义文件也会被部署到通道中。更多信息请看下面的第三节。

使用链码API读写私有数据

接下来将通过在链码中构建数据定义来让您理解数据在通道中的私有化。弹珠私有数据示例将私有数据拆分为两个数据定义来进行数据权限控制。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// marbles_chaincode_private.go
// Peers in Org1 and Org2 will have this private data in a side database
type marble struct {
  ObjectType string `json:"docType"`
  Name       string `json:"name"`
  Color      string `json:"color"`
  Size       int    `json:"size"`
  Owner      string `json:"owner"`
}

// Only peers in Org1 will have this private data in a side database
type marblePrivateDetails struct {
  ObjectType string `json:"docType"`
  Name       string `json:"name"`
  Price      int    `json:"price"`
}

对私有数据的访问将遵循以下策略:

  • name, color, size, and owner 通道中所有成员都可见(Org1 和 Org2)
  • price 仅对 Org1 中的成员可见

弹珠示例中有两个不同的私有数据定义。这些数据和限制访问权限的集合策略将由链码接口进行控制。具体来说,就是读取和写入带有集合定义的私有数据需要使用 GetPrivateData()PutPrivateData() 接口,你可以在 这里 找到他们。

下图说明了弹珠私有数据示例中使用的私有数据模型。

_images/SideDB-org1-org2.png

读取集合数据

使用链码 API GetPrivateData() 在数据库中访问私有数据GetPrivateData() 有两个参数,集合名(collection name)数据键(data key)。 重申一下,集合 collectionMarbles 允许 Org1 和 Org2 的成员在侧数据库中保存私有数据,集合 collectionMarblePrivateDetails 只允许 Org1 在侧数据库中保存私有数据。有关接口的实现详情请查看 弹珠私有数据方法

  • readMarble 用来查询 name, color, size and owner 这些属性
  • readMarblePrivateDetails 用来查询 price 属性

下面教程中,使用 peer 命令查询数据库的时候,会使用这两个方法。

写入私有数据

使用链码接口 PutPrivateData() 将私有数据保存到私有数据库中。该接口需要集合名称。由于弹珠私有数据示例中包含两个不同的私有数据集,因此这个接口在链码中会被调用两次:

  1. 使用集合 collectionMarbles 写入私有数据 name, color, size 和 owner
  2. 使用集合 collectionMarblePrivateDetails 写入私有数据price

例如,在链码的 initMarble 方法片段中,PutPrivateData() 被调用了两次,每个私有数据调用一次。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// ==== Create marble object, marshal to JSON, and save to state ====
      marble := &marble{
              ObjectType: "marble",
              Name:       marbleInput.Name,
              Color:      marbleInput.Color,
              Size:       marbleInput.Size,
              Owner:      marbleInput.Owner,
      }
      marbleJSONasBytes, err := json.Marshal(marble)
      if err != nil {
              return shim.Error(err.Error())
      }

      // === Save marble to state ===
      err = stub.PutPrivateData("collectionMarbles", marbleInput.Name, marbleJSONasBytes)
      if err != nil {
              return shim.Error(err.Error())
      }

      // ==== Create marble private details object with price, marshal to JSON, and save to state ====
      marblePrivateDetails := &marblePrivateDetails{
              ObjectType: "marblePrivateDetails",
              Name:       marbleInput.Name,
              Price:      marbleInput.Price,
      }
      marblePrivateDetailsBytes, err := json.Marshal(marblePrivateDetails)
      if err != nil {
              return shim.Error(err.Error())
      }
      err = stub.PutPrivateData("collectionMarblePrivateDetails", marbleInput.Name, marblePrivateDetailsBytes)
      if err != nil {
              return shim.Error(err.Error())
      }

总结一下,在上边的 collections_config.json 中定义的策略,允许 Org1 和 Org2 中的所有成员都能在他们的私有数据库中对私有数据 name, color, size, owner 进行存储和交易。但是只有 Org1 中的成员才能够对 price 进行存储和交易。

数据私有化的另一个好处就是,使用集合时,只有私有数据的哈希值会通过排序节点, 而数据本身不会参与排序。这样就保证了私有数据对排序节点的保密性

启动网络

现在我们准备使用一些命令来演示如何使用私有数据。

Try it yourself

在安装、定义和使用弹珠私有数据示例链码之前,我们需要启动 Fabric 测试网络。为了大家可以正确使用本教程,我们将从一个已知的初始化状态开始操作。接下来的命令将会停止你主机上所有正在运行的 Docker 容器,并会清除之前生成的构件。所以我们运行以下命令来清除之前的环境。

1
2
cd fabric-samples/test-network
./network.sh down

如果你之前没有运行过本私有数据的教程,你需要在我们部署链码前下载链码所需的依赖。运行如下命令:

1
2
3
cd ../chaincode/marbles02_private/go
GO111MODULE=on go mod vendor
cd ../../../test-network

如果你之前已经运行过本教程,你也需要删除之前弹珠私有数据链码的 Docker 容器。运行如下命令:

1
2
docker rm -f $(docker ps -a | awk '($2 ~ /dev-peer.*.marblesp.*/) {print $1}')
docker rmi -f $(docker images | awk '($1 ~ /dev-peer.*.marblesp.*/) {print $3}')

test-network 目录中,你可以使用如下命令启动使用 CouchDB 的 Fabric 测试网络

1
./network.sh up createChannel -s couchdb

这个命令将会部署一个 Fabric 网络,包括一个名为的通道 mychannel,两个组织(各拥有一个 Peer 节点),Peer 节点将使用 CouchDB 作为状态数据库。用默认的 LevelDB 和 CouchDB 都可以使用私有数据集合。我们选择 CouchDB 来演示如何使用私有数据的索引

为了保证私有数据集正常工作,需要正确地配置组织间的 gossip 通信。请参考文档 Gossip 数据传播协议,需要特别注意 “锚节点(anchor peers)” 章节。本教程不关注 gossip,它在测试网络中已经配置好了。但当我们配置通道的时候,gossip 的锚节点是否被正确配置影响到私有数据集能否正常工作

安装并定义一个带集合的链码

客户端应用程序是通过链码与区块链账本进行数据交互的。因此我们需要在每个节点上安装链码,用他们来执行和背书我们的交易。然而,在我们与链码进行交互之前,通道中的成员需要一致同意链码的定义,以此来建立链码的治理,当然还包括链私有数据集合的定义。我们将要使用命令:peer lifecycle chaincode 打包、安装,以及在通道上定义链码。

链码安装到 Peer 节点之前需要先进行打包操作。我们可以用 peer lifecycle chaincode package 命令对弹珠链码进行打包。

测试网络包含两个组织,Org1 和 Org2,各自拥有一个节点。所以要安装链码包到两个节点上:

  • peer0.org1.example.com
  • peer0.org2.example.com

链码打包之后,我们可以使用 peer lifecycle chaincode install 命令将弹珠链码安装到每个节点上。

Try it yourself

如果你已经成功启动测试网络,复制粘贴如下环境变量到你的 CLI 以 Org1 管理员的身份与测试网络进行交互。请确保你在 test-network 目录中。

1
2
3
4
5
6
7
export PATH=${PWD}/../bin:$PATH
export FABRIC_CFG_PATH=$PWD/../config/
export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
  1. 用以下命令打包弹珠私有数据链码
1
peer lifecycle chaincode package marblesp.tar.gz --path ../chaincode/marbles02_private/go/ --lang golang --label marblespv1

这个命令将会生成一个链码包文件 marblesp.tar.gz。

  1. 用以下命令在节点 peer0.org1.example.com 上安装链码包。
1
peer lifecycle chaincode install marblesp.tar.gz

安装成功会返回链码标识,类似如下响应:

1
2
2019-04-22 19:09:04.336 UTC [cli.lifecycle.chaincode] submitInstallProposal -> INFO 001 Installed remotely: response:<status:200 payload:"\nKmarblespv1:57f5353b2568b79cb5384b5a8458519a47186efc4fcadb98280f5eae6d59c1cd\022\nmarblespv1" >
2019-04-22 19:09:04.336 UTC [cli.lifecycle.chaincode] submitInstallProposal -> INFO 002 Chaincode code package identifier: marblespv1:57f5353b2568b79cb5384b5a8458519a47186efc4fcadb98280f5eae6d59c1cd
  1. 现在在 CLI 中切换到 Org2 管理员。复制粘贴如下代码到你的命令行窗口并运行:
1
2
3
4
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 的节点上安装链码:
1
peer lifecycle chaincode install marblesp.tar.gz
批准链码定义

每个通道中的成员想要使用链码,都需要为他们的组织审批链码定义。由于本教程中的两个组织都要使用链码,所以我们需要使用 peer lifecycle chaincode approveformyorg 为Org1 和 Org2 审批链码定义。链码定义也包含私有数据集合的定义,它们都在 marbles02_private 示例中。我们会使用 --collections-config 参数来指明私有数据集 JSON 文件的路径

Try it yourself

test-network 目录下运行如下命令来为 Org1 和 Org2 审批链码定义。

  1. 使用如下命令来查询节点上已安装链码包的 ID。
1
peer lifecycle chaincode queryinstalled

这个命令将返回和安装命令一样的链码包的标识,你会看到类似如下的输出信息:

1
2
Installed chaincodes on peer:
Package ID: marblespv1:f8c8e06bfc27771028c4bbc3564341887881e29b92a844c66c30bac0ff83966e, Label: marblespv1
  1. 将包 ID 声明为一个环境变量。粘贴 peer lifecycle chaincode queryinstalled 命令返回的包 ID 到下边的命令中。包 ID 在不同用户中是不一样的,所以你的 ID 可能与本教程中的不同,所以你需要使用你的终端中返回的包 ID 来完成这一步。
1
export CC_PACKAGE_ID=marblespv1:f8c8e06bfc27771028c4bbc3564341887881e29b92a844c66c30bac0ff83966e
  1. 为了确保我们在以 Org1 的身份运行 CLI。复制粘贴如下信息到节点容器中并执行:
1
2
3
4
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. 用如下命令审批 Org1 的弹珠私有数据链码定义。此命令包含了一个集合文件的路径。
1
2
export ORDERER_CA=${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name marblesp --version 1.0 --collections-config ../chaincode/marbles02_private/collections_config.json --signature-policy "OR('Org1MSP.member','Org2MSP.member')" --package-id $CC_PACKAGE_ID --sequence 1 --tls --cafile $ORDERER_CA

当命令成功完成后,你会收到类似如下的返回信息:

1
2020-01-03 17:26:55.022 EST [chaincodeCmd] ClientWait -> INFO 001 txid [06c9e86ca68422661e09c15b8e6c23004710ea280efda4bf54d501e655bafa9b] committed with status (VALID) at
  1. 将 CLI 转换到 Org2。复制粘贴如下信息到节点容器中并执行:
1
2
3
4
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 审批链码定义
1
peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name marblesp --version 1.0 --collections-config ../chaincode/marbles02_private/collections_config.json --signature-policy "OR('Org1MSP.member','Org2MSP.member')" --package-id $CC_PACKAGE_ID --sequence 1 --tls --cafile $ORDERER_CA
提交链码定义

当组织中大部分成员审批通过了链码定义,该组织才可以提交该链码定义到通道上

使用 peer lifecycle chaincode commit 命令来提交链码定义。这个命令同样也会部署私有数据集合到通道上

在链码定义被提交到通道后,我们就可以使用这个链码了。因为弹珠私有数据示例包含一个初始化方法,我们在调用链码中的其他方法前,需要使用 [peer chaincode invoke](https://hyperledger-fabric.readthedocs.io/zh_CN/latest/commands/peerchaincode.html? chaincode instantiate#peer-chaincode-instantiate) 命令去调用 Init() 方法。

Try it yourself

  1. 运行如下命令提交弹珠私有数据示例链码定义到 mychannel 通道。
1
2
3
4
export ORDERER_CA=${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
export ORG1_CA=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export ORG2_CA=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
peer lifecycle chaincode commit -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name marblesp --version 1.0 --sequence 1 --collections-config ../chaincode/marbles02_private/collections_config.json --signature-policy "OR('Org1MSP.member','Org2MSP.member')" --tls --cafile $ORDERER_CA --peerAddresses localhost:7051 --tlsRootCertFiles $ORG1_CA --peerAddresses localhost:9051 --tlsRootCertFiles $ORG2_CA

提交成功后,你会看到类似如下的输出信息:

1
2
2020-01-06 16:24:46.104 EST [chaincodeCmd] ClientWait -> INFO 001 txid [4a0d0f5da43eb64f7cbfd72ea8a8df18c328fb250cb346077d91166d86d62d46] committed with status (VALID) at localhost:9051
2020-01-06 16:24:46.184 EST [chaincodeCmd] ClientWait -> INFO 002 txid [4a0d0f5da43eb64f7cbfd72ea8a8df18c328fb250cb346077d91166d86d62d46] committed with status (VALID) at localhost:7051

存储私有数据

Org1 的成员已经被授权使用弹珠私有数据示例中的所有私有数据进行交易,切换回 Org1 节点并提交添加一个弹珠的请求:

Try it yourself

在 CLI 的 test-network 的目录中,复制粘贴如下命令:

1
2
3
4
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051

调用 initMarble 方法,将会创建一个带有私有数据的弹珠,该弹珠名为 marble1,所有者为 tom,颜色为 blue,尺寸为 35,价格为 99。重申一下,私有数据 price 将会和私有数据 name, owner, color, size 分开存储。因此, initMarble 方法会调用 PutPrivateData() 接口两次来存储私有数据。另外注意,传递私有数据时使用 --transient 参数。作为瞬态的输入不会被记录到交易中,以此来保证数据的隐私性。瞬态数据会以二进制的方式被传输,所以在 CLI 中使用时,必须使用 base64 编码。我们设置一个环境变量来获取 base64 编码后的值,并使用 tr 命令来去掉 linux base64 命令添加的换行符

1
2
export MARBLE=$(echo -n "{\"name\":\"marble1\",\"color\":\"blue\",\"size\":35,\"owner\":\"tom\",\"price\":99}" | base64 | tr -d \\n)
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c '{"Args":["InitMarble"]}' --transient "{\"marble\":\"$MARBLE\"}"

你会看到类似如下的输出结果:

1
[chaincodeCmd] chaincodeInvokeOrQuery->INFO 001 Chaincode invoke successful. result: status:200

授权节点查询私有数据

我们的集合定义定义允许 Org1 和 Org2 的所有成员在他们的侧数据库中保存 name, color, size, owner 私有数据,但是只有 Org1 的成员才可以在他们的侧数据库中保存price私有数据。作为一个已授权的 Org1 的节点,我们可以查询两个私有数据集。

第一个 query 命令调用了 readMarble 方法并将 collectionMarbles 作为参数传入。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ===============================================
// readMarble - read a marble from chaincode state
// ===============================================

func (t *SimpleChaincode) readMarble(stub shim.ChaincodeStubInterface, args []string) pb.Response {
     var name, jsonResp string
     var err error
     if len(args) != 1 {
             return shim.Error("Incorrect number of arguments. Expecting name of the marble to query")
     }

     name = args[0]
     valAsbytes, err := stub.GetPrivateData("collectionMarbles", name) //get the marble from chaincode state

     if err != nil {
             jsonResp = "{\"Error\":\"Failed to get state for " + name + "\"}"
             return shim.Error(jsonResp)
     } else if valAsbytes == nil {
             jsonResp = "{\"Error\":\"Marble does not exist: " + name + "\"}"
             return shim.Error(jsonResp)
     }

     return shim.Success(valAsbytes)
}

第二个 query 命令调用了 readMarblePrivateDetails 方法, 并将 collectionMarblePrivateDetails 作为参数传入。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ===============================================
// readMarblePrivateDetails - read a marble private details from chaincode state
// ===============================================

func (t *SimpleChaincode) readMarblePrivateDetails(stub shim.ChaincodeStubInterface, args []string) pb.Response {
     var name, jsonResp string
     var err error

     if len(args) != 1 {
             return shim.Error("Incorrect number of arguments. Expecting name of the marble to query")
     }

     name = args[0]
     valAsbytes, err := stub.GetPrivateData("collectionMarblePrivateDetails", name) //get the marble private details from chaincode state

     if err != nil {
             jsonResp = "{\"Error\":\"Failed to get private details for " + name + ": " + err.Error() + "\"}"
             return shim.Error(jsonResp)
     } else if valAsbytes == nil {
             jsonResp = "{\"Error\":\"Marble private details does not exist: " + name + "\"}"
             return shim.Error(jsonResp)
     }
     return shim.Success(valAsbytes)
}

用 Org1 的成员来查询 marble1 的私有数据 name, color, size 和 owner。注意,因为查询操作不会在账本上留下记录,因此没必要以瞬态的方式传入弹珠名称。

1
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarble","marble1"]}'

你会看到如下输出结果:

1
{"color":"blue","docType":"marble","name":"marble1","owner":"tom","size":35}

Query for the price private data of marble1 as a member of Org1.

1
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarblePrivateDetails","marble1"]}'

你会看到如下输出结果:

1
{"docType":"marblePrivateDetails","name":"marble1","price":99}

未授权节点查询私有数据

现在我们将切换到 Org2 的成员。Org2 在侧数据库中存有私有数据 name, color, size, owner,但是不存储弹珠的 price 数据。我们来同时查询两个私有数据集。

切换到Org2的节点

运行如下命令以 Org2 管理员的身份操作并查询 Org2 节点:

Try it yourself

1
2
3
4
export CORE_PEER_LOCALMSPID="Org2MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=localhost:9051
查询Org2被授权的私有数据

Org2 的节点应该拥有第一个私有数据集(name, color, size and owner)的访问权限,可以使用 readMarble() 方法,该方法使用了 collectionMarbles 参数。

1
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarble","marble1"]}'

你会看到类似如下的输出结果:

1
{"docType":"marble","name":"marble1","color":"blue","size":35,"owner":"tom"}
查询Org2未被授权的私有数据

Org2 的节点的侧数据库中不存在 price 数据。当你尝试查询这个数据时,将会返回一个公共状态中对应键的 hash 值,但并不会返回私有状态。

Try it yourself

1
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarblePrivateDetails","marble1"]}'

你会看到类似如下的输出结果:

1
2
3
4
Error: endorsement failure during query. response: status:500
message:"{\"Error\":\"Failed to get private details for marble1:
GET_STATE failed: transaction ID: d9c437d862de66755076aeebe79e7727791981606ae1cb685642c93f102b03e5:
tx creator does not have read access permission on privatedata in chaincodeName:marblesp collectionName: collectionMarblePrivateDetails\"}"

Org2 的成员,将只能看到私有数据的公共 hash。

清除私有数据

对于一些案例,私有数据仅需在账本上保存到在链下数据库复制之后就可以了,我们可以将 数据在过了一定数量的区块后进行“清除”,仅仅把数据的哈希作为交易不可篡改的证据保存下来

私有数据可能会包含私人的或者机密的信息,比如我们例子中的价格数据,这是交易伙伴不想让通道中的其他组织知道的。而且,它具有有限的生命周期,就可以根据集合定义中的 blockToLive 属性在固定的区块数量之后清除。

我们的 collectionMarblePrivateDetails 中定义的 blockToLive 值为3,表明这个数据会在侧数据库中保存三个区块的时间,之后它就会被清除。将所有内容放在一起,回想一下绑定了私有数据 price 的私有数据集 collectionMarblePrivateDetails,在函数 initMarble() 中,当调用 PutPrivateData() API 并传递了参数 collectionMarblePrivateDetails

我们将在链上增加区块,然后来通过执行四笔新交易(创建一个新弹珠,然后转移三个弹珠)看一看价格信息被清除的过程,增加新交易的过程中会在链上增加四个新区块。在第四笔交易完成之后(第三个弹珠转移后),我们将验证一下价格私有数据是否被清除了。

使用如下命令切换到 Org1 。复制和粘贴下边的一组命令到节点容器并执行:

1
2
3
4
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

打开一个新终端窗口,通过运行如下命令来查看这个节点上私有数据日志。注意当前区块高度,为 6。

1
docker logs peer0.org1.example.com 2>&1 | grep -i -a -E 'private|pvt|privdata'

回到节点容器中,使用如下命令查询 marble1price 数据(查询并不会产生一笔新的交易)。

1
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarblePrivateDetails","marble1"]}'

你将看到类似下边的结果:

1
{"docType":"marblePrivateDetails","name":"marble1","price":99}

price 数据仍然存在于私有数据库上

执行如下命令创建一个新的 marble2。这个交易将在链上创建一个新区块

1
2
export MARBLE=$(echo -n "{\"name\":\"marble2\",\"color\":\"blue\",\"size\":35,\"owner\":\"tom\",\"price\":99}" | base64 | tr -d \\n)
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c '{"Args":["InitMarble"]}' --transient "{\"marble\":\"$MARBLE\"}"

再次切换回终端窗口并查看节点的私有数据日志。你将看到区块高度增加了 1,变为 7。

1
docker logs peer0.org1.example.com 2>&1 | grep -i -a -E 'private|pvt|privdata'

返回到节点容器,运行如下命令查询 marble1 的价格数据:

1
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarblePrivateDetails","marble1"]}'

私有数据没有被清除,查询结果也没有改变:

1
{"docType":"marblePrivateDetails","name":"marble1","price":99}

运行下边的命令将 marble2 转移给 “joe” 。这个交易将使链上增加第二个区块

1
2
export MARBLE_OWNER=$(echo -n "{\"name\":\"marble2\",\"owner\":\"joe\"}" | base64 | tr -d \\n)
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c '{"Args":["TransferMarble"]}' --transient "{\"marble_owner\":\"$MARBLE_OWNER\"}"

再次切换回终端窗口并查看节点的私有数据日志。你将看到区块高度增加了 1 ,为 8。

1
docker logs peer0.org1.example.com 2>&1 | grep -i -a -E 'private|pvt|privdata'

返回到节点容器,再次运行如下命令查询 marble1 的价格数据:

1
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarblePrivateDetails","marble1"]}'

仍然可以看到价格

1
{"docType":"marblePrivateDetails","name":"marble1","price":99}

运行下边的命令将 marble2 转移给 “tom” 。这个交易将使链上增加第三个区块

1
2
export MARBLE_OWNER=$(echo -n "{\"name\":\"marble2\",\"owner\":\"tom\"}" | base64 | tr -d \\n)
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c '{"Args":["TransferMarble"]}' --transient "{\"marble_owner\":\"$MARBLE_OWNER\"}"

再次切换回终端窗口并查看节点的私有数据日志。你将看到区块高度增加了 1 ,为 9。

1
docker logs peer0.org1.example.com 2>&1 | grep -i -a -E 'private|pvt|privdata'

返回到节点容器,再次运行如下命令查询 marble1 的价格数据:

1
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarblePrivateDetails","marble1"]}'

仍然可以看到价格数据

1
{"docType":"marblePrivateDetails","name":"marble1","price":99}

最后,运行下边的命令将 marble2 转移给 “jerry” 。这个交易将使链上增加第四个区块。在此次交易之后,price 私有数据将会被清除。

1
2
export MARBLE_OWNER=$(echo -n "{\"name\":\"marble2\",\"owner\":\"jerry\"}" | base64 | tr -d \\n)
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c '{"Args":["TransferMarble"]}' --transient "{\"marble_owner\":\"$MARBLE_OWNER\"}"

再次切换回终端窗口并查看节点的私有数据日志。你将看到区块高度增加了 1 ,为 10。

1
docker logs peer0.org1.example.com 2>&1 | grep -i -a -E 'private|pvt|privdata'

返回到节点容器,再次运行如下命令查询 marble1 的价格数据:

1
peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarblePrivateDetails","marble1"]}'

因为价格数据已经被清除了,所以你就查询不到了。你应该会看到类似下边的结果:

1
2
Error: endorsement failure during query. response: status:500
message:"{\"Error\":\"Marble private details does not exist: marble1\"}"

使用私有数据索引

可以通过打包链码目录中的索引 META-INF/statedb/couchdb/collections/<collection_name>/indexes 目录,将索引也用于私有数据数据集这里 有一个可用的索引示例。

生产环境中部署链码时,建议在链码目录中定义所有索引,这样当链码在通道中的节点上安装和初始化的时候就可以自动作为一个单元自动部署。当使用 --collections-config 标识私有数据集的 JSON 文件路径时,通道上链码初始化的时候相关的索引会自动被部署。

使用 CouchDB

本教程将讲述在 Hyperledger Fabric 中使用 CouchDB 作为状态数据库的步骤。现在,你应该已经熟悉 Fabric 的概念并且已经浏览了一些示例和教程。

本教程将带你按如下步骤与学习:

  1. 在 Hyperledger Fabric 中启用 CouchDB
  2. 创建一个索引
  3. 将索引添加到你的链码文件夹
  4. 安装和定义链码
  5. 查询 CouchDB 状态数据库
  6. 查询和索引的最佳实践
  7. 在 CouchDB 状态数据库查询中使用分页
  8. 升级索引
  9. 删除索引

想要更深入的研究 CouchDB 的话,请参阅 使用 CouchDB 作为状态数据库 ,关于 Fabric 账本的更多信息请参阅 Ledger 主题。下边的教程将详细讲述如何在你的区块链网络中使用 CouchDB 。

本教程将使用 Marbles sample 作为演示在 Fabric 中使用 CouchDB 的用例,并且将会把 Marbles 部署在 构建你的第一个网络 (BYFN)教程网络上。

为什么是CouchDB

Fabric 支持两种类型的节点数据库。LevelDB 是默认嵌入在 peer 节点的状态数据库。 LevelDB 用于将链码数据存储为简单的键-值对,仅支持键、键范围和复合键查询。CouchDB 是一个可选的状态数据库,支持以 JSON 格式在账本上建模数据并支持富查询,以便您查询实际数据内容而不是键。CouchDB 同样支持在链码中部署索引,以便高效查询和对大型数据集的支持。

为了发挥 CouchDB 的优势,也就是说基于内容的 JSON 查询,你的数据必须以 JSON 格式建模。你必须在设置你的网络之前确定使用 LevelDB 还是 CouchDB 。由于数据兼容性的问题,不支持节点从 LevelDB 切换为 CouchDB 。网络中的所有节点必须使用相同的数据库类型。如果你想 JSON 和二进制数据混合使用,你同样可以使用 CouchDB ,但是二进制数据只能根据键、键范围和复合键查询。

在Fabric中启用CouchDB

CouchDB 是独立于节点运行的一个数据库进程。在安装、管理和操作的时候有一些额外的注意事项。有一个可用的 Docker 镜像 CouchDB 并且我们建议它和节点运行在同一个服务器上。我们需要在每一个节点上安装一个 CouchDB 容器,并且更新每一个节点的配置文件 core.yaml ,将节点指向 CouchDB 容器core.yaml 文件的路径必须在环境变量 FABRIC_CFG_PATH 中指定

  • 对于 Docker 的部署,在节点容器中 FABRIC_CFG_PATH 指定的文件夹中的 core.yaml 是预先配置好的。如果你要使用 docker 环境,你可以通过重写 docker-compose-couch.yaml 中的环境变量来覆盖 core.yaml
  • 对于原生的二进制部署, core.yaml 包含在发布的构件中。

编辑 core.yaml 中的 stateDatabase 部分。将 stateDatabase 指定为 CouchDB 并且填写 couchDBConfig 相关的配置。在 Fabric 中配置 CouchDB 的更多细节,请参阅 CouchDB 配置

创建一个索引

为什么索引很重要?

索引可以让数据库不用在每次查询的时候都检查每一行,可以让数据库运行的更快和更高效。 一般来说,对频繁查询的数据进行索引可以使数据查询更高效。为了充分发挥 CouchDB 的优势 – 对 JSON 数据进行富查询的能力 – 并不需要索引,但是为了性能考虑强烈建议建立索引。另外,如果在一个查询中需要排序,CouchDB 需要在排序的字段有一个索引

没有索引的情况下富查询也是可以使用的,但是会在 CouchDB 的日志中抛出一个没有找到索引的警告。如果一个富查询中包含了一个排序的说明,需要排序的那个字段就必须有索引;否则,查询将会失败并抛出错误

为了演示构建一个索引,我们将会使用来自 Marbles sample. 的数据。 在这个例子中, Marbles 的数据结构定义如下:

1
2
3
4
5
6
7
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 属性来区分在链码命名空间中的每一个文档。(每一个链码都需要有他们自己的 CouchDB 数据库,也就是说,每一个链码都有它自己的键的命名空间。)

在 Marbles 数据结构的定义中, docType 用来识别这个文档或者资产是一个弹珠资产。 同时在链码数据库中也可能存在其他文档或者资产。数据库中的文档对于这些属性值来说都是可查询的。

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

需要以下三条信息来定义一个索引:

  • fields: 这些是常用的查询字段
  • name: 索引名
  • type: 它的内容一般是 json

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

1
2
3
4
5
6
7
{
    "index": {
        "fields": ["foo"]
    },
    "name" : "foo-index",
    "type" : "json"
}

可选地,设计文档( design document )属性 ddoc 可以写在索引的定义中。design document 是 CouchDB 结构,用于包含索引索引可以以组的形式定义在设计文档中以提升效率,但是 CouchDB 建议每一个设计文档包含一个索引

定义一个索引的时候,最好将 ddoc 属性和值包含在索引内。包含这个属性以确保在你需要的时候升级索引,这是很重要的。它还使你能够明确指定要在查询上使用的索引。

这里有另外一个使用 Marbles 示例定义索引的例子,在索引 indexOwner 使用了多个字段 docTypeowner 并且包含了 ddoc 属性:

1
2
3
4
5
6
7
8
{
  "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 只包含 owner 属性, index2 包含 owner 和 color 属性, index3 包含 owner、 color 和 size 属性。另外,注意,根据 CouchDB 的建议,每一个索引的定义都包含一个它们自己的 ddoc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
  "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 documentation

关于索引最后要说的是,Fabric 在数据库中为文档建立索引的时候使用一种称为 索引升温 (index warming) 的模式。 CouchDB 直到下一次查询的时候才会索引新的或者更新的文档。Fabric 通过在每一个数据区块提交完之后请求索引更新的方式,来确保索引处于 ‘热 (warm)’ 状态。这就确保了查询速度快,因为在运行查询之前不用索引文档。这个过程保持了索引的现状,并在每次新数据添加到状态数据的时候刷新。

将索引添加到你的链码文件夹

当你完成索引之后,你需要把它打包到你的链码中,以便于将它部署到合适的元数据文件夹。你可以使用 peer lifecycle chaincode 命令安装链码。JSON 索引文件必须放在链码目录的 META-INF/statedb/couchdb/indexes 路径下

下边的 Marbles 示例 展示了如何将索引打包到链码中。

Marbles Chaincode Index Package

这个例子包含了一个名为 indexOwnerDoc 的索引:

1
{"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}
启动网络

我们将会启动一个 Fabric 测试网络并且使用它来部署 marbles 链码。 使用下面的命令导航到 Fabric samples 中的目录 test-network :

1
cd fabric-samples/test-network

对于这个教程,我们希望在一个已知的初始状态进行操作。 下面的命令会删除正在进行的或停止的 docker 容器并且移除之前生成的构件:

1
./network.sh down

如果你之前从没运行过这个教程,在我们部署链码到网络之前你需要使用 vendor 来安装链码的依赖文件。 运行以下的命令:

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

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

1
./network.sh up createChannel -s couchdb

运行这个命令会创建两个使用 CouchDB 作为状态数据库的 fabric 节点。 同时也会创建一个排序节点和一个名为 mychannel 的通道

安装和定义链码

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

链码在安装到 Peer 节点之前需要打包。我们可以使用 peer lifecycle chaincode package 命令来打包弹珠链码。

  1. 启动测试网络后,在你终端拷贝粘贴下面的环境变量,这样就可以使用 Org1 管理员用户和网络交互。 确保你在 test-network 目录中。
1
2
3
4
5
6
7
export PATH=${PWD}/../bin:$PATH
export FABRIC_CFG_PATH=${PWD}/../config/
export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
  1. 使用下面的命令来打包 marbles 链码
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:
1
peer lifecycle chaincode install marbles.tar.gz

一个成功的安装命令会返回链码 id ,就像下面的返回信息:

1
2
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. 使用下面的命令来用你的当前节点查询已安装链码的 package ID 。
1
peer lifecycle chaincode queryinstalled

这个命令会返回和安装命令相同的 package ID 。 你应该看到类似下面的输出:

1
2
Installed chaincodes on peer:
Package ID: marbles_1:60ec9430b221140a45b96b4927d1c3af736c1451f8d432e2a869bdbf417f9787, Label: marbles_1
  1. 将 package ID 声明为一个环境变量。 将 peer lifecycle chaincode queryinstalled 命令返回的 marbles_1 的 package ID 粘贴到下面的命令中。 package ID 不是所有用户都一样,所以你需要使用终端返回的 package ID 来完成这个步骤。
1
export CC_PACKAGE_ID=marbles_1:60ec9430b221140a45b96b4927d1c3af736c1451f8d432e2a869bdbf417f9787
  1. 使用下面的命令让 Org1 同意 marbles 链码定义
1
2
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

命令成功运行的时候你应该看到和下面类似的信息:

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

在链码定义提交之前,我们需要大多数组织同意链码定义。这意味着我们需要 Org2 也同意该链码定义。因为我们不需要 Org2 背书链码并且不安装链码包到 Org2 的节点,所以 packageID 作为链码定义的一部分,我们不需要向 Org2 提供它

  1. 让终端使用 Org2 管理员身份操作。将下面的命令一起拷贝粘贴到节点容器并且一次性全部运行。
1
2
3
4
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 同意链码定义
1
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 命令来提交链码定义到通道
1
2
3
4
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

提交交易成功的时候你应该看到类似下面的信息:

1
2
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. 因为 marbles 链码包含一个初始化函数,所以在我们使用链码其他函数前需要使用 [peer chaincode invoke](https://hyperledger-fabric.readthedocs.io/zh_CN/release-2.2/commands/peerchaincode.html? chaincode instantiate#peer-chaincode-invoke) 命令调用 Init()
1
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 是否被创建成功。

为了查看节点 Docker 容器的日志,打开一个新的终端窗口,然后运行下边的命令来匹配索 引被创建的确认信息。

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

你将会看到类似下边的结果:

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

如果 Marbles 没有安装在节点 peer0.org1.example.com 上,你可能需要切换到其他的安装了 Marbles 的节点。

查询CouchDB状态数据库

现在索引已经在 JSON 中定义了并且和链码部署在了一起,链码函数可以对 CouchDB 状态数据库执行 JSON 查询,同时 peer 命令可以调用链码函数。

在查询的时候指定索引的名字是可选的。如果不指定,同时索引已经在被查询的字段上存在了,已存在的索引会自动被使用。

在查询的时候使用 use_index 关键字包含一个索引名字是一个好的习惯。如果不使用索引名,CouchDB 可能不会使用最优的索引。而且 CouchDB 也可能会不使用索引,但是在测试期间数据少的化你很难意识到。只有在数据量大的时候,你才可能会意识到因为 CouchDB 没有使用索引而导致性能较低

在链码中构建一个查询

你可以使用链码中定义的富查询来查询账本上的数据。 marbles02 示例 中包含了两个富查询方法:

  • queryMarbles
    • 一个 富查询 示例。这是一个可以将一个(选择器)字符串传入函数的查询。 这个查询对于需要在运行时动态创建他们自己的选择器的客户端应用程序很有用。 更多关于选择器的信息请参考 CouchDB selector syntax
  • queryMarblesByOwner
    • 一个查询逻辑保存在链码中的参数查询的示例。在这个例子中,函数值接受单个参数, 就是弹珠的主人。然后使用 JSON 查询语法查询状态数据库中匹配 “marble” 的 docType 和 拥有者 id 的 JSON 文档。
使用peer命令运行查询

由于缺少一个客户端程序,我们可以使用节点命令来测试链码中定义的查询函数。我们将自定义 [peer chaincode query](https://hyperledger-fabric.readthedocs.io/zh_CN/release-2.2/commands/peerchaincode.html? chaincode query#peer-chaincode-query) 命令来使用Marbles索引 indexOwner 并且使用 queryMarbles 函数查询所有 marbles 中拥有者是 “tom” 的 marble 。

在查询数据库之前,我们应该添加些数据。运行下面的命令使用 Org1 创建一个拥有者是 “tom” 的 marble

1
2
3
4
5
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 关键字来明确地指定索引的例子:

1
2
# 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

Marbles 链码中的函数名称。注意使用了一个 shim shim.ChaincodeStubInterface 来访问和修改账本。 getQueryResultForQueryString() 传递 queryString 给 shim API getQueryResult()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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 选择器 字符串的示例,用来查找所有 owner 属性值为 tommarble 的文档。

  • "use_index":["_design/indexOwnerDoc", "indexOwner"]

指定设计文档名 indexOwnerDoc 和索引名 indexOwner 。在这个示例中,查询选择器通过指定 use_index 关键字明确包含了索引名。回顾一下上边的索引定义 创建一个索引 , 它包含了设计文档, "ddoc":"indexOwnerDoc" 。在 CouchDB 中,如果你想在查询中明确包含索引名,在索引定义中必须包含 ddoc 值,然后它才可以被 use_index 关键字引用

利用索引的查询成功后返回如下结果:

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

查询和索引的最佳实践

由于不必扫描整个数据库,couchDB 中使用索引的查询会完成的更快。理解索引的机制会使你在网络中写出更高性能的查询语句并帮你的应用程序处理更大的数据或区块。

规划好安装在你链码上的索引同样重要。你应该每个链码只安装少量能支持大部分查询的索引添加太多索引或索引使用过多的字段会降低你网络的性能。这是因为每次区块提交后都会更新索引。 “索引升温( index warming )”需要更新的索引越多,完成交易的时间就越长

这部分的案例有助于演示查询该如何使用索引什么类型的查询拥有最好的性能。当你写查询的时候记得下面几点:

  • 使用的索引中所有字段必须同样包含在选择器和排序部分。
  • 越复杂的查询性能越低并且使用索引的几率也越低。
  • 你应该尽量避免会引起全表查询或全索引查询的操作符,比如: $or, $in and $regex

在教程的前面章节,你已经对 marbles 链码执行了下面的查询:

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

Marbles 链码已经安装了 indexOwnerDoc 索引:

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

注意查询中的字段 docTypeowner 都包含在索引中,这使得该查询成为一个完全支持查询( fully supported query )。 因此这个查询能使用索引中的数据,不需要搜索整个数据库。像这样的完全支持查询比你链码中的其他查询返回地更快。

如果你在上述查询中添加了额外字段,它仍会使用索引。然后,查询会另外在索引数据中查找符合额外字段的数据,导致相应时间变长。 下面的例子中查询仍然使用索引,但是会比前面的查询返回更慢。

1
2
# 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\"]}"]}'

没有包含全部索引字段的查询会查询整个数据库。举个例子,下面的查询使用 owner 字段查找数据, 没有指定该项拥有的类型。因为索引 ownerIndexDoc 包含两个字段 ownerdocType , 所以下面的查询不会使用索引。

1
2
# 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 使得查询会搜索每一个 marble 和每一条拥有者是 tom 的数据。

1
2
# 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 需要扫描索引中所有的项,导致响应时间变长。

索引不支持下面这个复杂查询的例子。

1
2
# 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。根据你账本的数据量,这个查询会很久才会响应或者可能超时。

虽然遵循查询的最佳实践非常重要,但是使用索引不是查询大量数据的解决方案。区块链的数据结构优化了校验和确定交易,但不适合数据分析或报告。如果你想要构建一个仪表盘( dashboard )作为应用程序的一部分或分析网络的数据,最佳实践是查询一个从你节点复制了数据的离线区块链数据库。这样可以使你了解区块链上的数据并且不会降低网络的性能或中断交易。

你可以使用来自你应用程序的区块或链码事件来写入交易数据到一个离线的链数据库或分析引擎。 对于每一个接收到的区块,区块监听应用将遍历区块中的每一个交易并根据每一个有效交易的 读写集 中的键值对构建一个数据存储。 文档 基于通道的 Peer 节点事件服务 提供了可重放事件,以确保下游数据存储的完整性。有关如何使用事件监听器将数据写入外部数据库的例子, 访问 Fabric Samples 的 Off chain data sample

在CouchDB状态数据库查询中使用分页

当 CouchDB 的查询返回了一个很大的结果集时,有一些将结果分页的 API 可以提供给链码调用。分页提供了一个将结果集合分区的机制,该机制指定了一个 pagesize 和起始点 – 一个从结果集 合的哪里开始的 书签 。客户端应用程序以迭代的方式调用链码来执行查询,直到没有更多的结果返回。更多信息请参考 topic on pagination with CouchDB

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

  • queryMarblesWithPagination

    一个 使用分页的 ad hoc 富查询 的示例。这是一个像上边的示例一样,可以将一个(选择器) 字符串传入函数的查询。在这个示例中,在查询中也包含了一个 pageSize 作为一个 书签

为了演示分页,需要更多的数据。本例假设你已经加入了 marble1 。在节点容器中执行下边的命令创建 4 个 “tom” 的弹珠,这样就创建了 5 个 “tom” 的弹珠:

1
2
3
4
5
6
7
8
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 从每一页从哪开始的 “锚(anchor)” 。(结果的每一页都返回一个唯一的书签)

  • queryMarblesWithPagination

Marbles 链码中函数的名称。注意 shim shim.ChaincodeStubInterface 用于访问和修改账本。 getQueryResultForQueryStringWithPagination() 将 queryString 、 pagesize 和 bookmark 传递给 shim API GetQueryResultWithPagination()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 没有指定 boomark 的方式调用 queryMarblesWithPagination 。

当没有指定 bookmark 的时候,查询从记录的**“第一”**页开始。

1
2
# 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",""]}'

下边是接收到的响应(为清楚起见,增加了换行),返回了五个弹珠中的三个,因为 pagesize 设置成了 3

1
2
3
4
5
[{"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"}}]

Bookmark 是 CouchDB 每次查询的时候唯一生成的,并显示在结果集中。将返回的 bookmark 传递给迭代查询的子集中来获取结果的下一个集合

下边是一个 pageSize 为 3 的调用 queryMarblesWithPagination 的 peer 命令。 注意一下这里,这次的查询包含了上次查询返回的 bookmark

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

下边是接收到的响应(为清楚起见,增加了换行),返回了五个弹珠中的三个,返回了剩下的两个记录:

1
2
3
4
[{"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"}}]

最后一个命令是调用 queryMarblesWithPagination 的 peer 命令,其中 pageSize 为 3 ,bookmark 是前一次查询返回的结果。

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

下边是接收到的响应(为清楚起见,增加了换行)。没有记录返回,说明所有的页面都获取到了

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

对于客户端应用程序如何使用分页迭代结果集,请在 Marbles sample 。 中搜索 getQueryResultForQueryStringWithPagination 函数。

升级索引

可能需要随时升级索引。相同的索引可能会存在安装的链码的子版本中。为了索引的升级, 原来的索引定义必须包含在设计文档 ddoc 属性和索引名。为了升级索引定义,使用相同的索引名并改变索引定义。简单编辑索引 JSON 文件并从索引中增加或者删除字段。 Fabric 只支持 JSON 类型的索引。不支持改变索引类型。升级后的索引定义在链码定义提交之后会重新部署在节点的状态数据库中。

如果状态数据库有大量数据,重建索引的过程会花费较长时间,在此期间链码执行或者查询可能会失败或者超时。

迭代索引定义

如果你在开发环境中访问你的节点的 CouchDB 状态数据库,你可以迭代测试各种索引以支持你的链码查询。链码的任何改变都可能需要重新部署。使用 CouchDB Fauxton interface 或者命令行 curl 工具来创建和升级索引

Fauxton 是用于创建、升级和部署 CouchDB 索引的一个网页,如果你想尝试这个接口, 有一个 Marbles 示例中索引的 Fauxton 版本格式的例子。如果你使用 CouchDB 部署了测试网络,可以通过在浏览器的导航栏中打开 http://localhost:5984/_utils 来 访问 Fauxton 。

另外,如果你不想使用 Fauxton UI,下边是通过 curl 命令在 mychannel_marbles 数据库上创建索引的例子:

1
2
3
4
5
6
7
# 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 替换 hostname:port 。

删除索引

Fabric 工具不能删除索引。如果你需要删除索引,就要手动使用 curl 命令或者 Fauxton 接口操作数据库。

删除索引的 curl 命令格式如下:

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

要删除本教程中的索引,curl 命令应该是:

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

创建通道

为了在Hyperledger Fabric网络上创建和转移资产,一个组织需要加入一个通道。通道是特定组织间交流的私有层并且对其它网络成员不可见。每个通道由单独的分隔的帐本组成,该分隔的帐本只能被通道成员读取和写入,他们的节点被允许加入到该通道并接收来自排序服务的新交易块。而在Peer节点、排序节点和CA构成的物理基础网络中,通道是组织间和组织内部交流的过程。

由于通道在Fabric运营和治理中发挥的基础性作用,我们提供一系列的教程,将涵盖如何创建通道的不同方面。 创建新通道 教程描述了网络管理员需要执行的操作步骤。 使用configtx.yaml创建通道配置 教程介绍了创建一个通道的概念,然后单独讨论通道策略

创建新通道

您可以使用本教程来学习如何使用 configtxgen CLI工具创建新通道,然后使用peer channel命令让您的Peer节点加入该通道。尽管本教程将利用Fabric测试网络来创建新通道,但是本教程中的步骤生产环境中的网络运维人员也可以使用。

在创建通道的过程中,本教程将带您逐个了解以下步骤和概念:

  • 配置configtxgen工具
  • 使用configtx.yaml
  • Orderer系统通道
  • 创建应用通道
  • Peer加入通道
  • 设置锚节点
配置configtxgen工具

通过构建 通道创建交易 并将该交易提交给排序服务来创建通道。通道创建交易 指定通道的初始配置,并由排序服务写入 通道创世块。尽管可以手动构建通道创建交易文件,但使用configtxgen工具会更容易。该工具通过读取定义通道配置的configtx.yaml文件,然后将相关信息写入 通道创建交易 中来工作。在下一节讨论configtx.yaml文件之前,我们可以先开始下载并配置好configtxgen工具。

您可以按照安装示例,二进制文件和Docker镜像的步骤下载configtxgen二进制文件。configtxgen和其他Fabric工具一起将被下载到本地fabric-samples的bin文件夹中。

对于本教程,我们将要在fabric-samples目录下的test-network目录中进行操作。使用以下命令进入到该目录:

1
cd fabric-samples/test-network

在本教程的其余部分中,我们将在test-network目录进行操作。使用以下命令将configtxgen工具添加到您的CLI路径:

1
export PATH=${PWD}/../bin:$PATH

为了使用configtxgen,您需要将FABRIC_CFG_PATH环境变量设置为本地包含configtx.yaml文件的目录的路径。在本教程中,我们将在configtx文件夹中引用用于设置Fabric测试网络的configtx.yaml

1
export FABRIC_CFG_PATH=${PWD}/configtx

您可以通过打印configtxgen帮助文本来检查是否可以使用该工具:

1
configtxgen --help
使用configtx.yaml

configtx.yaml文件指定新通道的通道配置。建立通道配置所需的信息在configtx.yaml文件中以读写友好的形式指定。configtxgen工具使用configtx.yaml文件中定义的通道配置文件来创建通道配置,并将其写入protobuf格式,然后由Fabric读取。

您可以在test-network目录下的configtx文件夹中找到configtx.yaml文件,该文件用于部署测试网络。该文件包含以下信息,我们将使用这些信息来创建新通道:

  • Organizations: 可以成为您的通道成员的组织。每个组织都有对用于建立通道MSP的密钥信息的引用。
  • Ordering service: 哪些排序节点将构成网络的排序服务,以及它们将用于同意一致交易顺序的共识方法。该文件还包含将成为排序服务管理员的组织。
  • Channel policies: 文件的不同部分共同定义策略,这些策略将控制组织与通道的交互方式以及哪些组织需要批准通道更新。就本教程而言,我们将使用Fabric使用的默认策略。
  • Channel profiles: 每个通道配置文件都引用configtx.yaml文件其他部分的信息来构建通道配置。使用预设文件来创建Orderer系统通道的创世块以及将被Peer组织使用的通道。为了将它们与系统通道区分开来,Peer组织使用的通道通常称为应用通道。

configtxgen工具使用configtx.yaml文件为系统通道创建完整的创世块。因此,系统通道配置文件需要指定完整的系统通道配置。用于创建通道创建交易的应用通道配置文件仅需要包含创建应用通道所需的其他配置信息。

您可以访问使用configtx.yaml创建通道创世块教程,以了解有关此文件的更多信息。现在,我们将返回创建通道的操作方面,尽管在后续的步骤中将引用此文件的某些部分。

启动网络

我们将使用Fabric测试网络来创建新通道。出于本教程的考虑,我们希望从一个已知的初始状态进行操作。以下命令将停掉所有容器并删除任何之前生成的文件。确保您仍在本地fabric-samplestest-network目录中进行操作。

1
./network.sh down

您可以使用以下命令来启动测试网络:

1
./network.sh up

这个命令将使用在configtx.yaml文件中定义的两个Peer组织和单个Orderer组织创建一个Fabric网络。两个组织将各自运营一个Peer节点,而排序服务管理员将运营单个Orderer节点。运行命令时,脚本将打印出正在创建的节点的日志:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Creating network "net_test" with the default driver
Creating volume "net_orderer.example.com" with default driver
Creating volume "net_peer0.org1.example.com" with default driver
Creating volume "net_peer0.org2.example.com" with default driver
Creating orderer.example.com    ... done
Creating peer0.org2.example.com ... done
Creating peer0.org1.example.com ... done
CONTAINER ID        IMAGE                               COMMAND             CREATED             STATUS                  PORTS                              NAMES
8d0c74b9d6af        hyperledger/fabric-orderer:latest   "orderer"           4 seconds ago       Up Less than a second   0.0.0.0:7050->7050/tcp             orderer.example.com
ea1cf82b5b99        hyperledger/fabric-peer:latest      "peer node start"   4 seconds ago       Up Less than a second   0.0.0.0:7051->7051/tcp             peer0.org1.example.com
cd8d9b23cb56        hyperledger/fabric-peer:latest      "peer node start"   4 seconds ago       Up 1 second             7051/tcp, 0.0.0.0:9051->9051/tcp   peer0.org2.example.com

我们测试网络实例的部署没有创建应用通道。但是,当您执行./network.sh up命令时,测试网络脚本会创建系统通道。在底层,脚本使用configtxgen工具和configtx.yaml文件构建系统通道的创世块。因为系统通道用于创建其他通道,所以在创建应用通道之前,我们需要花一些时间来了解Orderer系统通道。

Orderer系统通道

在Fabric网络中创建的第一个通道是系统通道。系统通道定义了形成排序服务的Orderer节点集合和充当排序服务管理员的组织集合。

系统通道还包括属于区块链联盟的组织。联盟是一组Peer组织,它们属于系统通道,但不是排序服务的管理员。联盟成员可以创建新通道,并包括其他联盟组织作为通道成员

要部署新的排序服务,需要系统通道的创世块。当您执行./network.sh up命令时,测试网络脚本已经创建了系统通道创世块。创世块用于部署单个Orderer节点,该Orderer节点使用该块创建系统通道并形成网络的排序服务。如果检查./ network.sh脚本的输出,则可以在日志中找到创建创世块的命令:

1
configtxgen -profile TwoOrgsOrdererGenesis -channelID system-channel -outputBlock ./system-genesis-block/genesis.block

configtxgen工具使用来自configtx.yamlTwoOrgsOrdererGenesis通道配置文件来写入创世块并将其存储在system-genesis-block文件夹中。 您可以在下面看到TwoOrgsOrdererGenesis配置文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Profiles:
    TwoOrgsOrdererGenesis:
        <<: *ChannelDefaults
        Orderer:
            <<: *OrdererDefaults
            Organizations:
                - *OrdererOrg
            Capabilities:
                <<: *OrdererCapabilities
        Consortiums:
            SampleConsortium:
                Organizations:
                    - *Org1
                    - *Org2

配置文件的Orderer部分:创建测试网络使用的单节点Raft排序服务,并OrdererOrg作为排序服务管理员。配置文件的Consortiums部分创建了一个名为SampleConsortium的Peer组织的联盟。 这两个Peer组织Org1和Org2都是该联盟的成员。因此,我们可以将两个组织都包含在测试网络创建的新通道中。如果我们想添加另一个组织作为通道成员而又不将该组织添加到联盟中,则我们首先需要使用Org1和Org2创建通道,然后通过更新通道配置添加该组织。

创建应用通道

现在我们已经部署了网络的节点并使用network.sh脚本创建了Orderer系统通道,我们可以开始为Peer组织创建新通道。我们已经设置了使用configtxgen工具所需的环境变量。运行以下命令channel1创建一个创建通道的交易

1
configtxgen -profile TwoOrgsChannel -outputCreateChannelTx ./channel-artifacts/channel1.tx -channelID channel1

-channelID是将要创建的通道的名称。通道名称必须全部为小写字母,少于250个字符,并且与正则表达式[a-z][a-z0-9.-]*匹配。该命令使用-profile标志来引用configtx.yaml中的TwoOrgsChannel:配置文件,测试网络使用它来创建应用通道

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
TwoOrgsChannel:
    Consortium: SampleConsortium
    <<: *ChannelDefaults
    Application:
        <<: *ApplicationDefaults
        Organizations:
            - *Org1
            - *Org2
        Capabilities:
            <<: *ApplicationCapabilities

该配置文件从系统通道引用SampleConsortium的名称,并且包括来自该联盟的两个Peer组织作为通道成员。因为系统通道用作创建应用通道的模板,所以系统通道中定义的排序节点成为新通道的默认共识者集合。排序服务的管理员成为该通道的Orderer管理员。可以使用通道更新在共识者者集合中添加或删除Orderer节点和Orderer组织。

如果命令执行成功,您将看到configtxgen的日志加载configtx.yaml文件并打印通道创建交易:

1
2
3
4
2020-03-11 16:37:12.695 EDT [common.tools.configtxgen] main -> INFO 001 Loading configuration
2020-03-11 16:37:12.738 EDT [common.tools.configtxgen.localconfig] Load -> INFO 002 Loaded configuration: /Usrs/fabric-samples/test-network/configtx/configtx.yaml
2020-03-11 16:37:12.740 EDT [common.tools.configtxgen] doOutputChannelCreateTx -> INFO 003 Generating new channel configtx
2020-03-11 16:37:12.789 EDT [common.tools.configtxgen] doOutputChannelCreateTx -> INFO 004 Writing new channel tx

我们可以使用peer CLI将通道创建交易提交给排序服务。要使用peer CLI,我们需要将FABRIC_CFG_PATH设置为fabric-samples/config目录中的core.yaml文件。通过运行以下命令来设置FABRIC_CFG_PATH环境变量:

1
export FABRIC_CFG_PATH=$PWD/../config/

在排序服务创建通道之前,排序服务将检查提交请求的身份的许可。默认情况下,只有属于系统通道的联盟组织的管理员身份才能创建新通道。发出以下命令,以Org1中的admin用户身份运行peer CLI:

1
2
3
4
5
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 channel create -o localhost:7050  --ordererTLSHostnameOverride orderer.example.com -c channel1 -f ./channel-artifacts/channel1.tx --outputBlock ./channel-artifacts/channel1.block --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem

上面的命令使用-f标志提供通道创建交易文件的路径,并使用-c标志指定通道名称。-o标志用于选择将用于创建通道的排序节点。--cafile是Orderer节点的TLS证书的路径。当您运行peer channel create命令时,peer CLI将生成以下响应:

1
2
2020-03-06 17:33:49.322 EST [channelCmd] InitCmdFactory -> INFO 00b Endorser and orderer connections initialized
2020-03-06 17:33:49.550 EST [cli.common] readBlock -> INFO 00c Received block: 0

由于我们使用的是Raft排序服务,因此您可能会收到一些状态不可用的消息,您可以放心地忽略它们。该命令会将新通道的创世块返回到--outputBlock标志指定的位置

Peer加入通道

创建通道后,我们可以让Peer加入通道。属于该通道成员的组织可以使用peer channel fetch命令从排序服务中获取通道创世块。然后,组织可以使用创世块,通过peer channel join命令将Peer加入到该通道。一旦Peer加入通道,Peer将通过从排序服务中获取通道上的其他区块来构建区块链账本。

由于我们已经以Org1管理员的身份使用peer CLI,因此让我们将Org1的Peer加入到通道中。由于Org1提交了通道创建交易,因此我们的文件系统上已经有了通道创世块。使用以下命令将Org1的Peer加入通道。

1
peer channel join -b ./channel-artifacts/channel1.block

环境变量CORE_PEER_ADDRESS已设置为以peer0.org1.example.com为目标。命令执行成功后将生成peer0.org1.example.com加入通道的响应:

1
2
2020-03-06 17:49:09.903 EST [channelCmd] InitCmdFactory -> INFO 001 Endorser and orderer connections initialized
2020-03-06 17:49:10.060 EST [channelCmd] executeJoin -> INFO 002 Successfully submitted proposal to join channel

您可以使用peer channel getinfo命令验证Peer是否已加入通道

1
peer channel getinfo -c channel1

该命令将列出通道的区块高度和最新区块的哈希。由于创世块是通道上的唯一区块,因此通道的高度将为1:

1
2
2020-03-13 10:50:06.978 EDT [channelCmd] InitCmdFactory -> INFO 001 Endorser and orderer connections initialized
Blockchain info: {"height":1,"currentBlockHash":"kvtQYYEL2tz0kDCNttPFNC4e6HVUFOGMTIDxZ+DeNQM="}

现在,我们可以将Org2的Peer加入通道。设置以下环境变量,以Org2管理员的身份运行peer CLI。环境变量还将把Org2的Peer peer0.org1.example.com设置为目标Peer。

1
2
3
4
5
export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_LOCALMSPID="Org2MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=localhost:9051

尽管我们的文件系统上仍然有通道创世块,但在更现实的场景下,Org2将从排序服务中获取块。例如,我们将使用peer channel fetch命令来获取Org2的创世块

1
peer channel fetch 0 ./channel-artifacts/channel_org2.block -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com -c channel1 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem

命令使用0来指定它需要获取加入通道所需的创世块。如果命令成功执行,则应该看到以下输出:

1
2
2020-03-13 11:32:06.309 EDT [channelCmd] InitCmdFactory -> INFO 001 Endorser and orderer connections initialized
2020-03-13 11:32:06.336 EDT [cli.common] readBlock -> INFO 002 Received block: 0

该命令返回通道生成块并将其命名为channel_org2.block,以将其与由Org1拉取的块区分开。现在,您可以使用该块将Org2的Peer加入该通道

1
peer channel join -b ./channel-artifacts/channel_org2.block
配置锚节点

组织的Peer加入通道后,他们应至少选择一个Peer成为锚定节点。为了利用诸如私有数据和服务发现之类的功能,需要Peer锚节点每个组织都应在一个通道上设置多个锚节点以实现冗余。有关Gossip和Peer锚节点的更多信息,请参见Gossip数据分发协议

通道配置中包含每个组织的Peer锚节点的端点信息。每个通道成员可以通过更新通道来指定其Peer锚节点。我们将使用configtxlator工具更新通道配置,并为Org1和Org2选择锚节点。设置Peer锚节点的过程与进行其他通道更新所需的步骤相似,并介绍了如何使用configtxlator 更新通道配置。您还需要在本地计算机上安装jq工具

我们将从为Org1选择一个Peer锚节点开始。第一步是使用peer channel fetch命令来获取最新的通道配置块。设置以下环境变量,以Org1 管理员身份运行peer CLI:

1
2
3
4
5
6
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 channel fetch config channel-artifacts/config_block.pb -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com -c channel1 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem

由于最新的通道配置块是通道创世块,因此您将看到该通道的命令返回块0。

1
2
3
4
2020-04-15 20:41:56.595 EDT [channelCmd] InitCmdFactory -> INFO 001 Endorser and orderer connections initialized
2020-04-15 20:41:56.603 EDT [cli.common] readBlock -> INFO 002 Received block: 0
2020-04-15 20:41:56.603 EDT [channelCmd] fetch -> INFO 003 Retrieving last config block: 0
2020-04-15 20:41:56.608 EDT [cli.common] readBlock -> INFO 004 Received block: 0

通道配置块存储在channel-artifacts文件夹中,以使更新过程与其他工件分开。进入到channel-artifacts文件夹以完成以下步骤:

1
cd channel-artifacts

现在,我们可以开始使用configtxlator工具开始通道配置相关工作。第一步是将来自protobuf的块解码为可以读写友好的JSON对象。我们还将去除不必要的块数据,仅保留通道配置。

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

这些命令将通道配置块转换为简化的JSON config.json,它将作为我们更新的基准。因为我们不想直接编辑此文件,所以我们将制作一个可以编辑的副本。我们将在以后的步骤中使用原始的通道配置。

1
cp config.json config_copy.json

您可以使用jq工具将Org1的Peer锚节点添加到通道配置中。

1
jq '.channel_group.groups.Application.groups.Org1MSP.values += {"AnchorPeers":{"mod_policy": "Admins","value":{"anchor_peers": [{"host": "peer0.org1.example.com","port": 7051}]},"version": "0"}}' config_copy.json > modified_config.json

完成此步骤后,我们在modified_config.json文件中以JSON格式获取了通道配置的更新版本。现在,我们可以将原始和修改的通道配置都转换回protobuf格式,并计算它们之间的差异

1
2
3
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 channel1 --original config.pb --updated modified_config.pb --output config_update.pb

名为config_update.pb的新的protobuf包含我们需要应用于通道配置的Peer锚节点更新。我们可以将配置更新包装在交易Envelope中,以创建通道配置更新交易

1
2
3
configtxlator proto_decode --input config_update.pb --type common.ConfigUpdate --output config_update.json
echo '{"payload":{"header":{"channel_header":{"channel_id":"channel1", "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

现在,我们可以使用**最终的工件config_update_in_envelope.pb**来更新通道。回到test-network目录:

1
cd ..

我们可以通过向peer channel update命令提供新的通道配置来添加Peer锚节点。因为我们正在更新仅影响Org1的部分通道配置,所以其他通道成员不需要批准通道更新

1
peer channel update -f channel-artifacts/config_update_in_envelope.pb -c channel1 -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

通道更新成功后,您应该看到以下响应:

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

我们可以**为Org2设置锚节点。**因为我们是第二次进行该过程,所以我们将更快地完成这些步骤。 设置环境变量,以Org2管理员的身份运行peer CLI:

1
2
3
4
5
export CORE_PEER_TLS_ENABLED=true
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
peer channel fetch config channel-artifacts/config_block.pb -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com -c channel1 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem

回到channel-artifacts目录:

1
cd channel-artifacts

然后,您可以解码并复制配置块

1
2
3
configtxlator proto_decode --input config_block.pb --type common.Block --output config_block.json
jq .data.data[0].payload.data.config config_block.json > config.json
cp config.json config_copy.json

通道配置中将加入通道的Org2的Peer添加为锚节点

1
jq '.channel_group.groups.Application.groups.Org2MSP.values += {"AnchorPeers":{"mod_policy": "Admins","value":{"anchor_peers": [{"host": "peer0.org2.example.com","port": 9051}]},"version": "0"}}' config_copy.json > modified_config.json

现在,我们可以将原始和更新的通道配置都转换回protobuf格式,并计算它们之间的差异

1
2
3
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 channel1 --original config.pb --updated modified_config.pb --output config_update.pb

将配置更新包装在交易Envelope中以创建通道配置更新交易:

1
2
3
configtxlator proto_decode --input config_update.pb --type common.ConfigUpdate --output config_update.json
echo '{"payload":{"header":{"channel_header":{"channel_id":"channel1", "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

回到test-network目录.

1
cd ..

通过执行以下命令来更新通道并设置Org2的Peer锚节点

1
peer channel update -f channel-artifacts/config_update_in_envelope.pb -c channel1 -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

您可以通过运行peer channel info命令来确认通道已成功更新:

1
peer channel getinfo -c channel1

现在,已经通过在通道创世块中添加两个通道配置块来更新通道,通道的高度将增加到3:

1
Blockchain info: {"height":3,"currentBlockHash":"eBpwWKTNUgnXGpaY2ojF4xeP3bWdjlPHuxiPCTIMxTk=","previousBlockHash":"DpJ8Yvkg79XHXNfdgneDb0jjQlXLb/wxuNypbfHMjas="}
在新通道上部署链码

通过将链码部署到通道,我们可以确认该通道已成功创建。我们可以使用network.sh脚本将Fabcar链码部署到任何测试网络通道。使用以下命令将链码部署到我们的新通道

1
./network.sh deployCC -ccn fabcar -ccp ../chaincode/fabcar/go -ccl go -c channel1

运行命令后,您应该在日志中看到链代码已部署到通道。调用链码将数据添加到通道账本中,然后查询。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[{"Key":"CAR0","Record":{"make":"Toyota","model":"Prius","colour":"blue","owner":"Tomoko"}},
{"Key":"CAR1","Record":{"make":"Ford","model":"Mustang","colour":"red","owner":"Brad"}},
{"Key":"CAR2","Record":{"make":"Hyundai","model":"Tucson","colour":"green","owner":"Jin Soo"}},
{"Key":"CAR3","Record":{"make":"Volkswagen","model":"Passat","colour":"yellow","owner":"Max"}},
{"Key":"CAR4","Record":{"make":"Tesla","model":"S","colour":"black","owner":"Adriana"}},
{"Key":"CAR5","Record":{"make":"Peugeot","model":"205","colour":"purple","owner":"Michel"}},
{"Key":"CAR6","Record":{"make":"Chery","model":"S22L","colour":"white","owner":"Aarav"}},
{"Key":"CAR7","Record":{"make":"Fiat","model":"Punto","colour":"violet","owner":"Pari"}},
{"Key":"CAR8","Record":{"make":"Tata","model":"Nano","colour":"indigo","owner":"Valeria"}},
{"Key":"CAR9","Record":{"make":"Holden","model":"Barina","colour":"brown","owner":"Shotaro"}}]
===================== Query successful on peer0.org1 on channel 'channel1' =====================

使用configtx.yaml创建通道配置

通过构建指定通道的初始配置的通道创建交易来创建通道。通道配置存储在账本中,并管理所有添加到通道的后续块。通道配置指定哪些组织是通道成员,可以在通道上添加新块的排序节点,以及管理通道更新的策略。可以通过通道配置更新来更新存储在通道创世块中的初始通道配置。如果足够多的组织批准通道更新,则在将新通道配置块提交到通道后,将对其进行管理。

虽然可以手动构建通道创建交易文件,但使用configtx.yaml文件和configtxgen工具可以更轻松地创建通道。configtx.yaml文件包含以易于理解和编辑的格式构建通道配置所需的信息。configtxgen工具读取configtx.yaml文件中的信息,并将其写入Fabric可以读取的protobuf格式

概览

您可以使用本教程来学习如何使用configtx.yaml文件来构建存储在创世块中的初始通道配置。本教程将讨论由文件的每个部分构建的通道配置部分。

由于文件的不同部分共同创建用于管理通道的策略,因此我们将在独立的教程中讨论通道策略。

创建通道教程的基础上,我们将使用configtx.yaml文件作为示例来部署Fabric测试网络。在本地计算机上打开命令终端,然后导航到本地Fabric示例中的test-network目录:

1
cd fabric-samples/test-network

测试网络使用的configtx.yaml文件位于configtx文件夹中。在文本编辑器中打开该文件。在本教程的每一节中,您都可以参考该文件。您可以在Fabric示例配置中找到configtx.yaml文件的更详细版本。

Organizations

通道配置中包含的最重要信息是作为通道成员的组织。每个组织都由MSP ID和通道MSP标识。通道MSP存储在通道配置中,并包含用于标识组织的节点,应用程序和管理员的证书configtx.yaml文件的Organizations部分用于为通道的每个成员创建通道MSP和随附的MSP ID。

测试网络使用的configtx.yaml文件包含三个组织。可以添加到应用程序通道的两个组织是Peer组织Org1和Org2。OrdererOrg是一个Orderer组织,是排序服务的管理员。因为最佳做法是使用不同的证书颁发机构来部署Peer节点和Orderer节点,所以即使组织实际上是由同一公司运营,也通常将其称为Peer组织或Orderer组织。

您可以在下面看到configtx.yaml的一部分,该部分定义了测试网络的Org1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- &Org1
    # DefaultOrg defines the organization which is used in the sampleconfig
    # of the fabric.git development environment
    Name: Org1MSP

    # ID to load the MSP definition as
    ID: Org1MSP

    MSPDir: ../organizations/peerOrganizations/org1.example.com/msp

    # Policies defines the set of policies at this level of the config tree
    # For organization policies, their canonical path is usually
    #   /Channel/<Application|Orderer>/<OrgName>/<PolicyName>
    Policies:
        Readers:
            Type: Signature
            Rule: "OR('Org1MSP.admin', 'Org1MSP.peer', 'Org1MSP.client')"
        Writers:
            Type: Signature
            Rule: "OR('Org1MSP.admin', 'Org1MSP.client')"
        Admins:
            Type: Signature
            Rule: "OR('Org1MSP.admin')"
        Endorsement:
            Type: Signature
            Rule: "OR('Org1MSP.peer')"
        # NOTE: this value should only be set when using the deprecated
        # `configtxgen --outputAnchorPeersUpdate` command. It is recommended
        # to instead use the channel configuration update process to set the
        # anchor peers for each organization.
    # leave this flag set to true.
    AnchorPeers:
        # AnchorPeers defines the location of peers which can be used
        # for cross org gossip communication.  Note, this value is only
        # encoded in the genesis block in the Application section context
        - Host: peer0.org1.example.com
          Port: 7051
  • Name字段是用于标识组织的非正式名称

  • ID字段是组织的MSP ID。MSP ID充当组织的唯一标识符,并且由通道策略引用,并包含在提交给通道的交易中。

  • MSPDir组织创建的MSP文件夹的路径configtxgen工具将使用此MSP文件夹来创建通道MSP。该MSP文件夹需要包含以下信息,这些信息将被传输到通道MSP并存储在通道配置中:

    • 一个CA根证书,为组织建立信任根。CA根证书用于验证应用程序,节点或管理员是否属于通道成员。

    • 来自TLS CA的根证书,该证书颁发了Peer节点或Orderer节点的TLS证书。TLS根证书用于通过Gossip协议标识组织。

    • 如果启用了Node OU,则MSP文件夹需要包含一个config.yaml文件,该文件根据x509证书的OU标识管理员,节点和客户端。

    • 如果未启用Node OU,则MSP需要包含一个admincerts文件夹,其中包含组织管理员身份的签名证书。

用于创建通道MSP的MSP文件夹仅包含公共证书。如此一来,您可以在本地生成MSP文件夹,然后将MSP发送到创建通道的组织。

  • Policies部分用于定义一组引用通道成员的签名策略。我们将在讨论通道策略时更详细地讨论这些策略。
  • AnchorPeers字段列出了组织的锚节点。为了利用诸如私有数据和服务发现之类的功能,锚节点是必需的。建议组织选择至少一个锚节点。虽然组织可以使用configtxgen工具首次选择其通道上的锚节点(比如像上面的配置文件中那样直接指明锚节点),但建议每个组织都使用configtxlator工具来设置锚节点来更新通道配置(上一教程中演示的那样)。因此,该字段不是必须的
Capabilities

Fabric通道可以由运行不同版本的Hyperledger Fabric的Orderer节点和Peer节点加入。通道功能通过仅启用某些功能,就可以允许运行不同Fabric二进制文件的组织参与同一通道。例如,只要通道功能级别设置为V1_4_X或更低,则运行Fabric v1.4的组织和运行Fabric v2.x的组织可以加入同一通道。所有通道成员都无法使用Fabric v2.0中引入的功能。

configtx.yaml文件中,您将看到三个功能组:

  • Channel功能设置可以由Peer节点和Orderer节点运行的Fabric的最低版本。由于Fabric测试网络的所有Peer和Orderer节点都运行版本v2.x,因此每个功能组均设置为V2_0。因此,运行Fabric版本低于v2.0的节点不能加入测试网络。有关更多信息,请参见capabilities概念主题。
  • Orderer功能可控制Orderer节点使用的功能,例如Raft共识,并设置可通过Orderer属于通道共识者集合的节点运行的Fabric二进制文件的最低版本。
  • Application功能可控制Peer节点使用的功能,例如Fabric链码生命周期,并可以设置由加入通道的Peer运行的Fabric二进制文件的最低版本。
Application

Application部分定义了控制Peer组织如何与应用程序通道交互的策略。这些策略控制需要批准链码定义或给更新通道配置的请求签名的Peer组织的数量。这些策略还用于限制对通道资源的访问,例如写入通道账本查询通道事件的能力。

测试网络使用Hyperledger Fabric提供的默认Application策略。如果您使用默认策略,则所有Peer组织都将能够读取数据并将数据写入账本。默认策略还要求大多数通道成员给通道配置更新签名,并且大多数通道成员需要批准链码定义,然后才能将链码部署到通道。本部分的内容在通道策略教程中进行了更详细的讨论。

Orderer

每个通道配置都在通道共识者集合中包括Orderer节点。共识者集合是一组排序节点,它们能够创建新的块并将其分发给加入该通道的Peer节点。在通道配置中存储作为共识者集合的成员的每个Orderer节点的端点信息。

测试网络使用configtx.yaml文件的Orderer部分来创建单节点Raft 排序服务。

  • OrdererType字段用于选择Raft作为共识类型
1
OrdererType: etcdraft

Raft 排序服务由可以参与共识过程的共识者列表定义。因为测试网络仅使用一个Orderer节点,所以共识者列表仅包含一个端点:

1
2
3
4
5
6
7
8
EtcdRaft:
    Consenters:
    - Host: orderer.example.com
      Port: 7050
      ClientTLSCert: ../organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/server.crt
      ServerTLSCert: ../organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/server.crt
    Addresses:
    - orderer.example.com:7050

共识者列表中的每个Orderer节点均由其端点地址以及其客户端和服务器TLS证书标识。如果要部署多节点排序服务,则需要提供每个节点的主机名,端口和每个节点使用的TLS证书的路径。您还需要将每个排序节点的端点地址添加到Addresses列表中。

  • 您可以使用BatchTimeoutBatchSize字段通过更改每个块的最大大小以及创建新块的频率来调整通道的延迟和吞吐量。
  • Policies部分创建用于管理通道共识者集合的策略。测试网络使用Fabric提供的默认策略,该策略要求大多数Orderer管理员批准添加或删除Orderer节点,组织或对分块切割参数进行更新。

因为测试网络用于开发和测试,所以它使用由单个Orderer节点组成的排序服务。生产中部署的网络应使用多节点排序服务以确保安全性和可用性。要了解更多信息,请参阅配置和操作Raft排序服务

Channel

通道部分定义了用于管理最高层级通道配置策略。对于应用程序通道,这些策略控制哈希算法,用于创建新块的数据哈希结构以及通道功能级别。在系统通道中,这些策略还控制Peer组织的联盟的创建或删除。

测试网络使用Fabric提供的默认策略,该策略要求大多数排序服务管理员需要批准对系统通道中这些值的更新。在应用程序通道中,更改需要获得大多数Orderer组织和大多数通道成员的批准。大多数用户不需要更改这些值

Profiles

configtxgen工具读取Profiles部分中的通道配置文件以构建通道配置。每个配置文件都使用YAML语法从文件的其他部分收集数据。 configtxgen工具使用此配置为应用程序通道创建通道创建交易,或为系统通道写入通道创世块。要了解有关YAML语法的更多信息,Wikipedia提供了一个良好的入门指南。

测试网络使用的configtx.yaml包含两个通道配置文件TwoOrgsOrdererGenesisTwoOrgsChannel

TwoOrgsOrdererGenesis配置文件用于创建系统通道创世块

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
TwoOrgsOrdererGenesis:
    <<: *ChannelDefaults
    Orderer:
        <<: *OrdererDefaults
        Organizations:
            - *OrdererOrg
        Capabilities:
            <<: *OrdererCapabilities
    Consortiums:
        SampleConsortium:
            Organizations:
                - *Org1
                - *Org2

系统通道定义了排序服务的节点以及排序服务管理员的组织集合。系统通道还包括一组属于区块链联盟的Peer组织。联盟中每个成员的通道MSP都包含在系统通道中,从而允许他们创建新的应用程序通道并将联盟成员添加到新通道中。

该配置文件创建一个名为SampleConsortium的联盟,该联盟在configtx.yaml文件中包含两个Peer组织Org1和Org2。配置文件的Orderer部分使用文件的**Orderer:**部分中定义的单节点Raft 排序服务。Organizations:部分中的OrdererOrg成为排序服务的唯一管理员。因为我们唯一的Orderer节点正在运行Fabric 2.x,所以我们可以将Orderer系统通道功能设置为V2_0。系统通道使用Channel部分中的默认策略,并启用V2_0作为通道功能级别。

测试网络使用TwoOrgsChannel配置文件创建应用程序通道

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
TwoOrgsChannel:
    Consortium: SampleConsortium
    <<: *ChannelDefaults
    Application:
        <<: *ApplicationDefaults
        Organizations:
            - *Org1
            - *Org2
        Capabilities:
            <<: *ApplicationCapabilities

排序服务将系统通道用作创建应用程序通道的模板。在系统通道中定义的排序服务的节点成为新通道的默认共识者集合,而排序服务的管理员则成为该通道的Orderer管理员。通道成员的通道MSP从系统通道转移到新通道。创建通道后,可以通过更新通道配置来添加或删除Orderer节点。您还可以更新通道配置以将其他组织添加为通道成员

TwoOrgsChannel提供了测试网络系统通道托管的联盟名称SampleConsortium。因此,TwoOrgsOrdererGenesis配置文件中定义的排序服务成为通道共识者集合。在Application部分中,来自联盟的两个组织Org1和Org2均作为通道成员包括在内。通道使用V2_0作为应用程序功能,并使用Application部分中的默认策略来控制Peer组织如何与通道进行交互。应用程序通道还使用Channel部分中的默认策略,并启用V2_0作为通道功能级别。

通道策略

通道是组织之间进行通信的一种私有方法。因此,通道配置的大多数更改都需要该通道的其他成员同意。如果一个组织可以在未经其他组织批准的情况下加入该通道并读取账上的数据,则通道将没有作用。通道组织结构的任何更改都必须由一组能够满足通道策略的组织批准。

策略还控制用户如何与通道交互的过程,例如在将链码部署到通道之前需要一些组织批准或着需要由通道管理员完成一些操作。

通道策略非常重要,因此需要在单独的主题中进行讨论。与通道配置的其他部分不同,控制通道的策略由configtx.yaml文件的不同部分组合起来才能确定。尽管可以在几乎没有任何约束的情况下为任何场景配置通道策略,但本主题将重点介绍如何使用Hyperledger Fabric提供的默认策略。如果您使用Fabric测试网络或Fabric示例配置使用的默认策略,则您创建的每个通道都会使用签名策略,ImplicitMeta策略和访问控制列表的组合来确定组织如何与通道进行交互并同意更新通道结构。您可以通过访问主题:策略概念了解有关Hyperledger Fabric中策略角色的更多信息。

签名策略

默认情况下,每个通道成员都定义了一组引用其组织的签名策略。当提案提交给Peer交易提交给Orderer节点时,节点将读取附加到交易上的签名,并根据通道配置中定义的签名策略对它们进行评估。每个签名策略都有一个规则,该规则指定了一组签名可以满足该策略的组织和身份。您可以在下面configtx.yaml中的Organizations部分中看到由Org1定义的签名策略:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
- &Org1

  ...

  Policies:
      Readers:
          Type: Signature
          Rule: "OR('Org1MSP.admin', 'Org1MSP.peer', 'Org1MSP.client')"
      Writers:
          Type: Signature
          Rule: "OR('Org1MSP.admin', 'Org1MSP.client')"
      Admins:
          Type: Signature
          Rule: "OR('Org1MSP.admin')"
      Endorsement:
          Type: Signature
          Rule: "OR('Org1MSP.peer')"

上面的所有策略都可以通过Org1的签名来满足。但是,每个策略列出了组织内部能够满足该策略的一组不同的角色。Admins策略只能由具有管理员角色的身份提交的交易满足,而只有具有peer的身份才能满足Endorsement策略。附加到单笔交易上的一组签名可以满足多个签名策略。例如,如果交易附加的背书由Org1和Org2共同提供,则此签名集将满足Org1和Org2的Endorsement策略。

ImplicitMeta策略

如果您的通道使用默认策略,则每个组织的签名策略将由通道配置中更高层级的ImplicitMeta策略评估。ImplicitMeta策略不是直接评估提交给通道的签名,而是使用规则在通道配置中指定可以满足该策略的一组其他策略。 如果交易可以满足该策略引用的下层签名策略集合,则它可以满足ImplicitMeta策略。

您可以在下面的configtx.yaml文件的Application部分中看到定义的ImplicitMeta策略:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Policies:
    Readers:
        Type: ImplicitMeta
        Rule: "ANY Readers"
    Writers:
        Type: ImplicitMeta
        Rule: "ANY Writers"
    Admins:
        Type: ImplicitMeta
        Rule: "MAJORITY Admins"
    LifecycleEndorsement:
        Type: ImplicitMeta
        Rule: "MAJORITY Endorsement"
    Endorsement:
        Type: ImplicitMeta
        Rule: "MAJORITY Endorsement"

Application部分中的ImplicitMeta策略控制Peer组织如何与通道进行交互。每个策略都引用与每个通道成员关联的签名策略。您会在下面看到Application部分的策略与Organization部分的策略之间的关系:

Application策略

图1:Admins ImplicitMeta策略可以由每个组织定义的大多数Admins签名策略满足。

每个策略均在通道配置中引用其路径。由于Application部分中的策略位于通道组内部的应用程序组中,因此它们被称为Channel/Application策略。由于Fabric文档中的大多数位置都是通过策略路径来引用策略的,因此在本教程的其余部分中,我们将通过策略路径来引用策略。

每个ImplicitMeta中的Rule均引用可以满足该策略的签名策略的名称。 例如,Channel/Application/Admins ImplicitMeta策略引用每个组织的Admins签名策略。 每个Rule还包含满足ImplicitMeta策略所需的签名策略的数量。例如,Channel/Application/Admins策略要求满足大多数Admins签名策略。

Application管理员

图2:提交给该通道的通道更新请求包含来自Org1,Org2和Org3的签名,满足每个组织的签名策略。因此,该请求满足Channel/Application/Admins策略。Org3检查呈浅绿色,因为不需要签名个数达到大多数。

再举一个例子,大多数组织的Endorsement策略都可以满足Channel/Application/Endorsement策略,这需要每个组织的Peer签名。Fabric链码生命周期将此策略用作默认链码背书策略。除非您提交链码定义时使用不同的背书策略,否则调用链码的交易必须得到大多数通道成员的认可。

通道背书策略

图3:来自客户端应用程序的交易调用了Org1和Org2的Peer上的链码。链码调用成功,并且该应用程序收到了两个组织的Peer背书。由于此交易满足Channel/Application/Endorsement策略,因此该交易符合默认的背书策略,可以添加到通道的账本中。

同时使用ImplicitMeta策略和签名策略的优点是,您可以在通道级别设置治理规则,同时允许每个通道成员选择对其组织进行签名所需的身份。例如,通道可以指定要求大多数组织管理员给通道配置更新签名。但是,每个组织可以使用其签名策略来选择其组织中的哪些身份是管理员,甚至要求其组织中的多个身份签名才能批准通道更新。

ImplicitMeta策略的另一个优点是,在从通道中添加或删除组织时,不需要更新它们。以图 3为例,如果将两个新组织添加到通道,则Channel/Application/Endorsement将需要三个组织的背书才能验证交易。

ImplicitMeta策略的一个缺点是它们不会显式读取通道成员使用的签名策略(这就是为什么它们被称为隐式策略)的原因。相反,他们假定用户具有基于通道配置的必需签名策略。Channel/Application/Endorsement策略的rule基于通道中Peer组织的数量。如果图 3中的三个组织中有两个不具备Endorsement签名策略,则任何交易都无法获得满足Channel/Application/Endorsement ImplicitMeta策略所需的大多数背书。

通道修改策略

通道结构由通道配置内的修改策略控制。通道配置的每个组件都有一个修改策略,需要满足修改策略才能被通道成员更新。例如,每个组织定义的策略和通道MSP,包含通道成员的应用程序组以及定义通道共识者集合的配置组件均具有不同的修改策略。

每个修改策略都可以引用ImplicitMeta策略或签名策略。例如,如果您使用默认策略,则定义每个组织的值将引用与该组织关联的Admins签名策略。因此,组织可以更新其通道MSP或设置锚节点,而无需其他通道成员的批准。定义通道成员集合的应用程序组的修改策略是Channel/Application/Admins ImplicitMeta策略。因此,默认策略是大多数组织需要批准添加或删除通道成员。

通道策略和访问控制列表

通道配置中的策略也由访问控制列表(ACLs)引用,该访问控制列表用于限制对通道使用的Fabric资源的访问。ACL扩展了通道配置内的策略,以管理通道的进程。您可以在示例 configtx.yaml文件中看到默认的ACL。每个ACL都使用路径引用通道策略。例如,以下ACL限制了谁可以基于/Channel/Application/Writers策略调用链码:

1
2
# ACL policy for invoking chaincodes on peer
peer/Propose: /Channel/Application/Writers

大多数默认ACL指向通道配置的Application部分中的ImplicitMeta策略。为了扩展上面的示例,如果组织可以满足/Channel/Application/Writers策略,则可以调用链码。

通道writer策略

图 4:/Channel/Application/Writers策略满足 peer/Propose ACL。可以使用任何writers签名策略的客户应用程序从任何组织提交的交易来满足此策略。

Orderer策略

configtx.yamlOrderer部分中的ImplicitMeta策略以与Application部分管理Peer组织类似的方式来管理通道的Orderer节点。 ImplicitMeta策略指向与排序服务管理员的组织相关联的签名策略。

Orderer策略

图 5:Channel/Orderer/Admins策略指向与排序服务的管理员相关联的Admins签名策略。

如果使用默认策略,则需要大多数Orderer组织批准添加或删除Orderer节点。

Orderer策略

图 6:提交的从通道中删除Orderer节点的请求包含来自网络中三个Orderer组织的签名,符合Channel/Orderer/Admins策略。 Org3检查为浅绿色,因为不需要签名个数达到大多数。

Peer使用Channel/Orderer/BlockValidation策略来确认添加到通道的新块是由作为通道共识者集合一部分的Orderer节点生成的,并且该块未被篡改或被另一个Peer组织创建。默认情况下,任何具有Writers签名策略的Orderer组织都可以创建和验证通道的块。

向通道添加组织

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

虽然我们在这里将只关注将新组织添加到通道中,但执行其他通道配置更新(如更新修改策略调整块大小)也可以采取类似的方式。要了解更多的通道配置更新的相关过程,请查看更新通道配置 。值得注意的是,像本文演示的这些通道配置更新通常是组织管理者(而非链码或者应用开发者)的职责。

环境构建

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

1
cd fabric-samples/test-network

首先,使用 network.sh 脚本清理环境。这个命令会清除所有活动状态或终止状态的容器,并且移除之前生成的构件。关闭Fabric网络并非执行通道配置升级的 必要 步骤。但是为了本教程,我们希望从一个已知的初始状态开始,因此让我们运行以下命令来清理之前的环境:

1
./network.sh down

现在可以执行脚本,运行带有一个命名为 mychannel 的通道的测试网络:

1
./network.sh up createChannel

如果上面的脚本成功执行,你能看到日志中打印出如下信息:

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

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

使用脚本将Org3加入通道

你应该在 test-network 目录下,简单地执行以下命令来使用脚本:

1
2
cd addOrg3
./addOrg3.sh up

此处的输出值得一读。你可以看到添加了Org3的加密材料,创建了Org3的组织定义,创建了配置更新和签名,然后提交到通道中。

如果一切顺利,你会看到以下信息:

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

现在我们已经确认了我们可以将Org3添加到通道中,我们可以执行以下步骤来更新通道配置,以了解脚本幕后完成的工作。

手动将Org3加入通道

如果你刚执行了 addOrg3.sh 脚本,你需要先将网络关掉。下面的命令将关掉所有的组件,并移出所有组织的加密材料:

1
./addOrg3.sh down

网络关闭后,将其再次启动:

1
2
cd ..
./network.sh up createChannel

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

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

生成Org3加密材料

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

1
cd addOrg3

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

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

test-network/organizations/peerOrganizations 目录中,你能在Org1和Org2证书和秘钥旁边找到已生成的Org3加密材料。

一旦我们生成了Org3的加密材料,我们就能使用 configtxgen 工具打印出Org3的组织定义。我们将在执行命令前告诉这个工具在当前目录去获取 configtx.yaml 文件。

1
2
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 的策略定义,还有三个 base 64 格式的重要的证书:

  • 一个CA 根证书, 用于建立组织的根信任
  • 一个TLS根证书, 用于在gossip协议中识别Org3的块传播和服务发现
  • 管理员用户证书 (以后作为Org3的管理员会用到它)

我们将通过把这个组织定义附加到通道配置中来实现将Org3添加到通道中。

启动Org3组件

在创建了Org3证书材料之后,现在可以启动Org3 peer节点。在addOrg3目录中执行以下命令:

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

如果命令成功执行,你将看到Org3 peer节点的创建和一个命名为Org3CLI的Fabric tools容器:

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

这个Docker Compose文件已经被配置为桥接我们的处所网络,所以Org3的peer节点和Org3CLI可以被测试网络中的peer节点和ordering节点解析。我们将使用Org3CLI容器和网络通信,并执行把Org3添加到到通道中的peer命令。

(EN版本)这个Docker Compose文件已经被配置为桥接我们的处所网络,所以Org3的peer节点和可以被测试网络中的peer节点和ordering节点解析。

EN版本NOTE:

/addOrg3.sh-up命令使用fabric工具CLI容器执行下面演示的通道配置更新过程。这是为了避免首次用户对jq的依赖性要求。但是,建议直接在本地计算机上执行以下过程,而不要使用不必要的CLI容器

准备CLI环境

这部分en版本没有,因为上面的脚本没有启动Org3cli容器

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

使用以下命令进入Org3CLI容器:

1
docker exec -it Org3cli bash

这个容器已经被挂载在 organizations 文件夹中,让我们能够访问所有组织和Orderer Org的加密材料和TLS证书。我们可以使用环境变量来操作Org3CLI容器,以切换Org1、Org2或Org3的管理员角色。首先,我们需要为orderer TLS证书和通道名称设置环境变量:

1
2
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

检查下以确保变量已经被正确设置:

1
echo $ORDERER_CA && echo $CHANNEL_NAME

如果出于任何原因需要重启Org3CLI容器,你还需要重新设置两个环境变量 – ORDERER_CA and CHANNEL_NAME .

获取配置

现在我们有了一个设置了 ORDERER_CACHANNEL_NAME 环境变量的 CLI容器。让我们获取通道 – mychannel 的最新的配置区块。

(EN版本)让我们获取通道mychannel 的最新的配置区块

我们必须拉取最新版本配置的原因是通道配置元素是版本化的。版本管理由于一些原因显得很重要。它可以防止通道配置更新被重复或者重放攻击(例如,回退到带有旧的 CRLs的通道配置将会产生安全风险)。同时它保证了并行性(例如,如果你想从你的通道中添加新的组织后,再删除一个组织,版本管理可以帮助你移除想移除的那个组织,并防止移除两个组织)。

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

1
2
3
4
5
6
7
8
9
# you can issue all of these commands at once

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

我们现在执行命令获取最新的配置块:

1
2
3
4
5
peer channel fetch config config_block.pb -o orderer.example.com:7050 -c $CHANNEL_NAME --tls --cafile $ORDERER_CA
# EN版本 
peer channel fetch config channel-artifacts/config_block.pb -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com -c mychannel --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem"
# 综合上面两个 更改下路径 用下面这个
peer channel fetch config config_block.pb -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com -c mychannel --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem"

这个命令将通道配置区块以二进制protobuf形式保存在 config_block.pb 。注意文件的名字和扩展名可以任意指定。但是,推荐遵循标识要表示的对象类型及其编码(protobuf或JSON)的约定。

当你执行 peer channel fetch 命令后,下面的输出将出现在你的日志中:

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

这是告诉我们最新的 mychannel 的配置区块实际上是区块 2,并非创始区块。 peer channel fetch config 命令默认返回目标通道最新的配置区块,在这个例子里是第三个区块。这是因为测试网络脚本 network.sh 分别在两个通道更新交易中为两个组织 Org1Org2 定义了锚节点。最终,我们有如下的配置块序列:

  • block 0: genesis block
  • block 1: Org1 anchor peer update
  • block 2: Org2 anchor peer update

将配置转换到JSON格式并裁剪

现在我们用 configtxlator 工具将这个通道配置解码为JSON格式(以便被友好地阅读和修改)。我们也必须裁剪所有的头部、元数据、创建者签名等以及其他和我们将要做的修改无关的内容。我们通过 jq 这个工具来完成裁剪:

1
2
3
4
configtxlator proto_decode --input config_block.pb --type common.Block | jq .data.data[0].payload.data.config > config.json
# EN版本 用下面这个
configtxlator proto_decode --input config_block.pb --type common.Block --output config_block.json
jq .data.data[0].payload.data.config config_block.json > config.json

这个命令使我们得到一个裁剪后的JSON对象 – config.json ,这个文件将作为我们配置更新的基准。

花一些时间用你的文本编辑器(或者你的浏览器)打开这个文件。即使你已经完成了这个教程, 也值得研究下它,因为它揭示了底层配置结构,和能做的其它类型的通道更新升级。我们将在更新通道配置更详细地讨论。

添加Org3加密材料

目前到这里你做的步骤和其他任何类型的通道配置升级所需步骤几乎是一致的。我们之所以选择在教程中添加一个组织,是因为这是能做的通道配置升级里最复杂的一个

我们将再次使用 jq 工具去追加 Org3 的配置定义 – org3.json –到通道的应用组字段,同时定义输出文件是 – modified_config.json

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

现在,我们有两个重要的 JSON 文件 – config.jsonmodified_config.json初始的文件包含 Org1 和 Org2 的材料,而**”modified”文件包含了总共3个组织**。现在只需要将这 2 个 JSON文件重新编码并计算出差异部分

首先,将 config.json 文件倒回到 protobuf 格式,命名为 config.pb

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

下一步,将 modified_config.json 编码成 modified_config.pb :

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

现在使用 configtxlator计算两个protobuf配置的差异。这条命令会输出一个新的 protobuf 二进制文件,命名为 org3_update.pb :

1
configtxlator compute_update --channel_id mychannel --original config.pb --updated modified_config.pb --output org3_update.pb

这个新的 proto 文件 – org3_update.pb – 包含了 Org3 的定义和指向Org1 和 Org2 材料的更高级别的指针。我们可以抛弃 Org1和Org2相关的MSP材料和修改策略信息,因为这些数据已经存在于通道的初始区块。因此,我们只需要两个配置的差异部分。

在我们提交通道更新前,我们执行最后做几个步骤。首先,我们将这个对象解码成可编辑的JSON 格式,并命名为 org3_update.json :

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

现在,我们有了一个解码后的更新文件 – org3_update.json –我们需要用信封消息来包装它。这个步骤要把之前裁剪掉的头部信息还原回来。我们将这个文件命名为 org3_update_in_envelope.json

1
echo '{"payload":{"header":{"channel_header":{"channel_id":"'mychannel'", "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

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

签名并提交配置更新

差不多大功告成了!

我们现在有一个 protobuf二进制文件 – org3_update_in_envelope.pb。但是,在配置写入到账本前,我们需要来自必要的Admin用户的签名。我们通道应用组的修改策略(mod_policy)设置为默认值”MAJORITY”,这意味着我们需要大多数已经存在的组织管理员去签名这个更新。因为我们只有两个组织 – Org1 和 Org2 – 所以两个的大多数也还是两个,我们需要它们都签名。没有这两个签名,排序服务会因为不满足策略而拒绝这个交易。

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

1
peer channel signconfigtx -f org3_update_in_envelope.pb

最后一步,我们将容器的身份切换为 Org2管理员用户。我们通过导出和Org2 MSP相关的4个环境变量实现这步。

切换不同的组织身份为配置交易签名(或者其他事情)不能反映真实世界里Fabric 的操作。一个单一容器不可能挂载了整个网络的加密材料。相反地,配置更新需要在网络外安全地递交给Org2管理员来审查和批准。

导出 Org2 的环境变量:

1
2
3
4
5
6
7
# you can issue all of these commands at once

export CORE_PEER_TLS_ENABLED=true
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

最后,我们执行 peer channel update 命令。Org2管理员在这个命令中会附带签名,因此就没有必要对 protobuf 进行两次签名:

将要做的对排序服务的更新调用,会经历一系列的系统级签名和策略检查。你会发现通过检视排序节点的日志流会非常有用。在另外一个终端执行 docker logs -f orderer.example.com 命令就能展示它们了。

发起更新调用:

1
peer channel update -f org3_update_in_envelope.pb -c mychannel -o orderer.example.com:7050 --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem"

如果你的更新提交成功,将会看到一个类似如下的信息:

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

成功的通道更新调用会返回一个新的区块 – 区块3 – 给所有在这个通道上的peer节点。你是否还记得,区块 0-2是初始的通道配置,区块3就是带有Org3定义的最新的通道配置

你可以通过用以下命令来检查查看 peer0.org1.example.com 的日志:

1
docker logs -f peer0.org1.example.com

将Org3加入通道

此时,通道的配置已经更新并包含了我们新的组织 – Org3 – 意味者这个组织下的节点可以加入到 mychannel

导出以下的环境变量用来以Org3Admin的身份来进行操作:

1
2
3
4
5
6
7
# you can issue all of these commands at once

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

现在,让我们向排序服务发送一个调用,请求 mychannel 的创世块。由于成功地更新了通道,排序服务将验证Org3可以拉取创世块并加入该通道。如果没有成功地将Org3附加到通道配置中,排序服务将拒绝此请求。

使用 peer channel fetch 命令来获取这个区块:

1
peer channel fetch 0 mychannel.block -o orderer.example.com:7050 -c mychannel --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem"

注意,我们传递了 0索引我们在这个通道账本上想要的区块(例如,创世块)。如果我们简单地执行 peer channel fetch config 命令,我们将会收到区块 3 – 那个带有Org3定义的更新后的配置。然而,我们的账本不能从一个下游的区块开始 – 我们必须从区块 0 开始。

如果成功,该命令将创世块返回到名为 mychannel.block 的文件。我们现在可以使用这个块加入peer到通道。执行 peer channel join 命令并传入创世块,以将Org3的peer节点加入到通道中:

1
peer channel join -b mychannel.block

配置领导节点选举

引入这个章节作为通用参考,是为了理解在完成网络通道配置初始化之后,增加组织时,领导节点选举的设置

新加入的节点是根据初始区块启动的,初始区块是不包含通道配置更新中新加入的组织信息的。因此新的节点无法利用gossip协议,因为它们无法验证从自己组织里其他节点发送过来的区块,除非它们接收到将组织加入到通道的那个配置交易。新加入的节点必须有以下配置之一才能从排序服务接收区块

  1. 采用静态领导者模式,将peer节点配置为组织的领导者。
1
2
CORE_PEER_GOSSIP_USELEADERELECTION=false
CORE_PEER_GOSSIP_ORGLEADER=true

这个配置在Fabric V2.2是默认启动并且对于新加入到通道中的所有节点必须一致。

  1. 采用动态领导者选举,配置节点采用领导选举的方式:
1
2
CORE_PEER_GOSSIP_USELEADERELECTION=true
CORE_PEER_GOSSIP_ORGLEADER=false

因为新加入组织的节点,无法生成成员关系视图,这个选项和静态配置类似,每个节点启动时宣称自己是领导者。但是,一旦它们更新到了将组织加入到通道的配置交易,组织中将只会有一个激活状态的领导者。因此,如果你想最终组织的节点采用领导选举,建议你采用这个配置。

安装、定义和调用链码

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

在我们以Org3来安装链码之前,我们可以使用 ./network.sh 脚本在通道上部署Fabcar链码。打开一个新的终端,并进入 test-network 目录。然后你可以使用 test-network 脚本来部署 Fabcar 链码:

1
2
cd fabric-samples/test-network
./network.sh deployCC -ccn fabcar -ccp ../chaincode/fabcar/go -ccl go -c mychannel

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

在部署了链码之后,我们可以使用以下步骤在Org3中调用Fabcar链代码。这些步骤在 test-network 目录中完成。在你的终端中复制和粘贴以下环境变量,以便以Org3管理员的身份与网络交互:

1
2
3
4
5
6
7
export PATH=${PWD}/../bin:$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链码

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

这个命令会创建一个链码包,命名为 fabcar.tar.gz,用它来在我们的Org3的peer节点上安装链码。如果通道中运行的是java或者Node.js语言写的链码,需要根据实际情况修改这个命令。输入下面的命令在peer0.org3.example.com上安装链码

1
peer lifecycle chaincode install fabcar.tar.gz

下一步是以Org3的身份批准链码Fabcar定义。Org3需要批准与Org1和Org2同样的链码定义,然后提交到通道中。为了调用链码,Org3需要在链码定义中包含包标识符。你可以在你的peer中查到包标识:

1
peer lifecycle chaincode queryinstalled

你应该会看到类似下面的输出:

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

我们后面的命令中会需要这个包标识。所以让我们继续把它保存到环境变量。把 peer lifecycle chaincode queryinstalled 返回的包标识粘贴到下面的命令中。这个包标识每个用户可能都不一样,所以需要使用从你控制台返回的包标识完成下一步。

1
export CC_PACKAGE_ID=fabcar_1:25f28c212da84a8eca44d14cf12549d8f7b674a0d8288245561246fa90f7ab03

使用下面的命令来为Org3批准链码Fabcar定义:

1
2
3
4
# 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.0 --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 命令来检查你批准的链码定义是否已经提交到通道中。

1
2
# 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

命令执行成功后会返回关于被提交的链码定义的信息:

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

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

在将链代码定义提交到通道后,链代码将在加入安装链代码的通道的peer节点上启动。Fabcar 链代码现在已准备好供客户端应用程序调用。使用以下命令在分布式账本上创建一组初始汽车。请注意,invoke 命令需要以足够数量的peer节点为目标,以满足链代码背书策略。

1
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:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"initLedger","Args":[]}'

如果命令成功,您应该能够收到类似于以下内容的响应:

1
2020-02-12 18:22:20.576 EST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200

你可以查询链码,以确保它已经在Org3的peer上启动。注意,你可能需要等待链码容器启动。

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

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

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

1
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”已结在我们的账本中了:

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

总结

通道配置更新过程确实非常复杂,但是各个步骤都有一个逻辑方法。最后就是为了形成一个用protobuf二进制表示的差异化的交易对象,然后获取必要数量的管理员签名使通道配置更新交易满足通道的修改策略

configtxlatorjq 工具,和不断使用的 peer channel 命令,为我们提供了完成这个任务的基本功能。

更新通道配置包括Org3的锚节点(可选)

因为Org1和Org2在通道配置中已经定义了锚节点,所以Org3的节点可以与Org1和Org2的节点通过gossip协议进行连接。同样,像Org3这样新添加的组织也应该在通道配置中定义它们的锚节点,以便来自其他组织的任何新节点可以直接发现Org3节点。在本节中,我们将对通道配置进行更新,以定义Org3锚节点。这个过程将类似于之前的配置更新,因此这次我们会更快。

和以前一样,我们开始会获取最新的通道配置。获取通道中最近的配置区块使用 peer channel fetch 命令。

1
peer channel fetch config config_block.pb -o orderer.example.com:7050 -c mychannel --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem"

在获取到配置区块后,我们将要把它转换成JSON格式。为此我们会使用configtxlator工具,正如前面在通道中加入Org3一样。当转换时,我们需要删除所有更新Org3不需要的头部、元数据和签名,使用jq工具添加包含一个锚节点的Org3更新。这些信息会在更新通道配置前重新合并。

1
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配置中

1
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

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

modified_anchor_config.json 翻译回protobuf格式 modified_anchor_config.pb

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

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

1
configtxlator compute_update --channel_id mychannel --original config.pb --updated modified_anchor_config.pb --output anchor_update.pb

现在我们已经有了期望的通道更新,下面必须把它包在一个信封消息里以便正确读取。要做到这一点,我们先把protobuf格式转换回JSON格式才能被包装

我们再此使用configtxlator命令,把 anchor_update.pb 转换成 anchor_update.json

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

接下来我们来把更新包在信封消息里恢复先前去掉的头,输出到 anchor_update_in_envelope.json 中。

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

现在我们已经重新合并了信封,我们需要把它装换成protobuf格式以便正确签名并提交到orderer进行更新。

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

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

1
2
3
4
5
6
# you can issue all of these commands at once

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

在将更新提交给order之前,现在我们以Org3 admin身份使用 peer channel update 命令进行签名

1
peer channel update -f anchor_update_in_envelope.pb -c mychannel -o orderer.example.com:7050 --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem"

orderer接收到配置更新请求,用这个配置更新生成区块。当节点接收到区块后,他们就会处理配置更新了。

检查其中一个peer节点的日志。当处理新区块带来的配置更新时,你会看到gossip使用新的锚节点与Org3重新建立连接。这就证明了配置更新已经成功应用。

1
2
3
4
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中定义锚节点。

更新通道配置

什么是通道配置

像许多复杂的系统一样,Hyperledger Fabric 网络由一些结构及其相关的过程组成。

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

识别区块链网络的结构和管理结构如何相互作用的过程的信息位于通道配置中。这些配置由通道成员共同决定,并包含在提交给通道账本的区块中。通道配置可以使用 configtxgen 工具来构建,该工具使用 configtx.yaml 文件作为输入。你可以在此处查看 configtx.yaml 的样例文件

由于配置位于区块中(第一个区块被称为创世区块,它包含通道的最新配置),因此更新通道配置的过程(比如,通过添加成员来更改结构或者修改通道配置)被称为配置更新交易

在生产网络中,通道的初始化配置由该通道的初始成员在外部决定,同样的,配置更新交易也通常是在外部讨论后由单个通道管理员提出。

在本主题中,我们将:

  • 展示一个应用程序通道的完整配置示例。
  • 讨论很多可以编辑的通道参数。
  • 展示更新通道配置的过程,该过程包括提取、转换和确定配置范围的命令,这些命令会将通道配置转换成易于人们阅读的格式。
  • 讨论可以用于修改通道配置的方法。
  • 展示重新格式化配置的过程,并获取使配置生效所必需的签名。

可以更新的通道参数

通道是高度可配置的,但也有限制。某些与通道相关的内容(例如通道的名称)被确定后就无法再进行修改。而且修改我们在本主题中讨论的某个参数时需要满足通道配置中指定的相关策略。

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

应用通道参数信息

在本节中,我们将深入研究可配置的值在配置上下文中的位置。

首先,在配置的多个部分中都有如下配置参数:

  • 策略。策略不仅是一个配置值(它可以按照 mod_policy 中的定义进行升级),它们还定义了所有参数在发生变更时所需的环境。更多相关信息,请查看 Policies
  • Capabilities。确保网络和通道以相同的方式处理事情,为诸如通道配置更新和链码调用之类的事情创建确定性的结果。如果没有确定性的结果,当通道上的某个peer节点使交易生效时,通道上的另一个peer节点可能会使交易无效。更多相关信息,请查看 Capabilities

Channel/Application

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

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

Channel/Orderer

管理排序服务或排序系统通道特有的配置参数,这需要大多数排序组织的管理员同意(默认情况下只有一个排序组织,但是也可以有多个,例如当多个组织向排序服务提供节点时就会存在多个排序组织)。

  • Batch size。 这些参数决定了一个区块中交易的数量和大小。没有哪个区块会大于 absolute_max_bytes,也没有哪个区块包含的交易数会比 max_message_count 多。如果可以构造大小不超过 preferred_max_bytes 的区块,那么区块将被提早分割,而大于此大小的交易将出现在它们自己的区块中。(译者注:现根据 Fabric release-2.0源代码对 preferred_max_bytes 做进一步阐述。打包区块时为防止区块过大,有一个 BlockCut 的过程,cut 的规则是:如果当前 Envelope message 的大小大于 preferred_max_bytes,则将之前接收到的message打包为一个区块,同时将当前 message 单独打包为另一个区块;如果当前message小于等于 preferred_max_bytes 且该message加上之前接收到的 message 大于 preferred_max_bytes,则将当前 message 与之前接收到的 message 打包为一个区块,否则继续接收 message。)

  • Batch timeout。 是指在第一个交易到达后至分割区块前的等待时间。降低这个值会改善延迟,但降低太多则会由于区块未达到最大容量而降低吞吐量。(译者注:假设区块大小为1M。如果超时时间太短,则生成的区块会远小于1M,此时出块较快、交易延迟较低,但这也会导致区块数量激增。处理大量的区块会消耗非常多的时间从而降低系统吞吐量。)

  • Block validation。 该策略指定了一个区块被视为有效区块需要满足什么样的签名要求。默认情况下,它需要排序组织中某个成员的签名。

  • Consensus type。为了能将基于 Kafka 的排序服务切换为基于Raft的排序服务,可以更改通道的共识类型。更多相关信息,请查看从Kafka迁移到Raft

  • **Raft 排序服务参数。**想了解Raft排序服务特有的参数,请查看Raft配置

  • kafka brokers(如果可用)。当设置 ConsensusType 设置为 kafka 时,brokers 列表将遍历 Kafka brokers 的某些子集(最好是全部),以供排序节点在启动时进行初始连接。

Channel

管理那些peer组织和排序服务组织都需要同意才能修改的配置参数,需要大多数应用程序组织管理员和排序组织管理员的同意。

  • Orderer addresses客户端可以用来调用排序节点BroadcastDeliver 功能的地址列表。peer 节点会在这些地址中随机选择,并在它们之间进行故障转移来获取区块。
  • 哈希结构。区块数据的哈希值用 Merkle Tree 进行计算。这个值定义了 Merkle Tree 的宽度。目前,这个值固定为 4294967295,它是对区块数据串联后进行简单的 flat hash 而得到的结果。
  • 哈希算法。 此算法用于计算编码到区块中的 hash 值。这会影响数据哈希和本区块的前一区块的哈希字段。请注意,这个字段当前仅有一个有效的值(SHA256),并且不应该被修改。
系统通道配置参数

某些配置值是排序系统通道中特有的

  • Channel creation policy。 此参数用于定义策略值,该策略值将被设置为在联盟中定义的新通道的 Application group 的 mod_policy。系统将根据策略在新通道中的实例化值对附加到通道创建请求中的签名集进行检查,以确保通道创建是经过授权的。请注意,这个配置值只能在排序系统通道中设置。
  • Channel restrictions。 此参数仅在排序系统通道中可编辑。排序节点可以分配的通道总数被定义为 max_count。这在使用弱联盟 ChannelCreation 策略的预生产环境中非常有用。

编辑配置

更新通道配置共有三步操作,从概念上来讲比较简单:

  1. 获取最新的通道配置
  2. 创建修改后的通道配置
  3. 创建配置更新交易

但是你将会发现,在这种概念上的简单之外,还伴随着一个有点复杂的过程。因此,有些用户可能选择将提取、转换和确定配置更新范围的过程脚本化。用户还可以选择如何修改通道配置:可以手动修改,也可以使用 jq 之类的工具进行修改。

我们提供了两个教程,这些教程专门用于编辑通道配置以实现特定的目标:

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

为配置更新设置环境变量

在尝试使用示例命令前,请确保设置以下环境变量,这些环境变量取决于你部署的方式。请注意,必须为每个要更新的通道设置通道名称 CH_NAME,因为通道配置更新只能应用于要更新的通道的配置(排序系统通道除外,因为默认情况下,它的配置会被拷贝到应用程序通道的配置中)。

  • CH_NAME:要更新的通道的名称。
  • TLS_ROOT_CA:提出配置更新的组织,其TLS CA的root CA证书的路径。
  • CORE_PEER_LOCALMSPID:MSP的名称。
  • CORE_PEER_MSPCONFIGPATH:组织MSP的绝对路径。
  • ORDERER_CONTAINER:排序节点容器的名称。请注意,在定位排序服务时,你可以定位排序服务中任意一个活动的节点,你的请求将被自动转发给排序服务的 leader 节点。

请注意:本主题将为各种要被提取、修改的 JSON 文件和 protobuf 文件提供默认名称(config_block.pbconfig_block.json等)。你可以使用任何名字。但需要注意的是,除非你在每次配置更新结束时回过头来清除这些文件,否则在进行其他更新时必须用其他文件名来命名文件。(译者注:请每次更新时尽量使用不同的名字,最好在每次更新结束后备份并删除本次更新产生的 JSON 文件和 protobuf 文件,避免下次更新时产生混淆。可以考虑为文件添加具有明确含义的前缀,如:”add_xxx_org_”。)

步骤1:提取并转换配置

更新通道配置的第一步是获取最新的配置区块,这个过程包含三步。首先,我们要获取 protobuf 格式的通道配置并将其写入 config_block.pb 文件。

请确保以下命令是在 peer 容器中进行的。

me注:也可以不在peer容器中进行,只要将变量$...改成具体通道名和证书路径就行

现在执行:

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

接下来,我们要把上述命令得到的 protobuf 版本的通道配置转换为 JSON 版本,并将转换后的内容写入 config_block.json 文件(JSON 文件更易于人们阅读和理解):

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

最后,为了方便阅读,我们将从配置中排除所有不必要的元数据并重新写入一个新文件。你可以给该文件起任何名字,但在本例中,我们将其命名为 config.json

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

现在我们要 copy 一份 config.json 并将其命名为 modified_config.json请不要直接编辑 config.json 文件,因为我们将在后面的步骤中计算 config.jsonmodified_config.json 之间的差异。

1
cp config.json modified_config.json
步骤2:修改配置

更新通道配置的第一步是获取最新的配置区块,这个过程包含三步。首先,我们要获取 protobuf 格式的通道配置并将其写入 config_block.pb 文件。

请确保以下命令是在 peer 容器中进行的。

me注:也可以不在peer容器中进行,只要将变量$...改成具体通道名和证书路径就行

现在执行:

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

接下来,我们要把上述命令得到的 protobuf 版本的通道配置转换为 JSON 版本,并将转换后的内容写入 config_block.json 文件(JSON 文件更易于人们阅读和理解):

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

最后,为了方便阅读,我们将从配置中排除所有不必要的元数据并重新写入一个新文件。你可以给该文件起任何名字,但在本例中,我们将其命名为 config.json

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

现在我们要 copy 一份 config.json 并将其命名为 modified_config.json请不要直接编辑 config.json 文件,因为我们将在后面的步骤中计算 config.jsonmodified_config.json 之间的差异。

1
cp config.json modified_config.json
步骤3:重新编码并提交配置

无论你是手动配置的还是使用向 jq 之类的工具进行配置的,除了计算旧配置和新配置之间的差异,你还必须运行与之前提取、确定配置范围相反的过程,然后将配置更新提交给通道上的其他管理员。

首先,我们将 config.json 文件恢复回protobuf格式并将恢复后的内容写入 config.pb 文件。接下来对 modified_config.json 文件执行相同的操作,然后计算两个文件之间的差异,并将差异写入 config_update.pb 文件。

1
2
3
4
5
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

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

1
2
3
4
5
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

提交配置更新交易:

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

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

获取必要的签名

无论你要修改的是什么内容,在生成新的 protobuf 配置文件后,protobuf 配置文件需要满足相关的策略才会被认为有效,通常(并非总是)需要其他组织的签名。

请注意:你可以根据你的应用程序编写收集签名的脚本。一般来说,你收集到的签名可能会比你需要的签名多。

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

这种方式的优点是简单——当有足够的签名时,最后一个管理员可以直接提交配置交易(在 Fabric 中,peer channel update 命令默认包含当前组织的签名)。但是这个过程只适用于较小的通道,因为 “pass it along” 方式可能会很耗时。

另一种方式是将更新提交给通道中每个管理员并等待他们返回足够的签名。这些签名会被集中在一起提交。这使创建配置更新的管理员的工作更加困难(他们必须处理每个签名者返回的文件),但对于正在开发 Fabric 管理应用程序的用户来说,这是推荐的工作流程。

配置被添加到账本后,最好将其提取并转换成 JSON 格式以确认所有内容均已被正确添加。确认过程中得到的文件也可以用作最新配置的副本。

链码开发者教程

链码是什么

链码是一段程序,由 Gonode.js 、或者 Java 编写,来实现一些预定义的接口。链码运行在一个和背书节点分开的独立进程中,通过应用程序提交的交易来初始化和管理账本状态。

链码一般处理网络中的成员一致同意的商业逻辑,所以它类似于“智能合约”。链码可以在提案交易中被调用用来升级或者查询账本。赋予适当的权限,链码就可以调用其他链码访问它的状态,不管是在同一个通道还是不同的通道。注意,如果被调用链码和调用链码在不同的通道,就只能执行只读查询。就是说,调用不同通道的链码只能进行“查询”,在提交的子语句中不能参与状态的合法性检查。

在下边的章节中,我们站在应用开发者的角度来介绍链码。我们将演示一个简单的链码示例应用,并浏览 Chaincode Shim API 中每一个方法的作用。如果你是网络管理员,负责将链码部署在运行中的网络上,请查看 Deploying a smart contract to a channel 教程和 Fabric 链码生命周期 概念主题。

本教程以底层视角提供 Fabric Chaincode Shim API 的概览。你也可以使用 Fabric Contract API 提供的高级 API。关于使用 Fabric Contract API 开发智能合约的更多信息,请访问 智能合约处理 主题。

后面EN版是Fabric Contract API 提供的高级API

链码API

每一个链码程序都必须实现 Chaincode 接口,该接口的方法在接收 到交易时会被调用。你可以在下边找到不同语言 Chaincode Shim API 的参考文档:

在每种语言中,客户端提交交易提案都会调用 Invoke 方法。该方法可以让你使用链码来读写通道账本上的数据。

你还要包含 Init 方法,该方法是实例化方法。该方法是链码接口需要的,你的应用程序没有必要调用。你可以使用 Fabric 链码生命周期过程来指定在 Invoke 之前是否必须调用 Init 方法。更多信息,请参考 Fabric 链码生命周期文档中 批准链码定义 步骤的实例化参数。

链码 “shim” API 中的其他接口是 ChaincodeStubInterface

用来访问和修改账本,以及在链码间发起调用

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

简单资产链码

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

选择一个位置存放代码

如果你没有写过 Go 的程序,你可能需要确认一下你是否安装了 Go 以及你的系统是否配置正确。我们假设你用的是支持modules的版本。

现在你需要为你的链码应用程序创建一个目录。

简单起见,我们使用如下命令:

1
mkdir sacc && cd sacc

现在,我们创建一个用于编写代码的源文件:

1
2
go mod init sacc
touch sacc.go
内务

首先,我们从内务开始。每一个链码都要实现 Chaincode 接口 中的 InitInvoke 方法。所以,我们先使用 Go import 语句来导入链码必要的依赖。我们将导入链码 shim 包和 peer protobuf 包 。然后,我们加入一个 SimpleAsset 结构体来作为 Chaincode shim 方法的接收者。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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 方法。

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

}

注意,链码升级的时候也要调用这个方法。当写一个用来升级已存在的链码的时候,请确保合理更改 Init 方法。特别地,当升级时没有“迁移”或者没东西需要初始化时,可以提供一个空的 Init 方法。

接下来,我们将使用 ChaincodeStubInterface.GetStringArgs 方法获取 Init 调用的参数,并且检查其合法性。在我们的用例中,我们希望得到一个键-值对。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 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 并将键和值作为参数传递给它。假设一切正常,将返回一个 peer.Response 对象,表明初始化成功。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 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 函数的签名。

1
2
3
4
5
6
// 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 来为链码应用程序的方法解析方法名和参数。

1
2
3
4
5
6
7
8
// 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()

}

然后,我们将验证函数名是否为 set 或者 get ,并执行链码应用程序的方法,通过 shim.Successshim.Error 返回一个适当的响应,这个响应将被序列化为 gRPC protobuf 消息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 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))
}
实现链码应用程序

就像我们说的,我们的链码应用程序实现了两个功能,它们可以通过 Invoke 方法调用。我们现在来实现这些方法。注意我们之前提到的,要访问账本状态,我们需要使用链码 shim API 中的 ChaincodeStubInterface.PutStateChaincodeStubInterface.GetState 方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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.Start 方法。下边是我们链码程序的完整源码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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 提供了扩展 API ,用于从提交者的证书中提取客户端标识用于访问控制决策,该证书可以是客户端身份本身,或者组织身份,或客户端身份属性。

例如,一个以键-值对表示的资产可以将客户端的身份作为值的一部分保存其中(比如以 JSON 属性标识资产主人),以后就只有被授权的客户端才可以更新键-值对。

详细信息请查阅 client identity (CID) library documentation

To add the client identity shim extension to your chaincode as a dependency, see 管理 Go 链码的扩展依赖.

将客户端身份 shim 扩展作为依赖添加到你的链码,请查阅 管理 Go 链码的扩展依赖

管理Go链码的扩展依赖

你的 Go 链码需要 Go 标准库之外的一些依赖包(比如链码 shim)。当链码安装到 peer 的时候,这些包的源码必须被包含在你的链码包中。如果你将你的链码构造为一个模块,最简单的方法就是在打包你的链码之前使用 go mod vendor 来 “vendor” 依赖。

1
2
go mod tidy
go mod vendor

这就把你链码的扩展依赖放进了本地的 vendor 目录。

当依赖都引入到你的链码目录后, peer chaincode packagepeer chaincode install 操作将把这些依赖一起放入链码包中

链码开发者教程(EN)

本教程概述了 Fabric Contract API 提供的高级 API

Fabric Contract API

fabric-contract-api合约接口是应用程序开发人员实现智能合约的高级 API。在 Hyperledger Fabric 中,智能合约也称为链代码。使用此 API 为编写业务逻辑提供了一个高级入口点。不同语言的 Fabric Contract API 文档可以在下面的链接中找到:

请注意,在使用合约 api 时,调用的每个链码函数都会传递一个事务上下文“ctx”,您可以从中获取chaincode stub (GetStub() ),它具有访问帐本的功能(例如 GetState() )并提出更新帐本的请求(例如 PutState() )。您可以在下面特定于语言的链接中了解更多信息。

在本教程使用的 Go 链代码中,我们将使用这些API来实现一个资产转移功能链码应用来管理简单资产。

资产转移链码

我们的应用程序是一个基本的示例链代码,用于使用资产初始化账本,创建、读取、更新和删除资产,检查资产是否存在,以及将资产从一个所有者转移到另一个所有者。

内务

首先,让我们从一些内务处理开始。与每个链代码一样,它实现了fabric-contract-api 接口,所以让我们为链代码的必要依赖项添加 Go import 语句。我们将导入 fabric contract api 包并定义我们的 SmartContract。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import (
  "fmt"
  "log"
  "github.com/hyperledger/fabric-contract-api-go/contractapi"
)

// SmartContract provides functions for managing an Asset
type SmartContract struct {
   contractapi.Contract
}

接下来,让我们添加一个结构Asset来表示账本上的简单资产。请注意 JSON 注释,它将用于将资产编组为存储在帐本中的 JSON。

1
2
3
4
5
6
7
8
// Asset describes basic details of what makes up a simple asset
type Asset struct {
    ID             string `json:"ID"`
    Color          string `json:"color"`
    Size           int    `json:"size"`
    Owner          string `json:"owner"`
    AppraisedValue int    `json:"appraisedValue"`
}
初始化链码

接下来,我们将实现InitLedger函数去用一些初始数据填充账本。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// InitLedger adds a base set of assets to the ledger
   func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
      assets := []Asset{
        {ID: "asset1", Color: "blue", Size: 5, Owner: "Tomoko", AppraisedValue: 300},
        {ID: "asset2", Color: "red", Size: 5, Owner: "Brad", AppraisedValue: 400},
        {ID: "asset3", Color: "green", Size: 10, Owner: "Jin Soo", AppraisedValue: 500},
        {ID: "asset4", Color: "yellow", Size: 10, Owner: "Max", AppraisedValue: 600},
        {ID: "asset5", Color: "black", Size: 15, Owner: "Adriana", AppraisedValue: 700},
        {ID: "asset6", Color: "white", Size: 15, Owner: "Michel", AppraisedValue: 800},
      }

   for _, asset := range assets {
      assetJSON, err := json.Marshal(asset)
      if err != nil {
        return err
      }

      err = ctx.GetStub().PutState(asset.ID, assetJSON)
      if err != nil {
        return fmt.Errorf("failed to put to world state. %v", err)
      }
    }

    return nil
  }

接下来,我们编写一个函数来在分类账上创建一个尚不存在的资产。编写链代码时,最好在对账本采取操作之前检查账本上是否存在某些内容,如CreateAsset下面的函数所示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// CreateAsset issues a new asset to the world state with given details.
   func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error {
    exists, err := s.AssetExists(ctx, id)
    if err != nil {
      return err
    }
    if exists {
      return fmt.Errorf("the asset %s already exists", id)
    }

    asset := Asset{
      ID:             id,
      Color:          color,
      Size:           size,
      Owner:          owner,
      AppraisedValue: appraisedValue,
    }
    assetJSON, err := json.Marshal(asset)
    if err != nil {
      return err
    }

    return ctx.GetStub().PutState(id, assetJSON)
  }

现在我们已经用一些初始资产填充了分类帐并创建了资产,让我们编写一个ReadAsset允许我们从分类帐中读取资产的函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// ReadAsset returns the asset stored in the world state with given id.
   func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) {
    assetJSON, err := ctx.GetStub().GetState(id)
    if err != nil {
      return nil, fmt.Errorf("failed to read from world state: %v", err)
    }
    if assetJSON == nil {
      return nil, fmt.Errorf("the asset %s does not exist", id)
    }

    var asset Asset
    err = json.Unmarshal(assetJSON, &asset)
    if err != nil {
      return nil, err
    }

    return &asset, nil
  }

既然我们的账本上有资产,我们可以与之交互,让我们编写一个链代码函数UpdateAsset,允许我们更新允许我们更改的资产的属性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// UpdateAsset updates an existing asset in the world state with provided parameters.
   func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error {
      exists, err := s.AssetExists(ctx, id)
      if err != nil {
        return err
      }
      if !exists {
        return fmt.Errorf("the asset %s does not exist", id)
      }

      // overwriting original asset with new asset
      asset := Asset{
        ID:             id,
        Color:          color,
        Size:           size,
        Owner:          owner,
        AppraisedValue: appraisedValue,
      }
      assetJSON, err := json.Marshal(asset)
      if err != nil {
        return err
      }

      return ctx.GetStub().PutState(id, assetJSON)
  }

在某些情况下,我们可能需要从账本中删除资产的能力,因此让我们编写一个DeleteAsset函数来处理该需求。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// DeleteAsset deletes an given asset from the world state.
   func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error {
      exists, err := s.AssetExists(ctx, id)
      if err != nil {
        return err
      }
      if !exists {
        return fmt.Errorf("the asset %s does not exist", id)
      }

      return ctx.GetStub().DelState(id)
   }

我们之前说过,在对资产采取操作之前检查资产是否存在是个好主意,所以让我们编写一个调用函数来AssetExists实现该要求。

1
2
3
4
5
6
7
8
9
// AssetExists returns true when asset with given ID exists in world state
   func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
      assetJSON, err := ctx.GetStub().GetState(id)
      if err != nil {
        return false, fmt.Errorf("failed to read from world state: %v", err)
      }

      return assetJSON != nil, nil
    }

接下来,我们将编写一个我们将调用的函数TransferAsset,该函数可以将资产从一个所有者转移到另一个所有者。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// TransferAsset updates the owner field of asset with given id in world state.
   func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string) error {
      asset, err := s.ReadAsset(ctx, id)
      if err != nil {
        return err
      }

      asset.Owner = newOwner
      assetJSON, err := json.Marshal(asset)
      if err != nil {
        return err
      }

      return ctx.GetStub().PutState(id, assetJSON)
    }

让我们编写一个我们将调用的函数GetAllAssets,使分类账查询能够返回分类账上的所有资产。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// GetAllAssets returns all assets found in world state
   func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) {
// range query with empty string for startKey and endKey does an
// open-ended query of all assets in the chaincode namespace.
    resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
    if err != nil {
      return nil, err
    }
    defer resultsIterator.Close()

    var assets []*Asset
    for resultsIterator.HasNext() {
      queryResponse, err := resultsIterator.Next()
      if err != nil {
        return nil, err
      }

      var asset Asset
      err = json.Unmarshal(queryResponse.Value, &asset)
      if err != nil {
        return nil, err
      }
      assets = append(assets, &asset)
    }

    return assets, nil
  }

下面的完整链码示例是为了让本教程尽可能清晰明了。在现实世界的实现中,很可能会在main包导入链码包的地方对包进行分段,以便于进行单元测试。要查看它是什么样子,请参阅 fabric-samples 中的资产转移Go 链代码。如果您查看assetTransfer.go,您会看到它包含定义在 中并位于 中的导入。package main``package chaincode``smartcontract.go``fabric-samples/asset-transfer-basic/chaincode-go/chaincode/

把它们组合在一起

最后,我们需要添加main调用ContractChaincode.Start函数的函数。这是整个链代码程序源。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
package main

import (
  "encoding/json"
  "fmt"
  "log"

  "github.com/hyperledger/fabric-contract-api-go/contractapi"
)

// SmartContract provides functions for managing an Asset
   type SmartContract struct {
      contractapi.Contract
    }

// Asset describes basic details of what makes up a simple asset
   type Asset struct {
      ID             string `json:"ID"`
      Color          string `json:"color"`
      Size           int    `json:"size"`
      Owner          string `json:"owner"`
      AppraisedValue int    `json:"appraisedValue"`
    }

// InitLedger adds a base set of assets to the ledger
   func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
    assets := []Asset{
      {ID: "asset1", Color: "blue", Size: 5, Owner: "Tomoko", AppraisedValue: 300},
      {ID: "asset2", Color: "red", Size: 5, Owner: "Brad", AppraisedValue: 400},
      {ID: "asset3", Color: "green", Size: 10, Owner: "Jin Soo", AppraisedValue: 500},
      {ID: "asset4", Color: "yellow", Size: 10, Owner: "Max", AppraisedValue: 600},
      {ID: "asset5", Color: "black", Size: 15, Owner: "Adriana", AppraisedValue: 700},
      {ID: "asset6", Color: "white", Size: 15, Owner: "Michel", AppraisedValue: 800},
    }

    for _, asset := range assets {
      assetJSON, err := json.Marshal(asset)
      if err != nil {
        return err
      }

      err = ctx.GetStub().PutState(asset.ID, assetJSON)
      if err != nil {
        return fmt.Errorf("failed to put to world state. %v", err)
      }
    }

    return nil
  }

// CreateAsset issues a new asset to the world state with given details.
   func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error {
    exists, err := s.AssetExists(ctx, id)
    if err != nil {
      return err
    }
    if exists {
      return fmt.Errorf("the asset %s already exists", id)
    }

    asset := Asset{
      ID:             id,
      Color:          color,
      Size:           size,
      Owner:          owner,
      AppraisedValue: appraisedValue,
    }
    assetJSON, err := json.Marshal(asset)
    if err != nil {
      return err
    }

    return ctx.GetStub().PutState(id, assetJSON)
  }

// ReadAsset returns the asset stored in the world state with given id.
   func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) {
    assetJSON, err := ctx.GetStub().GetState(id)
    if err != nil {
      return nil, fmt.Errorf("failed to read from world state: %v", err)
    }
    if assetJSON == nil {
      return nil, fmt.Errorf("the asset %s does not exist", id)
    }

    var asset Asset
    err = json.Unmarshal(assetJSON, &asset)
    if err != nil {
      return nil, err
    }

    return &asset, nil
  }

// UpdateAsset updates an existing asset in the world state with provided parameters.
   func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error {
    exists, err := s.AssetExists(ctx, id)
    if err != nil {
      return err
    }
    if !exists {
      return fmt.Errorf("the asset %s does not exist", id)
    }

    // overwriting original asset with new asset
    asset := Asset{
      ID:             id,
      Color:          color,
      Size:           size,
      Owner:          owner,
      AppraisedValue: appraisedValue,
    }
    assetJSON, err := json.Marshal(asset)
    if err != nil {
      return err
    }

    return ctx.GetStub().PutState(id, assetJSON)
  }

  // DeleteAsset deletes an given asset from the world state.
  func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error {
    exists, err := s.AssetExists(ctx, id)
    if err != nil {
      return err
    }
    if !exists {
      return fmt.Errorf("the asset %s does not exist", id)
    }

    return ctx.GetStub().DelState(id)
  }

// AssetExists returns true when asset with given ID exists in world state
   func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
    assetJSON, err := ctx.GetStub().GetState(id)
    if err != nil {
      return false, fmt.Errorf("failed to read from world state: %v", err)
    }

    return assetJSON != nil, nil
  }

// TransferAsset updates the owner field of asset with given id in world state.
   func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string) error {
    asset, err := s.ReadAsset(ctx, id)
    if err != nil {
      return err
    }

    asset.Owner = newOwner
    assetJSON, err := json.Marshal(asset)
    if err != nil {
      return err
    }

    return ctx.GetStub().PutState(id, assetJSON)
  }

// GetAllAssets returns all assets found in world state
   func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) {
// range query with empty string for startKey and endKey does an
// open-ended query of all assets in the chaincode namespace.
    resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
    if err != nil {
      return nil, err
    }
    defer resultsIterator.Close()

    var assets []*Asset
    for resultsIterator.HasNext() {
      queryResponse, err := resultsIterator.Next()
      if err != nil {
        return nil, err
      }

      var asset Asset
      err = json.Unmarshal(queryResponse.Value, &asset)
      if err != nil {
        return nil, err
      }
      assets = append(assets, &asset)
    }

    return assets, nil
  }

  func main() {
    assetChaincode, err := contractapi.NewChaincode(&SmartContract{})
    if err != nil {
      log.Panicf("Error creating asset-transfer-basic chaincode: %v", err)
    }

    if err := assetChaincode.Start(); err != nil {
      log.Panicf("Error starting asset-transfer-basic chaincode: %v", err)
    }
  }

链码访问控制

Chaincode 可以使用客户端(提交者)证书进行访问控制决策通过ctx.GetStub().GetCreator()。此外,Fabric Contract API 提供扩展 API,从提交者的证书中提取客户端身份,可用于访问控制决策,无论是基于客户端身份本身、组织身份还是客户端身份属性。

例如,表示为键/值的资产可能包括客户端的身份作为值的一部分(例如作为指示资产所有者的 JSON 属性),并且只有该客户端可以被授权更新键/值将来。可以在链代码中使用客户端身份库扩展 API 来检索此提交者信息以做出此类访问控制决策。

Built with Hugo
Theme Stack designed by Jimmy