本文整合了 2017 年 10 月到 2018 年 4 月间一系列 Hyperledger Fabric 学习笔记,包括网络启动、配置文件、MSP 与身份治理、容器内部结构、链码示例、Cello 与 Composer 周边工具、以及文档拾遗。内容覆盖 Fabric v1.0 ~ v1.1 时代的实践,时间戳与"截至日前(2017.10.27)"这类标注一并保留,便于追溯具体版本背景。

一、概述

Hyperledger Fabric 可以说是 Hyperledger 的拳头项目。虽然同为 Apache 的顶级项目,但大部分其他 Hyperledger 项目都以 Fabric 为基础。它是顶级项目中的顶级项目,可以认为是 0 级项目。

Fabric 的核心组件构成一个联盟链网络:

  • peer:保存账本(ledger)、运行链码(chaincode),分背书节点(endorser)、提交节点(commiter)、锚节点(anchor)、leader 节点等角色
  • orderer:负责对事务排序、打包成区块。orderer 可被视作跨 channel 的
  • channel:peers 的逻辑组合。同一个 channel 的 peers 共享完全一样的账本
  • chaincode:链码,运行在专门的安全容器里,对账本进行读写
  • ledger:账本,由 chain(区块链)和 state database 组成
  • MSP(Membership Service Provider):身份与权限治理
  • CA:证书签发机构(可选)
  • client/SDK:通过 gRPC + Protocol Buffers 与网络通信

二、核心概念与架构

2.1 ledger、chain 与 state database

ledger:账本上一系列由事务驱动的状态迁移的记录。状态迁移是链码调用(调用即事务)的结果。这些记录是不可修改顺序的,因此也是抗篡改的。

每个 channel 有一个账本,但恐怕不只一个账本。理论上账本是由产生它的链码的命名空间隔离开来的,不能直接被其他链码访问到。

chain:由包含一系列 transaction 的 block 通过 hash-link(由散列值作为前驱指针的一种连接方式)组成的数据结构。

state database:记录各种 key 的 latest value。可以被认为是 chain 的 indexed view,可以随时被从链上重建出来。

所以 Fabric 自己就有双层数据结构。

2.2 读写集语义

读集和写集是同一个事务里的数据结构。实际上读集(read set)和写集(write set)是分开的,它们共同组成了事务。

1
2
3
4
5
6
7
8
9
10
11
12
13
<TxReadWriteSet>
<NsReadWriteSet name="chaincode1">
<read-set>
<read key="K1", version="1">
<read key="K2", version="1">
</read-set>
<write-set>
<write key="K1", value="V1"
<write key="K3", value="V2"
<write key="K4", isDelete="true"
</write-set>
</NsReadWriteSet>
<TxReadWriteSet>

2.3 peer / channel / orderer 关系

每个 peer 可以拥有若干个 chaincode,也可以拥有若干个 ledger,但并不是一开始就拥有的,而是逐渐被创建出来的。chaincode 一定会定义一个 asset,也就生成了 ledger。一个 peer 可以拥有 ledger 而无 chaincode,可见也并不是必然由 chaincode 生成 ledger。比如同一个组织里面多个 peer,只有一个安装了 chaincode(只有这个 peer 可以当作 endorser),其它的 peer 一样可以拿到 ledger。

为了预防有 peer 的数据不一致,有可能需要 client application 向多个 peer 进行查询。

channel 可以认为是一系列 peers 的逻辑组合,orderer 可以被认为是跨 channel 的。同一个 channel 的 peers 共享完全一样的账本。

不同的组织完全可以基于同样的账本 copy,产生不同的 application。

Fabric 有 identity,identity 有 principal。

transaction 到达 orderer 的顺序,并不一定就是 transaction 在 block 里的顺序。

现在整个共识过程分为 proposal-packaging-validation 三个阶段。第一步和第三步都是去中心化的,第二步等局部中心化只是为了模块化共识算法的实现,也为了解耦第一和第三步,让它们并行执行。实际上,endorser 和 commitor 在逻辑上还可以进一步分离,又进一步提升了并发性。

在 validation 阶段,不合格的 transaction 会被保留(在区块中?)以备审计,而不会被应用在账本上。

peer 并不都要连到 orderer 上,这就要求 gossip 协议出场了。

2.4 锚节点(anchor)

一个 peer 节点,全 channel 里所有的其他节点都可以跟它交谈。每个组织(Member)都有一个锚节点,Member 里的所有节点通过它来发现频道中的所有其他节点,通过锚节点和其他组织交换数据

特别像以太坊中的 bootnode。

2.5 leader 节点

这个节点代表组织跟 orderer 通信,拉取最新到达的节点以后,通过 gossip 协议向其他节点做同步。

2.6 Member

法律上分离的实体,拥有独立的网络根证书。

2.7 Membership Service Provider 与 Membership Services

MSP:一个抽象的插件化组件,可以切换实现。主要用意是提供 credentials 甚至 peers 给 client,让它跟网络交流。理论上每个 Member 应该有个 MSP。MSP 的本质是一系列证书、私钥和验证算法,实际上就是一堆文件夹。

Membership Services:每个 peer 和 orderer 都可能实现 MSP,也就是说 MS 其实是它们的一个服务组件。

2.8 BCCSP(Blockchain Crypto Service Provider)

和 MSP 一样,是加密服务的提供者。

2.9 transaction 的流程

  1. client 根据 endorsement policy 发送 transaction proposal 到各个 peer
  2. 各个 peer 通过合约容器试算,生成 read-write-set
  3. client 再根据读写集语义签真正的 transaction,如果不是查询的试算,会把 transaction 发给 orderer
  4. orderer 只负责收集所有的 transaction 打包成区块
  5. 区块发送给 peer,如果 read-write-set 语义还依然满足,则 transaction validate 通过,把区块添加到区块链上

三、网络启动流程

本节是截至 2017.10.27 时对官方教程和自我实验的重新梳理。

3.1 环境准备

  • docker 要有高于 17.06.2-ce 的版本。docker-compose 要有 1.14.0 及以上的版本。当然当前的高版本的 docker 已经自带了高版本的 docker-compose,这通常不用担心
  • 安装 1.9+ 的 Golang。应该预期这样的结果:
1
2
> echo $GOPATH
/Users/xxx/go
  • 如果这个结果出不来,考虑当前 Shell 的环境变量没有正确设置:
1
2
> export GOPATH=$HOME/go
> export PATH=$PATH:$GOPATH/bin
  • 要用一个很特别的 nodejs 版本。6.9 以上,却不能用 8.x。npm 也有特别的版本要求:
1
> npm install npm@3.10.10 -g
  • 要用一个很反直觉的 python 版本,python 2.7(也就是不能用 ubuntu 自带的)

3.2 下载范例与镜像

开始下载范例网络:

1
2
> git clone https://github.com/hyperledger/fabric-samples.git
cd fabric-samples

在 fabric-samples 目录下,用这个命令下载必须的镜像和二进制库:

1
> curl -sSL https://goo.gl/Q3YRTi | bash

实际上,这会在目录中替我们下载:

1
2
3
4
> cryptogen(配置密码学相关材料的二进制工具)
configtxgen(配置频道交易事务相关材料的二进制工具)
configtxlator(配置的转译器)
peer (peer 的操纵工具)

我们还可以把这些工具的路径加入 PATH 中:

1
> export PATH=<path to download location>/bin:$PATH

3.3 生成密码学材料与频道配置

在 first-network 的文件夹内,依次执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> 生成密码学文件:
> ../bin/cryptogen generate --config=./crypto-config.yaml
>
> 重映射当前路径为 fabric 相关环境变量:
> export FABRIC_CFG_PATH=$PWD
>
> 根据双组织排序器的配置,生成创世区块:
> ../bin/configtxgen -profile TwoOrgsOrdererGenesis -outputBlock ./channel-artifacts/genesis.block
>
> 指定当前频道名称(自定义频道不在各种 yml 文件里面,而在这里面):
> export CHANNEL_NAME=mychannel
>
> 根据频道名称,生成频道的事务文件:
> ../bin/configtxgen -profile TwoOrgsChannel -outputCreateChannelTx ./channel-artifacts/channel.tx -channelID $CHANNEL_NAME
>
> 生成组织一与组织二的 MSP,注意**锚平等**节点必须配置好了:
> ../bin/configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./channel-artifacts/Org1MSPanchors.tx -channelID $CHANNEL_NAME -asOrg Org1MSP
> ../bin/configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./channel-artifacts/Org2MSPanchors.tx -channelID $CHANNEL_NAME -asOrg Org2MSP

注释掉 docker-compose-cli.yaml 里的这一行:

1
> #command: /bin/bash -c './scripts/script.sh ${CHANNEL_NAME} ${DELAY}; sleep $TIMEOUT'

3.4 启动容器

1
> CHANNEL_NAME=$CHANNEL_NAME TIMEOUT=10000 docker-compose -f docker-compose-cli.yaml up -d

进入 cli 容器,只有在 cli 容器里,才能进行 channel 相关的操作:

1
> docker exec -it cli bash

3.5 容器内 channel 与 chaincode 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
> 又要重新命名频道名为环境变量:
> export CHANNEL_NAME=mychannel
>
> 生成频道区块:
> peer channel create -o orderer.example.com:7050 -c $CHANNEL_NAME -f ./channel-artifacts/channel.tx --tls $CORE_PEER_TLS_ENABLED --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
>
> 把当前的 org1 的 peer0 加入这个频道(注意此时已经有四个核心的环境变量被注入到容器内部了):
> peer channel join -b mychannel.block
>
> 把智能合约装到当前的四个 peer 里的一个(哪一个?是仅仅 peer0 吗?有待再实验):
> peer chaincode install -n mycc -v 1.0 -p github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02
>
> 初始化一个合约,初始化合约会为 peer node 生成一个专门拿来跑智能合约的安全容器(即 dev-xxx 的容器):
> peer chaincode instantiate -o orderer.example.com:7050 --tls $CORE_PEER_TLS_ENABLED --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n mycc -v 1.0 -c '{"Args":["init","a", "100", "b","200"]}' -P "OR ('Org1MSP.member','Org2MSP.member')"
>
> 测试初始合约的状态:
> peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["query","a"]}'
>
> 第一次 invoke 这个 chaincode:
> peer chaincode invoke -o orderer.example.com:7050 --tls $CORE_PEER_TLS_ENABLED --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n mycc -c '{"Args":["invoke","a","b","10"]}'
>
> 再次查询:
> peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["query","a"]}'

3.6 容器之外的运维命令

1
2
3
4
5
6
7
8
9
10
11
12
13
> 优雅关闭网络:
> docker-compose -f docker-compose-cli.yaml down
>
> 去除无用的 docker 容器:
> docker rm -f $(docker ps -aq)
>
> 在容器外查看特定容器的日志:
> sudo docker logs -f peer0
> # control + c will exit the process
> sudo docker logs -f orderer0
>
> 专门删除与 chaincode 有关的镜像
> docker rmi -f (docker images | grep peer[0-9]-peer[0-9] | awk '{print3}')

3.7 启动脚本入口

1
./byfn.sh -m up -l node

默认的 chaincode 开发语言是 Golang,但也可以切换到 node 上面。

1
./byfn.sh -m down

这里的 -m 是 mode 的意思。这个命令会消灭掉 docker 网络残余文件,包括密码学文件和链码镜像。这个大脚本会大量依赖父文件夹的 ../bin 脚本。

3.8 初始化的步骤总览

  1. cryptogen 这个工具根据配置文件生成指定拓扑的 x.509 证书材料
  2. 用 configtxgen 工具生成 orderer 创世区块,给 orderer 用
  3. 生成频道配置事务——算是元事务的一种吧,给 channel 用
  4. 生成相关组织的锚节点

跑完 cryptogen 工具生成的材料都在 crypto-config 这个文件夹下,它总会归属于 ordererOrganzations 和 peerOrganizations。这两个文件夹下的子文件夹就是由拓扑决定的几个域文件夹。每个域下必有 ca、msp、peers/orderers、tlsca 和 users 五个文件夹。每个 user、peer 和 orderer 还必然有自己的 MSP。

四、配置文件深度解读

4.1 crypto-config.yaml(cryptogen 配置)

x.509 相关的文件主要包含两个东西:证书和 signing keys。

cryptogen 使用的配置文件是 crypto-config.yaml

x.509 的根证书是 ca-cert。它把 peers 和 orderers 绑定到一个 Org 里面。在这个网络里,每个组织都有签发自己的证书的能力,可以用这个 ca 来签发其他证书给节点和 client。

签发交易用的是私钥(keystore),验证交易用的是公钥(signcerts)。

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
# Copyright IBM Corp. All Rights Reserved.
#
# SPDX-License-Identifier: Apache-2.0
#
# 注意这个文件里一点指定 MSP 的 ID 的地方都没有。
# ---------------------------------------------------------------------------
# "OrdererOrgs" - Definition of organizations managing orderer nodes
# ---------------------------------------------------------------------------
OrdererOrgs:
# ---------------------------------------------------------------------------
# Orderer
# ---------------------------------------------------------------------------
# 这里的 Name 是 MemberShip 的 ID,大致就是一个法律实体作为会员。
- Name: Orderer
Domain: ORDERER_DOMAIN
# ---------------------------------------------------------------------------
# "Specs" - See PeerOrgs below for complete description
# ---------------------------------------------------------------------------
Specs:
# orderer docker 容器的名字
- Hostname: orderer
# ---------------------------------------------------------------------------
# "PeerOrgs" - Definition of organizations managing peer nodes
# ---------------------------------------------------------------------------
PeerOrgs:
# ---------------------------------------------------------------------------
# Org1
# ---------------------------------------------------------------------------
- Name: Org1
# 组织1的完整 domain
Domain: ORG1_DOMAIN
# ---------------------------------------------------------------------------
# "Specs"
# ---------------------------------------------------------------------------
# Uncomment this section to enable the explicit definition of hosts in your
# configuration. Most users will want to use Template, below
#
# Specs is an array of Spec entries. Each Spec entry consists of two fields:
# - Hostname: (Required) The desired hostname, sans the domain.
# - CommonName: (Optional) Specifies the template or explicit override for
# the CN. By default, this is the template:
#
# "{{.Hostname}}.{{.Domain}}"
# 这个 CommonName 搞不好是拼出来的完整容器名,比如peer0.org1.com 之类的。
#
# which obtains its values from the Spec.Hostname and
# Org.Domain, respectively.
# ---------------------------------------------------------------------------
# Specs:
# - Hostname: foo # implicitly "foo.ORG1_DOMAIN"
# CommonName: foo27.org5.example.com # overrides Hostname-based FQDN set above
# - Hostname: bar
# - Hostname: baz
# ---------------------------------------------------------------------------
# "Template"
# ---------------------------------------------------------------------------
# Allows for the definition of 1 or more hosts that are created sequentially
# from a template. By default, this looks like "peer%d" from 0 to Count-1.
# You may override the number of nodes (Count), the starting index (Start)
# or the template used to construct the name (Hostname).
#
# Note: Template and Specs are not mutually exclusive. You may define both
# sections and the aggregate nodes will be created for you. Take care with
# name collisions
# ---------------------------------------------------------------------------
# 这里的容器名就不是用 Hostname 指定出来的,而是用 spec 推导出来的了。具体还是看文档,这里就是规定这个组织里有多少个 peer,peer 用什么名字。现在这个名字就是 peer0,peer1 的形式。当然都可以改。
Template:
Count: 2
# Start: 5
# Hostname: {{.Prefix}}{{.Index}} # default
# ---------------------------------------------------------------------------
# "Users"
# ---------------------------------------------------------------------------
# Count: The number of user accounts _in addition_ to Admin
# ---------------------------------------------------------------------------
# 这个users 本身就是指的非 admin 的 user 要创建出多少套 identity 证书来。
Users:
Count: 1
# ---------------------------------------------------------------------------
# Org2: See "Org1" for full specification
# ---------------------------------------------------------------------------
- Name: Org2
Domain: ORG2_DOMAIN
Template:
Count: 2
Users:
Count: 1
- Name: Org3
Domain: ORG3_DOMAIN
Template:
Count: 2
Users:
Count: 1

4.2 configtx.yaml(configtxgen 配置)

configtxgen 需要使用的配置文件是 configtx.yaml,解说文件大致如下:

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
# Copyright IBM Corp. All Rights Reserved.
#
# SPDX-License-Identifier: Apache-2.0
#

---
################################################################################
#
# Profile
#
# - Different configuration profiles may be encoded here to be specified
# as parameters to the configtxgen tool
#
################################################################################
Profiles:
# 这个 profile 是用来定义 consortium 的。远在产生频道以前,就定义 orderer 和组织之间的关系。
TwoOrgsOrdererGenesis:
Orderer:
# 按照 YAML 的语法,<<: * 是对 anchor 的引用
<<: *OrdererDefaults
Organizations:
- *OrdererOrg
Consortiums:
# 这个名称似乎是默认的样例联盟,在没有联盟的时候会被拿出来用
SampleConsortium:
Organizations:
- *Org1
- *Org2
# 这两个 profile 则是产生频道用的。换言之,一套 orderer 和多个组织,可以产生多个 consortium。
TwoOrgsChannel:
Consortium: SampleConsortium
Application:
<<: *ApplicationDefaults
Organizations:
- *Org1
- *Org2

# 因为增加了 profile,所以要去改使用 profile 的地方。
ThreeOrgsOrdererGenesis:
Orderer:
# 按照 YAML 的语法,这是对 anchor 的引用
<<: *OrdererDefaults
Organizations:
- *OrdererOrg
Consortiums:
# 这里换了一个联盟来跑这个 network。
BusinessConsortium:
Organizations:
- *Org1
- *Org2
- *Org3
ThreeOrgsChannel:
Consortium: BusinessConsortium
Application:
<<: *ApplicationDefaults
Organizations:
- *Org1
- *Org2
- *Org3

################################################################################
#
# Section: Organizations
#
# - This section defines the different organizational identities which will
# be referenced later in the configuration.
#
################################################################################
Organizations:

# SampleOrg defines an MSP using the sampleconfig. It should never be used
# in production but may be used as a template for other definitions
- &OrdererOrg
# DefaultOrg defines the organization which is used in the sampleconfig
# of the fabric.git development environment
Name: OrdererOrg

# ID to load the MSP definition as
ID: OrdererMSP

# MSPDir is the filesystem path which contains the MSP configuration
MSPDir: crypto-config/ordererOrganizations/ORDERER_DOMAIN/msp

- &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: crypto-config/peerOrganizations/ORG1_DOMAIN/msp

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_DOMAIN
Port: 7051

- &Org2
# DefaultOrg defines the organization which is used in the sampleconfig
# of the fabric.git development environment
Name: Org2MSP

# ID to load the MSP definition as
ID: Org2MSP

MSPDir: crypto-config/peerOrganizations/ORG2_DOMAIN/msp

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.ORG2_DOMAIN
Port: 7051

- &Org3
# 这个组织 name取什么名字,完全无碍于MSP ID。但会影响 configtxgen 的 -asOrg 参数,进而影响 anchor 节点 configtx 的生成。
Name: Org3MSP
# 这里这个 MSP ID 完全可以不按照惯例来,也完全不影响整个网络的启动,也完全不受 crypto-config.yaml 的配置文件影响。
ID: Org3-MSP
MSPDir: crypto-config/peerOrganizations/ORG3_DOMAIN/msp
# 锚节点可以是数组
AnchorPeers:
- Host: peer0.ORG3_DOMAIN
Port: 7051

################################################################################
#
# SECTION: Orderer
#
# - This section defines the values to encode into a config transaction or
# genesis block for orderer related parameters
#
################################################################################
# 这里就是 anchor 了。
Orderer: &OrdererDefaults

# Orderer Type: The orderer implementation to start
# Available types are "solo" and "kafka"
OrdererType: solo

Addresses:
- orderer.ORDERER_DOMAIN:7050

# Batch Timeout: The amount of time to wait before creating a batch
BatchTimeout: 2s

# Batch Size: Controls the number of messages batched into a block
BatchSize:

# Max Message Count: The maximum number of messages to permit in a batch
MaxMessageCount: 10

# Absolute Max Bytes: The absolute maximum number of bytes allowed for
# the serialized messages in a batch.
AbsoluteMaxBytes: 99 MB

# Preferred Max Bytes: The preferred maximum number of bytes allowed for
# the serialized messages in a batch. A message larger than the preferred
# max bytes will result in a batch larger than preferred max bytes.
PreferredMaxBytes: 512 KB

Kafka:
# Brokers: A list of Kafka brokers to which the orderer connects
# NOTE: Use IP:port notation
Brokers:
- 127.0.0.1:9092

# Organizations is the list of orgs which are defined as participants on
# the orderer side of the network
Organizations:

################################################################################
#
# SECTION: Application
#
# - This section defines the values to encode into a config transaction or
# genesis block for application related parameters
#
################################################################################
Application: &ApplicationDefaults

# Organizations is the list of orgs which are defined as participants on
# the application side of the network
Organizations:

4.3 docker-compose 文件家族

服务的定义依赖关系大概是 peer-base.yaml -> docker-compose-base.yaml -> docker-compose-cli.yaml。

4.3.1 常见环境变量前缀

  • Orderer 相关的环境变量开头是 ORDERER_GENERAL_
  • Peer 相关的环境变量开头是 CORE_PEER_
  • 其他环境变量开头是 CORE_

4.3.2 peer-base.yaml

启动 chaincode 容器都是在本网桥网络里启动的。

它的工作目录是 /opt/gopath/src/github.com/hyperledger/fabric/peer。二进制的可执行文件应该都在 gopath 下。

它的工作路径里有二进制的 peer 文件,所以可以直接 peer node start 启动。

它的所谓的 vm enpoint 看起来就是容器服务治理的思路,用 port 来做 endpoint 的端点 CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 这个文件夹是纯净的,与 example 组织无关。
version: '2'

services:
peer-base:
image: hyperledger/fabric-peer:$IMAGE_TAG
environment:
- CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
# 用默认的网桥网络来启动链码容器。
# the following setting starts chaincode containers on the same
# bridge network as the peers
# https://docs.docker.com/compose/networking/
- CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=${COMPOSE_PROJECT_NAME}_byfn
#- CORE_LOGGING_LEVEL=ERROR
- CORE_LOGGING_LEVEL=DEBUG
- CORE_PEER_TLS_ENABLED=true
- CORE_PEER_GOSSIP_USELEADERELECTION=true
- CORE_PEER_GOSSIP_ORGLEADER=false
- CORE_PEER_PROFILE_ENABLED=true
- CORE_PEER_TLS_CERT_FILE=/etc/hyperledger/fabric/tls/server.crt
- CORE_PEER_TLS_KEY_FILE=/etc/hyperledger/fabric/tls/server.key
- CORE_PEER_TLS_ROOTCERT_FILE=/etc/hyperledger/fabric/tls/ca.crt
working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer
command: peer node start

4.3.3 docker-compose-base.yaml

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
version: '2'

services:
# orderer 的配置
orderer.example.com:
container_name: orderer.example.com
image: hyperledger/fabric-orderer:$IMAGE_TAG
environment:
- ORDERER_GENERAL_LOGLEVEL=debug
- ORDERER_GENERAL_LISTENADDRESS=0.0.0.0
- ORDERER_GENERAL_GENESISMETHOD=file
# 这个给 orderer 准备的创世区块,看下面的 volumes 的映射
- ORDERER_GENERAL_GENESISFILE=/var/hyperledger/orderer/orderer.genesis.block
- ORDERER_GENERAL_LOCALMSPID=OrdererMSP
- ORDERER_GENERAL_LOCALMSPDIR=/var/hyperledger/orderer/msp
# enabled TLS
- ORDERER_GENERAL_TLS_ENABLED=true
# 服务器的 key
- ORDERER_GENERAL_TLS_PRIVATEKEY=/var/hyperledger/orderer/tls/server.key
# 服务器的证书
- ORDERER_GENERAL_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/server.crt
# ca 的证书
- ORDERER_GENERAL_TLS_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
working_dir: /opt/gopath/src/github.com/hyperledger/fabric
# 启动命令,不像 peer,不再需要加参数了。
command: orderer
volumes:
# 对创世区块的映射
- ../channel-artifacts/genesis.block:/var/hyperledger/orderer/orderer.genesis.block
# 对 msp 文件夹的映射
- ../crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/msp:/var/hyperledger/orderer/msp
# 对 tls 文件夹的映射
- ../crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/tls/:/var/hyperledger/orderer/tls
# 注意,这一步需要的命名卷,实际上就是要在 docker-compose-cli 里被顶层 volume 实例化
- orderer.example.com:/var/hyperledger/production/orderer
ports:
- 7050:7050

peer0.org1.example.com:
# 经典的继承语法
container_name: peer0.org1.example.com
extends:
file: peer-base.yaml
service: peer-base
environment:
- CORE_PEER_ID=peer0.org1.example.com
- CORE_PEER_ADDRESS=peer0.org1.example.com:7051
# 外部的流言地址,和本机的 peer 地址一样
- CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.org1.example.com:7051
- CORE_PEER_LOCALMSPID=Org1MSP
volumes:
- /var/run/:/host/var/run/
# peer 容器必备:MSP 与 TLS 文件夹
- ../crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/msp:/etc/hyperledger/fabric/msp
- ../crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls:/etc/hyperledger/fabric/tls
- peer0.org1.example.com:/var/hyperledger/production
ports:
- 7051:7051
- 7053:7053

peer1.org1.example.com:
container_name: peer1.org1.example.com
extends:
file: peer-base.yaml
service: peer-base
environment:
- CORE_PEER_ID=peer1.org1.example.com
- CORE_PEER_ADDRESS=peer1.org1.example.com:7051
- CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer1.org1.example.com:7051
- CORE_PEER_GOSSIP_BOOTSTRAP=peer0.org1.example.com:7051
- CORE_PEER_LOCALMSPID=Org1MSP
volumes:
- /var/run/:/host/var/run/
- ../crypto-config/peerOrganizations/org1.example.com/peers/peer1.org1.example.com/msp:/etc/hyperledger/fabric/msp
- ../crypto-config/peerOrganizations/org1.example.com/peers/peer1.org1.example.com/tls:/etc/hyperledger/fabric/tls
- peer1.org1.example.com:/var/hyperledger/production

ports:
- 8051:7051
- 8053:7053

peer0.org2.example.com:
container_name: peer0.org2.example.com
extends:
file: peer-base.yaml
service: peer-base
environment:
- CORE_PEER_ID=peer0.org2.example.com
- CORE_PEER_ADDRESS=peer0.org2.example.com:7051
- CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.org2.example.com:7051
- CORE_PEER_GOSSIP_BOOTSTRAP=peer0.org2.example.com:7051
- CORE_PEER_LOCALMSPID=Org2MSP
volumes:
- /var/run/:/host/var/run/
- ../crypto-config/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/msp:/etc/hyperledger/fabric/msp
- ../crypto-config/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls:/etc/hyperledger/fabric/tls
- peer0.org2.example.com:/var/hyperledger/production
ports:
- 9051:7051
- 9053:7053
# 所有节点的变化:
# 1 修改 service name。
# 2 修改 container_name
# 3 修改本 peer 地址
# 4 修改 gossip 相关,实际上还是本机地址
peer1.org2.example.com:
container_name: peer1.org2.example.com
extends:
file: peer-base.yaml
service: peer-base
environment:
- CORE_PEER_ID=peer1.org2.example.com
- CORE_PEER_ADDRESS=peer1.org2.example.com:7051
- CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer1.org2.example.com:7051
- CORE_PEER_GOSSIP_BOOTSTRAP=peer1.org2.example.com:7051
- CORE_PEER_LOCALMSPID=Org2MSP
volumes:
- /var/run/:/host/var/run/
- ../crypto-config/peerOrganizations/org2.example.com/peers/peer1.org2.example.com/msp:/etc/hyperledger/fabric/msp
- ../crypto-config/peerOrganizations/org2.example.com/peers/peer1.org2.example.com/tls:/etc/hyperledger/fabric/tls
- peer1.org2.example.com:/var/hyperledger/production
ports:
- 10051:7051
- 10053:7053

4.3.4 docker-compose-cli.yaml

在 cli 容器内的的 Gopath 就是普通的 /opt/gopath

在容器内有四个已经写好的目录,要映射到本地目录上才能用,映射关系大致是:

  • ./../chaincode/:/opt/gopath/src/github.com/hyperledger/fabric/examples/chaincode/go
  • ./scripts:/opt/gopath/src/github.com/hyperledger/fabric/peer/scripts/:脚本的位置,这里应该可以放一些让容器外的控制命令操纵整个 fabric 的脚本
  • ./channel-artifacts:/opt/gopath/src/github.com/hyperledger/fabric/peer/channel-artifacts:当然这就是 channel 的器物所在地了,本身的相关文件的放置地
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
# Copyright IBM Corp. All Rights Reserved.
#
# SPDX-License-Identifier: Apache-2.0
#
# 当前 compose 语法已经升级到了 version 3了。
version: '2'

# 官方文档的释义,大义是顶级的 volume 元素是为了在不同的 service 之间共享 volume 而准备的。
# You can mount a host path as part of a definition for a single service, and there is no need to define it in the top level volumes key.
# But, if you want to reuse a volume across multiple services, then define a named volume in the top-level volumes key. Use named volumes with services, swarms, and stack files.
volumes:
# 这个语法相当于声明了若干个命名卷。不同的容器之间把相同的卷挂载到自己本地的路径里,就相当于打通了两个容器的数据共享。
# 注意,命名卷的开头并不是路径形式的,所以不要求当前的 host 有这个名字的绝对路径或者相对路径。
orderer.ORDERER_DOMAIN:
peer0.org1.example.com:
peer1.org1.example.com:
peer0.org2.example.com:
peer1.org2.example.com:

# 一个顶级网络名称,供服务之间引用
networks:
byfn:

services:
orderer.ORDERER_DOMAIN:
extends:
file: base/docker-compose-base.yaml
service: orderer.ORDERER_DOMAIN
# 在这里虽然可以覆盖 container name,但有必要么?
container_name: orderer.ORDERER_DOMAIN
networks:
# 这是要加入的网络名称
- byfn

peer0.org1.example.com:
container_name: peer0.org1.example.com
extends:
file: base/docker-compose-base.yaml
service: peer0.org1.example.com
networks:
- byfn

peer1.org1.example.com:
container_name: peer1.org1.example.com
extends:
file: base/docker-compose-base.yaml
service: peer1.org1.example.com
networks:
- byfn

peer0.org2.example.com:
container_name: peer0.org2.example.com
extends:
file: base/docker-compose-base.yaml
service: peer0.org2.example.com
networks:
- byfn

peer1.org2.example.com:
container_name: peer1.org2.example.com
extends:
file: base/docker-compose-base.yaml
service: peer1.org2.example.com
networks:
- byfn

cli:
container_name: cli
# 这里不继承任何配置文件,而是直接使用镜像初始化容器
image: hyperledger/fabric-tools:$IMAGE_TAG
tty: true
environment:
- GOPATH=/opt/gopath
- CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
- CORE_LOGGING_LEVEL=DEBUG
- CORE_PEER_ID=cli
- CORE_PEER_ADDRESS=peer0.org1.example.com:7051
- CORE_PEER_LOCALMSPID=Org1MSP
- CORE_PEER_TLS_ENABLED=true
- CORE_PEER_TLS_CERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.crt
- CORE_PEER_TLS_KEY_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.key
- CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
- CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer
# 容器运行的时候,就会在内部的 console 里启动这段命令。类似我们经常 docker run 使用的 /bin/bash 一样。
# 在这里我们传导了两个关键的环境变量进去,一个频道名,一个延迟。这也是目前唯二的还能在 cli 容器启动时改变的东西了。
command: /bin/bash -c './scripts/script.sh ${CHANNEL_NAME} ${DELAY}; sleep $TIMEOUT'
volumes:
# host 上路径: 容器内路径
# 映射本机的运行时配置文件到容器内。
- /var/run/:/host/var/run/
# :这就是链码的主要位置了,看来这个目录也是镜像里就写死的,要写自定义的链码也只能从这里安装进去。
- ./../chaincode/:/opt/gopath/src/github.com/hyperledger/fabric/examples/chaincode/go
- ./crypto-config:/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
- ./scripts:/opt/gopath/src/github.com/hyperledger/fabric/peer/scripts/
- ./channel-artifacts:/opt/gopath/src/github.com/hyperledger/fabric/peer/channel-artifacts
depends_on:
# 等这些 service 启动完了,再启动自己这个 service。
- orderer.ORDERER_DOMAIN
- peer0.org1.example.com
- peer1.org1.example.com
- peer0.org2.example.com
- peer1.org2.example.com
networks:
- byfn

4.3.5 docker-compose-e2e-template.yaml

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
# Copyright IBM Corp. All Rights Reserved.
#
# SPDX-License-Identifier: Apache-2.0
#
# 这个文件会被 byfn.sh 的 replacePrivateKey() 函数生成实际的 docker-compose-e2e.yaml。
# 然后被 sed 命令另作他用。
version: '2'

volumes:
orderer.example.com:
peer0.org1.example.com:
peer1.org1.example.com:
peer0.org2.example.com:
peer1.org2.example.com:

networks:
byfn:
services:
ca0:
image: hyperledger/fabric-ca:$IMAGE_TAG
environment:
- FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server
- FABRIC_CA_SERVER_CA_NAME=ca-org1
- FABRIC_CA_SERVER_TLS_ENABLED=true
- FABRIC_CA_SERVER_TLS_CERTFILE=/etc/hyperledger/fabric-ca-server-config/ca.org1.example.com-cert.pem
- FABRIC_CA_SERVER_TLS_KEYFILE=/etc/hyperledger/fabric-ca-server-config/CA1_PRIVATE_KEY
ports:
- "7054:7054"
command: sh -c 'fabric-ca-server start --ca.certfile /etc/hyperledger/fabric-ca-server-config/ca.org1.example.com-cert.pem --ca.keyfile /etc/hyperledger/fabric-ca-server-config/CA1_PRIVATE_KEY -b admin:adminpw -d'
volumes:
- ./crypto-config/peerOrganizations/org1.example.com/ca/:/etc/hyperledger/fabric-ca-server-config
container_name: ca_peerOrg1
networks:
- byfn

ca1:
image: hyperledger/fabric-ca:$IMAGE_TAG
environment:
- FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server
- FABRIC_CA_SERVER_CA_NAME=ca-org2
- FABRIC_CA_SERVER_TLS_ENABLED=true
- FABRIC_CA_SERVER_TLS_CERTFILE=/etc/hyperledger/fabric-ca-server-config/ca.org2.example.com-cert.pem
- FABRIC_CA_SERVER_TLS_KEYFILE=/etc/hyperledger/fabric-ca-server-config/CA2_PRIVATE_KEY
ports:
- "8054:7054"
command: sh -c 'fabric-ca-server start --ca.certfile /etc/hyperledger/fabric-ca-server-config/ca.org2.example.com-cert.pem --ca.keyfile /etc/hyperledger/fabric-ca-server-config/CA2_PRIVATE_KEY -b admin:adminpw -d'
volumes:
- ./crypto-config/peerOrganizations/org2.example.com/ca/:/etc/hyperledger/fabric-ca-server-config
container_name: ca_peerOrg2
networks:
- byfn

orderer.example.com:
extends:
file: base/docker-compose-base.yaml
service: orderer.example.com
container_name: orderer.example.com
networks:
- byfn

peer0.org1.example.com:
container_name: peer0.org1.example.com
extends:
file: base/docker-compose-base.yaml
service: peer0.org1.example.com
networks:
- byfn

peer1.org1.example.com:
container_name: peer1.org1.example.com
extends:
file: base/docker-compose-base.yaml
service: peer1.org1.example.com
networks:
- byfn

peer0.org2.example.com:
container_name: peer0.org2.example.com
extends:
file: base/docker-compose-base.yaml
service: peer0.org2.example.com
networks:
- byfn

peer1.org2.example.com:
container_name: peer1.org2.example.com
extends:
file: base/docker-compose-base.yaml
service: peer1.org2.example.com
networks:
- byfn

4.4 byfn.sh 大脚本

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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
#!/bin/bash

#
# Copyright IBM Corp All Rights Reserved
#
# SPDX-License-Identifier: Apache-2.0
#

# This script will orchestrate a sample end-to-end execution of the Hyperledger
# Fabric network.
#
# The end-to-end verification provisions a sample Fabric network consisting of
# two organizations, each maintaining two peers, and a “solo” ordering service.
#
# This verification makes use of two fundamental tools, which are necessary to
# create a functioning transactional network with digital signature validation
# and access control:
#
# * cryptogen - generates the x509 certificates used to identify and
# authenticate the various components in the network.
# * configtxgen - generates the requisite configuration artifacts for orderer
# bootstrap and channel creation.
#
# Each tool consumes a configuration yaml file, within which we specify the topology
# of our network (cryptogen) and the location of our certificates for various
# configuration operations (configtxgen). Once the tools have been successfully run,
# we are able to launch our network. More detail on the tools and the structure of
# the network will be provided later in this document. For now, let's get going...

# prepending $PWD/../bin to PATH to ensure we are picking up the correct binaries
# this may be commented out to resolve installed version of tools if desired
export PATH=${PWD}/../bin:${PWD}:$PATH
# 这个环境变量很重要,这里的 export 保证这个变量是在 environment 里。可以被子进程(另一个 shell 脚本继承)。
# 并不是所有的父进程里的 variable 都会被子进程看到的。
export FABRIC_CFG_PATH=${PWD}

# Print the usage message
function printHelp () {
echo "Usage: "
echo " byfn.sh -m up|down|restart|generate [-c <channel name>] [-t <timeout>] [-d <delay>] [-f <docker-compose-file>] [-s <dbtype>] [-i <imagetag>]"
echo " byfn.sh -h|--help (print this message)"
echo " -m <mode> - one of 'up', 'down', 'restart' or 'generate'"
echo " - 'up' - bring up the network with docker-compose up"
echo " - 'down' - clear the network with docker-compose down"
echo " - 'restart' - restart the network"
echo " - 'generate' - generate required certificates and genesis block"
echo " -c <channel name> - channel name to use (defaults to \"mychannel\")"
echo " -t <timeout> - CLI timeout duration in microseconds (defaults to 10000)"
echo " -d <delay> - delay duration in seconds (defaults to 3)"
echo " -f <docker-compose-file> - specify which docker-compose file use (defaults to docker-compose-cli.yaml)"
echo " -s <dbtype> - the database backend to use: goleveldb (default) or couchdb"
echo " -i <imagetag> - pass the image tag to launch the network using the tag: 1.0.1, 1.0.2, 1.0.3, 1.0.4 (defaults to latest)"
echo
echo "Typically, one would first generate the required certificates and "
echo "genesis block, then bring up the network. e.g.:"
echo
echo " byfn.sh -m generate -c mychannel"
echo " byfn.sh -m up -c mychannel -s couchdb"
echo " byfn.sh -m up -c mychannel -s couchdb -i 1.0.6"
echo " byfn.sh -m down -c mychannel"
echo
echo "Taking all defaults:"
echo " byfn.sh -m generate"
echo " byfn.sh -m up"
echo " byfn.sh -m down"
}

# Ask user for confirmation to proceed
function askProceed () {
read -p "Continue (y/n)? " ans
case "$ans" in
y|Y )
# 只有这个地方会正常走下去
echo "proceeding ..."
# 这个双分号是 case 的休止符
;;
n|N )
echo "exiting..."
exit 1
;;
* )
# 这里会递归
echo "invalid response"
askProceed
;;
esac
}

# Obtain CONTAINER_IDS and remove them
# TODO Might want to make this optional - could clear other containers
function clearContainers () {
# -a 表示输出全部容器,-q 表示只输出容器 id,这样就可以迭代使用了
# 结果大概是这样
# 8a25b455f05e
# d2d84545a902
# c67d337e5bd9
# e5cbd8457caf
# 9ee6a1cb33c1
# a0df580d4633
# cb6b441b083f
# 0c87d55710c8
# c24f3eb2e0e0
CONTAINER_IDS=$(docker ps -aq)
if [ -z "$CONTAINER_IDS" -o "$CONTAINER_IDS" == " " ]; then
echo "---- No containers available for deletion ----"
else
# 直接拿这个列表来 rm,而不用再 xargs 了。
docker rm -f $CONTAINER_IDS
fi
}

# Delete any images that were generated as a part of this setup
# specifically the following images are often left behind:
# TODO list generated image naming patterns
function removeUnwantedImages() {
# 用 awk 来截取多段输出的好例子。
# 注意在这里我们可以看到不要乱改 peer 的名字,不然不能在这里搞到所有的 image_id。
# $()的形式可以把命令求和为一个匿名变量。
DOCKER_IMAGE_IDS=$(docker images | grep "dev\|none\|test-vp\|peer[0-9]-" | awk '{print $3}')
if [ -z "$DOCKER_IMAGE_IDS" -o "$DOCKER_IMAGE_IDS" == " " ]; then
echo "---- No images available for deletion ----"
else
docker rmi -f $DOCKER_IMAGE_IDS
fi
}

# Generate the needed certificates, the genesis block and start the network.
function networkUp () {
# generate artifacts if they don't exist
# 确认一个目录是否存在的命令
if [ ! -d "crypto-config" ]; then
# 第一步,生成证书。其实不只是生成证书,还生成证书相关的东西。
generateCerts
replacePrivateKey
generateChannelArtifacts
fi
if [ "${IF_COUCHDB}" == "couchdb" ]; then
IMAGE_TAG=$IMAGETAG CHANNEL_NAME=$CHANNEL_NAME TIMEOUT=$CLI_TIMEOUT DELAY=$CLI_DELAY docker-compose -f $COMPOSE_FILE -f $COMPOSE_FILE_COUCH up -d 2>&1
else
# 这一行让 Docker-Compose 来按照配置文件装配容器配置,生成由容器组成的项目网络
# cli 容器起来以后,会自己去创建频道、一个一个地把容器加入网络中。一个一个地初始化链码,一个一个地测试链码。
IMAGE_TAG=$IMAGETAG CHANNEL_NAME=$CHANNEL_NAME TIMEOUT=$CLI_TIMEOUT DELAY=$CLI_DELAY docker-compose -f $COMPOSE_FILE up -d 2>&1
fi
if [ $? -ne 0 ]; then
echo "ERROR !!!! Unable to start network"
# 这个命令就是去读一个容器启动的 command 在 console 里的输出。
docker logs -f cli
exit 1
fi
docker logs -f cli
}

# Tear down running network
function networkDown () {
docker-compose -f $COMPOSE_FILE down --volumes
docker-compose -f $COMPOSE_FILE -f $COMPOSE_FILE_COUCH down --volumes
# Don't remove the generated artifacts -- note, the ledgers are always removed
if [ "$MODE" != "restart" ]; then
# Bring the containers down deleting their volumes
#Cleanup the chaincode containers
clearContainers
#Cleanup images
removeUnwantedImages
# remove orderer block and other channel configuration transactions and certs
rm -rf channel-artifacts/*.block channel-artifacts/*.tx crypto-config
# remove the docker-compose yaml file that was customized to the example
rm -f docker-compose-e2e.yaml
fi
}

# 但这个文件有什么用呢?docker-compose-e2e-template.yaml
# Using docker-compose-e2e-template.yaml, replace constants with private key file names
# generated by the cryptogen tool and output a docker-compose.yaml specific to this
# configuration
function replacePrivateKey () {
# sed on MacOSX does not support -i flag with a null extension. We will use
# 't' for our back-up's extension and depete it at the end of the function
ARCH=`uname -s | grep Darwin`
if [ "$ARCH" == "Darwin" ]; then
OPTS="-it"
else
OPTS="-i"
fi

# Copy the template to the file that will be modified to add the private key
cp docker-compose-e2e-template.yaml docker-compose-e2e.yaml

# The next steps will replace the template's contents with the
# actual values of the private key file names for the two CAs.
# 把当前的父文件夹记住
CURRENT_DIR=$PWD
cd crypto-config/peerOrganizations/org1.example.com/ca/
PRIV_KEY=$(ls *_sk)
cd "$CURRENT_DIR"
sed $OPTS "s/CA1_PRIVATE_KEY/${PRIV_KEY}/g" docker-compose-e2e.yaml
cd crypto-config/peerOrganizations/org2.example.com/ca/
PRIV_KEY=$(ls *_sk)
cd "$CURRENT_DIR"
sed $OPTS "s/CA2_PRIVATE_KEY/${PRIV_KEY}/g" docker-compose-e2e.yaml
# If MacOSX, remove the temporary backup of the docker-compose file
if [ "$ARCH" == "Darwin" ]; then
rm docker-compose-e2e.yamlt
fi
}

# X.509只是 PKI 密码学架构的一个实现。
# ca-cert 是根证书。
# 为什么 count 必须放在这个文件里面。
# 这个函数不止生成证书,还有 keystore。
# 私钥签署,公钥验证!而不是公钥签署,私钥验证!
# We will use the cryptogen tool to generate the cryptographic material (x509 certs)
# for our various network entities. The certificates are based on a standard PKI
# implementation where validation is achieved by reaching a common trust anchor.
#
# Cryptogen consumes a file - ``crypto-config.yaml`` - that contains the network
# topology and allows us to generate a library of certificates for both the
# Organizations and the components that belong to those Organizations. Each
# Organization is provisioned a unique root certificate (``ca-cert``), that binds
# specific components (peers and orderers) to that Org. Transactions and communications
# within Fabric are signed by an entity's private key (``keystore``), and then verified
# by means of a public key (``signcerts``). You will notice a "count" variable within
# this file. We use this to specify the number of peers per Organization; in our
# case it's two peers per Org. The rest of this template is extremely
# self-explanatory.
#
# After we run the tool, the certs will be parked in a folder titled ``crypto-config``.

# Generates Org certs using cryptogen tool
function generateCerts (){
which cryptogen
if [ "$?" -ne 0 ]; then
echo "cryptogen tool not found. exiting"
exit 1
fi
echo
echo "##########################################################"
echo "##### Generate certificates using cryptogen tool #########"
echo "##########################################################"
if [ -d "crypto-config" ]; then
rm -Rf crypto-config
fi
cryptogen generate --config=./crypto-config.yaml
if [ "$?" -ne 0 ]; then
echo "Failed to generate certificates..."
exit 1
fi
echo
}

# 每一个新的 org 都要有一个 anchor 节点。这也就意味着不仅配置文件里要有相关的配置,也意味着初始化步骤里
# 要有相关的配置命令。
# 把三件事做起来:
# 1 生成创世区块。 2 生成频道配置 3 配置锚节点。
#
# The `configtxgen tool is used to create four artifacts: orderer **bootstrap
# block**, fabric **channel configuration transaction**, and two **anchor
# peer transactions** - one for each Peer Org.
#
# The orderer block is the genesis block for the ordering service, and the
# channel transaction file is broadcast to the orderer at channel creation
# time. The anchor peer transactions, as the name might suggest, specify each
# Org's anchor peer on this channel.
#
# Configtxgen consumes a file - ``configtx.yaml`` - that contains the definitions
# for the sample network. There are three members - one Orderer Org (``OrdererOrg``)
# and two Peer Orgs (``Org1`` & ``Org2``) each managing and maintaining two peer nodes.
# This file also specifies a consortium - ``SampleConsortium`` - consisting of our
# two Peer Orgs. Pay specific attention to the "Profiles" section at the top of
# this file. You will notice that we have two unique headers. One for the orderer genesis
# block - ``TwoOrgsOrdererGenesis`` - and one for our channel - ``TwoOrgsChannel``.
# These headers are important, as we will pass them in as arguments when we create
# our artifacts. This file also contains two additional specifications that are worth
# noting. Firstly, we specify the anchor peers for each Peer Org
# (``peer0.org1.example.com`` & ``peer0.org2.example.com``). Secondly, we point to
# the location of the MSP directory for each member, in turn allowing us to store the
# root certificates for each Org in the orderer genesis block. This is a critical
# concept. Now any network entity communicating with the ordering service can have
# its digital signature verified.
#
# This function will generate the crypto material and our four configuration
# artifacts, and subsequently output these files into the ``channel-artifacts``
# folder.
#
# If you receive the following warning, it can be safely ignored:
#
# [bccsp] GetDefault -> WARN 001 Before using BCCSP, please call InitFactories(). Falling back to bootBCCSP.
#
# You can ignore the logs regarding intermediate certs, we are not using them in
# this crypto implementation.

# Generate orderer genesis block, channel configuration transaction and
# anchor peer update transactions
function generateChannelArtifacts() {
# 检测一个命令是不是存在
which configtxgen
if [ "$?" -ne 0 ]; then
echo "configtxgen tool not found. exiting"
exit 1
fi

echo "##########################################################"
echo "######### Generating Orderer Genesis block ##############"
echo "##########################################################"
# Note: For some unknown reason (at least for now) the block file can't be
# named orderer.genesis.block or the orderer will fail to launch!
# 1 生成创世区块
configtxgen -profile TwoOrgsOrdererGenesis -outputBlock ./channel-artifacts/genesis.block
if [ "$?" -ne 0 ]; then
echo "Failed to generate orderer genesis block..."
exit 1
fi
echo
echo "#################################################################"
echo "### Generating channel configuration transaction 'channel.tx' ###"
echo "#################################################################"
# 2 生成频道配置,实际上就是 channel 事务。
configtxgen -profile TwoOrgsChannel -outputCreateChannelTx ./channel-artifacts/channel.tx -channelID $CHANNEL_NAME
if [ "$?" -ne 0 ]; then
echo "Failed to generate channel configuration transaction..."
exit 1
fi

echo
echo "#################################################################"
echo "####### Generating anchor peer update for Org1MSP ##########"
echo "#################################################################"
# configtxgen -help 可以看到它的各种 options。其中 outputAnchorPeersUpdate 只能在创建缺省频道(实际上自定义频道也可以),和最初建 anchor 时才可以使用。
# 这个 channel 名和 asOrg 参数共同决定 Org1MSPanchors.tx 的内容。
configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./channel-artifacts/Org1MSPanchors.tx -channelID $CHANNEL_NAME -asOrg Org1MSP
if [ "$?" -ne 0 ]; then
echo "Failed to generate anchor peer update for Org1MSP..."
exit 1
fi

echo
echo "#################################################################"
echo "####### Generating anchor peer update for Org2MSP ##########"
echo "#################################################################"
configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate \
./channel-artifacts/Org2MSPanchors.tx -channelID $CHANNEL_NAME -asOrg Org2MSP
if [ "$?" -ne 0 ]; then
echo "Failed to generate anchor peer update for Org2MSP..."
exit 1
fi
echo
}

# 定义完函数以后,这里才是程序的起点
# Obtain the OS and Architecture string that will be used to select the correct
# native binaries for your platform
OS_ARCH=$(echo "$(uname -s|tr '[:upper:]' '[:lower:]'|sed 's/mingw64_nt.*/windows/')-$(uname -m | sed 's/x86_64/amd64/g')" | awk '{print tolower($0)}')
# timeout duration - the duration the CLI should wait for a response from
# another container before giving up
CLI_TIMEOUT=10
#default for delay
CLI_DELAY=3
# channel name defaults to "mychannel"
CHANNEL_NAME="mychannel"
# use this as the default docker-compose yaml definition
COMPOSE_FILE=docker-compose-cli.yaml
#
COMPOSE_FILE_COUCH=docker-compose-couch.yaml
# default image tag
IMAGETAG="latest"
# Parse commandline args
while getopts "h?m:c:t:d:f:s:i:" opt; do
case "$opt" in
h|\?)
printHelp
exit 0
;;
m) MODE=$OPTARG
;;
c) CHANNEL_NAME=$OPTARG
;;
t) CLI_TIMEOUT=$OPTARG
;;
d) CLI_DELAY=$OPTARG
;;
f) COMPOSE_FILE=$OPTARG
;;
s) IF_COUCHDB=$OPTARG
;;
i) IMAGETAG=`uname -m`"-"$OPTARG
;;
esac
done

# Determine whether starting, stopping, restarting or generating for announce
if [ "$MODE" == "up" ]; then
EXPMODE="Starting"
elif [ "$MODE" == "down" ]; then
EXPMODE="Stopping"
elif [ "$MODE" == "restart" ]; then
EXPMODE="Restarting"
elif [ "$MODE" == "generate" ]; then
EXPMODE="Generating certs and genesis block for"
else
printHelp
exit 1
fi

# Announce what was requested

if [ "${IF_COUCHDB}" == "couchdb" ]; then
echo
echo "${EXPMODE} with channel '${CHANNEL_NAME}' and CLI timeout of '${CLI_TIMEOUT}' using database '${IF_COUCHDB}'"
else
echo "${EXPMODE} with channel '${CHANNEL_NAME}' and CLI timeout of '${CLI_TIMEOUT}'"
fi
# ask for confirmation to proceed
askProceed

#Create the network using docker compose
if [ "${MODE}" == "up" ]; then
networkUp
elif [ "${MODE}" == "down" ]; then ## Clear the network
networkDown
elif [ "${MODE}" == "generate" ]; then ## Generate Artifacts
generateCerts
replacePrivateKey
generateChannelArtifacts
elif [ "${MODE}" == "restart" ]; then ## Restart the network
networkDown
networkUp
else
printHelp
exit 1
fi

4.5 script/script.sh(cli 容器内脚本)

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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
#!/bin/bash

echo
echo " ____ _____ _ ____ _____ "
echo "/ ___| |_ _| / \ | _ \ |_ _|"
echo "\___ \ | | / _ \ | |_) | | | "
echo " ___) | | | / ___ \ | _ < | | "
echo "|____/ |_| /_/ \_\ |_| \_\ |_| "
echo
echo "Build your first network (BYFN) end-to-end test"
echo
CHANNEL_NAME="$1"
DELAY="$2"
: ${CHANNEL_NAME:="mychannel"}
: ${TIMEOUT:="60"}
COUNTER=1
MAX_RETRY=5
# 是不是使用 TLS,其实只与 orderer 和 channel 有关。
ORDERER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/ORDERER_DOMAIN/orderers/orderer.ORDERER_DOMAIN/msp/tlscacerts/tlsca.ORDERER_DOMAIN-cert.pem

echo "Channel name : "$CHANNEL_NAME

# verify the result of the end-to-end test
verifyResult () {
if [ $1 -ne 0 ] ; then
echo "!!!!!!!!!!!!!!! "$2" !!!!!!!!!!!!!!!!"
echo "========= ERROR !!! FAILED to execute End-2-End Scenario ==========="
echo
exit 1
fi
}

setGlobals () {

if [ $1 -eq 0 -o $1 -eq 1 ] ; then
CORE_PEER_LOCALMSPID="Org1MSP"
CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
# 传进来不同的命令,更改的核心环境变量指标是容器的 endpoint 地址。也就是CORE_PEER_ADDRESS。相同组织的 enpoint 使用的是相同的 MSP 密码学目录。
if [ $1 -eq 0 ]; then
CORE_PEER_ADDRESS=peer0.org1.example.com:7051
else
CORE_PEER_ADDRESS=peer1.org1.example.com:7051
# 此处有重复,疑为错误。但 github 上原版的代码就是这样写的。
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
fi
else
CORE_PEER_LOCALMSPID="Org2MSP"
CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
if [ $1 -eq 2 ]; then
CORE_PEER_ADDRESS=peer0.org2.example.com:7051
else
CORE_PEER_ADDRESS=peer1.org2.example.com:7051
fi
fi

env |grep CORE
}

# 创建频道是第一步
createChannel() {
# 把 cli 内的全局变量设置为0系列
setGlobals 0

if [ -z "$CORE_PEER_TLS_ENABLED" -o "$CORE_PEER_TLS_ENABLED" = "false" ]; then
# 1 -o 是 orderer string,实际上就是从cli 容器出发,可以读到的容器地址,这里还要考虑容器网络连通性问题。-c 是频道名。channel.tx是不可读文件。
# 换言之,如果我们有足够多的创世区块,和 channel.tx。我们可以在一个 cli 容器里面生成多个频道。
# 2 实际上我们从这一步开始,就知道了 orderer 才是最先 join 进这个 channel 里的一个节点。
# 3 几乎所有的频道、peer、orderer 节点相关的操作,都要依靠这个 peer channel 命令开头的系列命令。
# 4 channel id 就是 channel name。
peer channel create -o orderer.ORDERER_DOMAIN:7050 -c $CHANNEL_NAME -f ./channel-artifacts/channel.tx >&log.txt
else
# 注意,如果需要打开 tls,那么这个--cafile选项特别特别重要。
peer channel create -o orderer.ORDERER_DOMAIN:7050 -c $CHANNEL_NAME -f ./channel-artifacts/channel.tx --tls $CORE_PEER_TLS_ENABLED --cafile $ORDERER_CA >&log.txt
fi
res=$?
cat log.txt
verifyResult $res "Channel creation failed"
echo "===================== Channel \"$CHANNEL_NAME\" is created successfully ===================== "
echo
}

updateAnchorPeers() {
PEER=$1
setGlobals $PEER

if [ -z "$CORE_PEER_TLS_ENABLED" -o "$CORE_PEER_TLS_ENABLED" = "false" ]; then
# 生成的 anchor artifact 到这里才有用。这样才算真的把锚节点注册上去了。
peer channel update -o orderer.ORDERER_DOMAIN:7050 -c $CHANNEL_NAME -f ./channel-artifacts/${CORE_PEER_LOCALMSPID}anchors.tx >&log.txt
else
peer channel update -o orderer.ORDERER_DOMAIN:7050 -c $CHANNEL_NAME -f ./channel-artifacts/${CORE_PEER_LOCALMSPID}anchors.tx --tls $CORE_PEER_TLS_ENABLED --cafile $ORDERER_CA >&log.txt
fi
res=$?
cat log.txt
verifyResult $res "Anchor peer update failed"
echo "===================== Anchor peers for org \"$CORE_PEER_LOCALMSPID\" on \"$CHANNEL_NAME\" is updated successfully ===================== "
sleep $DELAY
echo
}

## Sometimes Join takes time hence RETRY atleast for 5 times
joinWithRetry () {
# peer channel join 实际上是消耗四个环境变量作为把 peer 加入 channel 的依据,所以外部传进来的环境变量到此几乎可说是无用的。
# CORE_PEER_MSPCONFIGPATH
# CORE_PEER_ADDRESS
# CORE_PEER_LOCALMSPID
# CORE_PEER_TLS_ROOTCERT_FILE
peer channel join -b $CHANNEL_NAME.block >&log.txt
res=$?
cat log.txt
if [ $res -ne 0 -a $COUNTER -lt $MAX_RETRY ]; then
COUNTER=` expr $COUNTER + 1`
# 这的 peer 是顺序 peer,和实际的容器名是不一样的。
echo "PEER$1 failed to join the channel, Retry after 2 seconds"
# bash sleep 的妙用
sleep $DELAY
# 递归重试
joinWithRetry $1
else
COUNTER=1
fi
verifyResult $res "After $MAX_RETRY attempts, PEER$ch has failed to Join the Channel"
}

joinChannel () {
for ch in 0 1 2 3; do
setGlobals $ch
joinWithRetry $ch
echo "===================== PEER$ch joined on the channel \"$CHANNEL_NAME\" ===================== "
sleep $DELAY
echo
done
}

installChaincode () {
PEER=$1
setGlobals $PEER
# 这里这个 p 就是在cli 容器内可以看到的 chaincode 的 go 文件路径了。n 则是链码的合约名字。
# 这个链码为什么不需要经过编译,真是奇也怪哉。
peer chaincode install -n mycc -v 1.0 -p github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02 >&log.txt
res=$?
cat log.txt
verifyResult $res "Chaincode installation on remote peer PEER$PEER has Failed"
echo "===================== Chaincode is installed on remote peer PEER$PEER ===================== "
echo
}

instantiateChaincode () {
PEER=$1
setGlobals $PEER
# 用硬编码的方式好过用接口的方式来读写 orderer endpoint 的位置。
# while 'peer chaincode' command can get the orderer endpoint from the peer (if join was successful),
# lets supply it directly as we know it using the "-o" option
if [ -z "$CORE_PEER_TLS_ENABLED" -o "$CORE_PEER_TLS_ENABLED" = "false" ]; then
# 在链码初始化的时候,制定背书策略。
# 供反射调用
peer chaincode instantiate -o orderer.ORDERER_DOMAIN:7050 -C $CHANNEL_NAME -n mycc -v 1.0 -c '{"Args":["init","a","100","b","200"]}' -P "OR ('Org1MSP.member','Org2MSP.member')" >&log.txt
else
peer chaincode instantiate -o orderer.ORDERER_DOMAIN:7050 --tls $CORE_PEER_TLS_ENABLED --cafile $ORDERER_CA -C $CHANNEL_NAME -n mycc -v 1.0 -c '{"Args":["init","a","100","b","200"]}' -P "OR ('Org1MSP.member','Org2MSP.member')" >&log.txt
fi
res=$?
cat log.txt
verifyResult $res "Chaincode instantiation on PEER$PEER on channel '$CHANNEL_NAME' failed"
echo "===================== Chaincode Instantiation on PEER$PEER on channel '$CHANNEL_NAME' is successful ===================== "
echo
}

chaincodeQuery () {
PEER=$1
echo "===================== Querying on PEER$PEER on channel '$CHANNEL_NAME'... ===================== "
setGlobals $PEER
local rc=1
local starttime=$(date +%s)

# continue to poll
# we either get a successful response, or reach TIMEOUT
while test "$(($(date +%s)-starttime))" -lt "$TIMEOUT" -a $rc -ne 0
do
sleep $DELAY
echo "Attempting to Query PEER$PEER ...$(($(date +%s)-starttime)) secs"
peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["query","a"]}' >&log.txt
test $? -eq 0 && VALUE=$(cat log.txt | awk '/Query Result/ {print $NF}')
test "$VALUE" = "$2" && let rc=0
done
echo
cat log.txt
if test $rc -eq 0 ; then
echo "===================== Query on PEER$PEER on channel '$CHANNEL_NAME' is successful ===================== "
else
echo "!!!!!!!!!!!!!!! Query result on PEER$PEER is INVALID !!!!!!!!!!!!!!!!"
echo "================== ERROR !!! FAILED to execute End-2-End Scenario =================="
echo
exit 1
fi
}

chaincodeInvoke () {
PEER=$1
setGlobals $PEER
# while 'peer chaincode' command can get the orderer endpoint from the peer (if join was successful),
# lets supply it directly as we know it using the "-o" option
if [ -z "$CORE_PEER_TLS_ENABLED" -o "$CORE_PEER_TLS_ENABLED" = "false" ]; then
# -c 是调用的消息的构造函数的参数。后台基本上是用反射什么的来执行调用的。
peer chaincode invoke -o orderer.ORDERER_DOMAIN:7050 -C $CHANNEL_NAME -n mycc -c '{"Args":["invoke","a","b","10"]}' >&log.txt
else
peer chaincode invoke -o orderer.ORDERER_DOMAIN:7050 --tls $CORE_PEER_TLS_ENABLED --cafile $ORDERER_CA -C $CHANNEL_NAME -n mycc -c '{"Args":["invoke","a","b","10"]}' >&log.txt
fi
res=$?
cat log.txt
verifyResult $res "Invoke execution on PEER$PEER failed "
echo "===================== Invoke transaction on PEER$PEER on channel '$CHANNEL_NAME' is successful ===================== "
echo
}

## Create channel
echo "Creating channel..."
createChannel

## Join all the peers to the channel
echo "Having all peers join the channel..."
joinChannel

## Set the anchor peers for each org in the channel
echo "Updating anchor peers for org1..."
# 这两个函数调用,要仔细更改当前的核心环境变量为两个组织预先生成好的 anchor 节点的 endpoint。
updateAnchorPeers 0
echo "Updating anchor peers for org2..."
updateAnchorPeers 2

# 只在 peer0 和 peer2上安装合约
## Install chaincode on Peer0/Org1 and Peer2/Org2
echo "Installing chaincode on org1/peer0..."
installChaincode 0
echo "Install chaincode on org2/peer2..."
installChaincode 2

# 只在 peer2上初始化合约
#Instantiate chaincode on Peer2/Org2
echo "Instantiating chaincode on org2/peer2..."
instantiateChaincode 2

# 只在 peer0 上查询合约
#Query on chaincode on Peer0/Org1
echo "Querying chaincode on org1/peer0..."
chaincodeQuery 0 100

# 只在 peer0 上调用合约
#Invoke on chaincode on Peer0/Org1
echo "Sending invoke transaction on org1/peer0..."
chaincodeInvoke 0

# 在 peer3 上追加安装合约
## Install chaincode on Peer3/Org2
echo "Installing chaincode on org2/peer3..."
installChaincode 3

# 在 peer3 上追加查询合约。可见追加进来的合约调用结果可以被明确查询到。
#Query on chaincode on Peer3/Org2, check if the result is 90
echo "Querying chaincode on org2/peer3..."
chaincodeQuery 3 90

echo
echo "========= All GOOD, BYFN execution completed =========== "
echo

echo
echo " _____ _ _ ____ "
echo "| ____| | \ | | | _ \ "
echo "| _| | \| | | | | | "
echo "| |___ | |\ | | |_| | "
echo "|_____| |_| \_| |____/ "
echo

exit 0

4.6 bin 文件夹下的内容

1
2
3
4
5
configtxlator
get-byfn.sh
get-docker-images.sh
orderer
peer

其中 orderer、peer 都会被映射到镜像之内。

五、MSP 与身份治理

Fabric 要求所有的 participant 有相关的 identity。identity 是由 x509 证书认证的(大致上也就是各种 signcert),每个 identity 有自己的 principal,包含了大量的 property,包括但不仅限于组织名。

PKI 生成 identity,而 MSP 表达治理组织的规则,包括哪些 identity 属于哪些组织,且参与网络中。

5.1 PKI 基础

PKI 是一种标准,一般由四个元素组成:

  • Digital Certificates
  • Public and Private Keys
  • Certificate Authorities
  • Certificate Revocation Lists

5.1.1 数字证书

一个持有一个组织的系列属性的数字文件。常见的数字证书是 X509 标准的,很像一个国家发放的身份证(有名称,省份,国家,还有组织的名字)。

For example, John Doe of Accounting division in FOO Corporation in Detroit, Michigan might have a digital certificate with a SUBJECT attribute of C=US, ST=Michigan, L=Detroit, O=FOO Corporation, OU=Accounting, CN=John Doe /UID=123456.

注意,这些属性都是这个 party 的 subject attribute 的一部分。

注意看,这个 party 的公钥是这个证书的一部分,而私钥则不是。

因为有密码学加持,所以这个证书是抗篡改的,里面的 attribute 可以被认为是真的,只要别人信任这个证书的签发方的话。因为这个签发方用自己的私钥的方法把这些 attribute 加密写入(secrete writing)了这个证书。注意,签发证书的时候,csr 的请求者不需要提供自己的私钥。在 CA 那里,是证书公钥 + CA 私钥 + CA 证书(当然也包含了 CA 的公钥)来生成新的证书。

5.1.2 质询和公私钥

质询和消息完整性是安全通信的重要概念。私钥签名,公钥验证签名。

5.1.3 CA

现实中的 CA 是一系列权威。用自己的私钥给一个 party 的属性和公钥签名。因为 CA 是周知的权威,它的签名可以被周知的 CA 公钥验证。

在区块链里,我们当然希望一个或多个 CA 可以帮我们定义一个组织里有哪几个成员。

因为根 CA 的证书,可以帮我们验证某个证书是不是它签发的,所以我们可以通过根 CA 签发一个中间 CA 的证书,而这个中间 CA 一旦被验证了,它也可以产生它签发的证书。这就是 CA 构成的信任链的本质了。

换言之,任何一个证书,都可以拿来当 CA 证书来用。

做区块链世界里,理论上不同的组织可以由不同的根 CA 签发证书。Fabric 的 CA 不能拿来提供 SSL 证书,这点来看就不如 openssl 这样的工具了。而且,实际上我们也可以使用公共的权威 CA 来生成相关的组织证书。

CA 有证书撤销列表(Certificate Revocation Lists),验证的 party 要验证证书的时候,还要查看撤销列表(在线查看?离线怎么办?):

5.2 MSP 工作方式(Fabric v1.1 文档视角)

MSP 的工作方式无非有两个:

  • 列出一个组织里所有的成员
  • 列出一个 CA,声称由这个 CA 签发的证书都是这个组织的成员

MSP 的实际作用还超过于此,它还规定了很多角色和特权的细节,如 admin、普通 user、reader 和 writer(后面这两项是由某些 policy 决定的)。

按照 1.1 的文档,现在 Fabric 提倡一个组织(这里的组织不同于 x.509 标准里的组织)使用一个 MSP(这也符合 gossip 协议的要求)。比如 ORG1-MSP 之于 ORG1,特殊情况下才需要 ORG2-MSP-NATIONAL 和 ORG2-MSP-GOVERNMENT 之于 ORG2。

如上图可以看出,合理的 MSP 总是由不同的根 CA 经过若干个 ICA 单独生成的。

每个组织还可以再分成不同的 OU(organizational units),用不同的 OU 来区分不同的业务线。这个 OU 就是 X.509 证书里的那个 attribute。我们可以靠 OU 这个属性来做权限控制(实际上我们应该可以通过任何属性来做权限控制)。Fabric 的原话是,我们可以用同一套 CA 体系,靠不同的 OU 来区分不同的组织成员(为什么不直接用不同的 O ?)。

5.3 频道 MSP vs 本地 MSP

MSP 可以分为两类,频道 MSP(频道配置文件)和本地 MSP(节点或者 user 的 msp 文件夹是必须存在的)。这两种 MSP 定义了 outside channel level 和 channel level 的管理和参与权利。因为频道 MSP 包含了这个 channel 的组织成员配置,只有这些配置里包含进了这些组织的 MSP,这些组织才能参与到频道中。其关系图如下:

从这个图可以看出,每个组织的 MSP,实际上是全局 MSP 的一部分。

似乎频道 MSP 会被拷贝到各个 peer 的本地。这样的话是不是网络中的 MSP 变动(加入新的组织)的时候要动态地滚动重启整个网络呢?

高级的 MSP(频道)管理网络资源,包括加入联盟等问题,低级(本地)MSP 管理本地资源,包括这本地部署合约初始化合约等问题。

此外,MSP 还可以分为 Network MSP、Channel MSP、Peer MSP、Orderer MSP。

5.4 MSP 的目录结构

MSP 的组成部分无外乎,所以 MSP 实际上是一大堆文件夹组成的东西:

现实中的结构有:

这里没有把 user 也列出来,基本上除了本节点或者 user 私有的证书(包含了公钥)和 keystore(包含了私钥)以外,有大量的 ca 证书,管理员证书和 tls 证书是整个 org 共享的。注意,不同组织的 ca 证书是完全不同的,足以证明 cryptogen 这个工具生成的不同的组织的组织根 ca 还是不一样的。

5.5 Fabric v1.0 文档视角

MSP 把签发和验证证书用户质询(换言之 MSP 即使是在 peer/client 上也提供 authentication 的功能)的过程给抽象化了。MSP 可以完全定制化实体(identity)的记号,和这些实体被治理的规则。实际上对 Peer 而言,MSP 就是他们的一部分,可以被认为是会员相关操作的一部分。

一个 Fabric 区块链可以被一个或多个 MSP 统治。这提供了会员制操作的模块化,和不同会员制标准的互操作性。

在 Fabric 中,签名验证(signature verification)是 authentication。

每一个 peer 和 orderer 都需要局部设置 MSP 配置(实际上就是需要配置证书和 signing keys)。每个 MSP 必须有个 MSP ID 来引用这个 MSP。实际上如果我们仔细观察 crypto-config 这个文件夹,就会发现组织域、peer、admin 和一般 user,凡是有 identity 的,都有 msp 的配置。而 MSP 的全套配置必然包括:

  • admincerts
  • cacerts
  • tlscacerts

如果可以发起和验证签名,还需要有:

  • keystore 这个文件似乎不是私钥本身,而是私钥的集合大概是这种形式的:
1
2
3
4
5
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg3dCRAHW1PFdllpwe
9/KnhNopUClPY1FFB5cDNZbTzzyhRANCAAShU8bpfce3KuaVSlR3aqS/YowoiKAd
xFQgtR/IIb6+xEqTCcVFdS/qJF+HS1JlhaDqyMYfJC+C/FqudHeIBIae
-----END PRIVATE KEY-----
  • signcerts

用户可以使用 Openssl(这个大概是没人用的)、cryptogen 和 Hyperledger Fabric CA 来生成用来配置 MSP 的证书和 signing keys

在每个节点的配置文件里面(对 peer 节点是 core.yaml 对 orderer 节点则是 orderer.yaml)指定 mspconfig 文件夹的路径和节点的 MSP 的 ID。mspconfig 文件夹的路径必须是相对于 FABRIC_CFG_PATH 的路径,在 peer 上用 mspConfigPath 参数表示,在 orderer 上用 LocalMSPDir 参数表示。MSP ID 在 peer 上用 localMspId 表示,在 orderer 上用 LocalMSPID 表示(注意大小写不同)。这些参数可以被环境变量给覆写掉,对于 peer 用 CORE_PEER_LOCALMSPID,对于 orderer 用 ORDERER_GENERAL_LOCALMSPID。当 orderer 启动了以后,必须把"系统 channel"的创世区块提供给 orderer。

只有手动操作才能重新配置一个"本地"MSP,而且要求那个 peer 或者 orderer 进程重启。未来可能会提供在线动态的重配置。

5.6 NodeOUs 与组织化单元

在 mspconfig 文件夹下可以用一个 config.yaml 文件来定义组织化单元。大概是这样:

1
2
3
4
5
OrganizationalUnitIdentifiers:
- Certificate: "cacerts/cacert1.pem"
OrganizationalUnitIdentifier: "commercial"
- Certificate: "cacerts/cacert2.pem"
OrganizationalUnitIdentifier: "administrators"

这些 ou 的定义是可选的,是为了在组织内再进一步按照业务线分权。默认的情况下没有的话,组织里的 identity 就不再进一步分权。

有一种特殊的分类方法,把签发事务、查询 peer 的 identity 称作 client,而把背书和提交事务的节点被称作 peer。问题是默认的情况下一个 peer 容器就是 peer 了,也不需要专门的 config 来配置。所以我想应该只是专门使用 client 来区分、鉴别 identity。

1
2
3
4
5
6
7
8
9
10
NodeOUs:
Enable: true
ClientOUIdentifier:
Certificate: "cacerts/cacert.pem"
# 不同的 OUIdentifier 都需要这个字段。MSP 的 administrator 还必须是 client
OrganizationalUnitIdentifier: "client"
PeerOUIdentifier:
Certificate: "cacerts/cacert.pem"

OrganizationalUnitIdentifier: "peer"

正如我们前面看到的,orderer 在启动的时候,需要得到一个"系统频道"的创世区块,这个创世区块必须包含全网络中出现的所有 MSP 的验证参数。这些验证参数包括:“verification parameters consist of the MSP identifier, the root of trust certificates, intermediate CA and admin certificates, as well as OU specifications and CRLs”。这样 orderer 才能处理频道创建请求(channel creation requests)。

而对于应用程序频道而言,只有治理那个频道的 MSP 的验证组件必须被包含在那个频道的创世区块中。在 peer 加入频道以前,应用程序有义务把正确的 MSP 配置信息包含在创世区块之中。

要修改一个频道里的 MSP,要一个有那个频道 administrator 证书的拥有者,创建一个 config_update 对象,然后把这个对象在频道里公布。这是通过 configuration block 来实现的吗?

网络中还可能存在大量的 intermediate CAs。

5.7 MSP 最佳实践

  • 一个组织拥有多个 MSP。这是一个组织下面有多个分支机构的时候才会用到的设计方法
  • 多个组织共用一个 MSP。什么情况下会让一个联盟中的多个组织共用一个 MSP 呢?除非它们极为相似吧
  • client 要和 peers 分开。怎么做到的呢?
  • 要有独立的 admin 和 CA 证书
  • 把中间 CA 放到黑名单里面
  • CA 和 TLS CA 放在不同的文件夹下

六、容器内部环境

6.1 peer 容器

6.1.1 /opt/gopath/src/github.com/hyperledger/fabric/peer

虽然是 WORKING_DIR,什么都没有。这个目录是 /bin/bash 永远的进入路径,不管在哪个目录退出,重新进入还是会进入这个路径。

6.1.2 /etc/hyperledger/fabric

1
2
3
4
5
6
7
8
9
10
# 原生的三个配置文件。所以修改peer的行为要通过环境变量来修改,让docker用COMMAND启动peer进程的时候吸收这几个配置文件和环境变量
core.yaml

# 这两个文件似乎不关peer的事情
configtx.yaml
orderer.yaml
# 这两个文件夹要被外部的数据卷映射修改过来,实际上只能依赖于外部
# 这个文件夹本质上还是 core.yaml 默认的 mspConfigPath 的值
msp
tls

6.1.3 /var/hyperledger/production

这个文件夹存放 unix 系统里面的动态程序数据。

1
2
3
4
5
6
# 它下面有打包好的CIP(chaincode install package)格式的链码 chaincodes/mycc.1.0。
chaincodes
# 这个文件夹就特别像以太坊的datadir了。
ledgersData
# 因为这是系统中唯一的进程,所以它的进程号是1,是不是docker内的主进程都是这样?
peer.pid

6.1.4 启动命令

整个容器内只有一个进程 peer node start。完全没有其他命令行参数,所以就是靠环境变量来支持。

这个启动命令被安装只 /usr/local/bin/ 下,里面只有这个命令(精简的 ubuntu 系统)。

6.1.5 全部环境变量

env 命令可以打出来:

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
HOSTNAME=fba1d49eb609
TERM=xterm
CORE_PEER_TLS_ROOTCERT_FILE=/etc/hyperledger/fabric/tls/ca.crt
CORE_PEER_TLS_KEY_FILE=/etc/hyperledger/fabric/tls/server.key
# 省略 LS_COLORS
CORE_PEER_PROFILE_ENABLED=true
CORE_PEER_GOSSIP_ORGLEADER=false
CORE_PEER_LOCALMSPID=Org3-MSP
CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
CORE_PEER_TLS_CERT_FILE=/etc/hyperledger/fabric/tls/server.crt
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/var/hyperledger/production
CORE_PEER_TLS_ENABLED=true
CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=net_byfn
CORE_PEER_ID=peer0.ORG3_DOMAIN
SHLVL=1
HOME=/root
CORE_LOGGING_LEVEL=DEBUG
CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.ORG3_DOMAIN:7051
FABRIC_CFG_PATH=/etc/hyperledger/fabric
CORE_PEER_ADDRESS=peer0.ORG3_DOMAIN:7051
CORE_PEER_GOSSIP_USELEADERELECTION=true
CORE_PEER_GOSSIP_BOOTSTRAP=peer0.ORG3_DOMAIN:7051
_=/usr/bin/env
OLDPWD=/etc/hyperledger/fabric

6.2 orderer 容器

6.2.1 /opt/gopath/src/github.com/hyperledger/fabric

虽然是 WORKING_DIR,什么都没有。这个目录是 /bin/bash 永远的进入路径,不管在哪个目录退出,重新进入还是会进入这个路径。

注意,这个目录没有 peer 子目录。

6.2.2 /etc/hyperledger/fabric

1
2
3
4
5
6
7
# 同 peer 容器
core.yaml
configtx.yaml
orderer.yaml
# 没有tls文件夹
# 这里的msp似乎没什么用,var下的msp才是有用的。
msp

6.2.3 /var/hyperledger/

这个文件夹下有专门的两个子文件夹。

1
2
3
# peer 容器没有单独的peer文件夹,直接就是production
orderer
production

orderer 文件夹下有三个文件:

1
2
3
4
5
# 这个创世区块是外部映射进来的。
orderer.genesis.block
# 这两个文件夹也是外部映射进来的,却不是映射到etc而是映射到这里,不知道为什么
msp
tls

production 文件夹下还有一个 orderer 文件夹:

1
2
3
4
5
# 类似peer存放链数据了,但peer又没有一个单独的production/peer文件夹
chains
index

# 没有 orderer 的pid

6.2.4 启动命令

orderer 连启动参数都没有。直接启动就会遇到地址被占用的错误。这个启动命令被安装只 /usr/local/bin/ 下,里面只有这个命令(精简的 ubuntu 系统)。

6.2.5 全部环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 专门指定了本地 msp 目录。
ORDERER_GENERAL_LOCALMSPDIR=/var/hyperledger/orderer/msp
HOSTNAME=bf4847ae1253
ORDERER_GENERAL_GENESISFILE=/var/hyperledger/orderer/orderer.genesis.block
TERM=xterm
# 省略 LS_COLORS
ORDERER_GENERAL_LOCALMSPID=OrdererMSP
ORDERER_GENERAL_TLS_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
ORDERER_GENERAL_LISTENADDRESS=0.0.0.0
ORDERER_GENERAL_TLS_PRIVATEKEY=/var/hyperledger/orderer/tls/server.key
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/opt/gopath/src/github.com/hyperledger/fabric
ORDERER_GENERAL_LOGLEVEL=debug
ORDERER_GENERAL_GENESISMETHOD=file
ORDERER_GENERAL_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/server.crt
SHLVL=1
HOME=/root
FABRIC_CFG_PATH=/etc/hyperledger/fabric
ORDERER_GENERAL_TLS_ENABLED=true
_=/usr/bin/env

6.3 cli 容器

6.3.1 /opt/gopath/src/github.com/hyperledger/fabric/peer

cli 容器的这个工作目录倒是有很多文件了:

1
2
3
4
5
6
7
8
9
# 重定向到控制台
log.txt
# 这个频道的channel创世区块,应该是在这个工作目录下由 createChannel() 函数创建出来的
mychannel.block
# 这3个文件夹就是从外部映射进来的
# 这个文件夹在这个相对路径下,就可以让peer channel 来生成新的频道
channel-artifacts
crypto
scripts

6.3.2 /opt/gopath/src/github.com/hyperledger/fabric/examples

cli 容器独有的放置链码的文件夹,也是外部的 docker 映射进来的。

6.3.3 /etc/hyperledger/fabric

这个文件夹可以说也是从标准环境中诞生出来的(标准镜像里就有这些文件夹了么?)

1
2
3
4
5
configtx.yaml  
core.yaml
# 也没人映射进来
msp
orderer.yaml

6.3.4 /var/hyperledger/

没有任何内容,没有 production 数据需要单独存放。

6.3.5 环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
HOSTNAME=cf5db270ec10
TERM=xterm
CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/ORG1_DOMAIN/peers/peer0.ORG1_DOMAIN/tls/ca.crt
CORE_PEER_TLS_KEY_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/ORG1_DOMAIN/peers/peer0.ORG1_DOMAIN/tls/server.key
# 省略 LS_COLORS
CORE_PEER_LOCALMSPID=Org1MSP
CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
CORE_PEER_TLS_CERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/ORG1_DOMAIN/peers/peer0.ORG1_DOMAIN/tls/server.crt
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/go/bin:/opt/gopath/bin
PWD=/opt/gopath/src/github.com/hyperledger/fabric/peer
CORE_PEER_TLS_ENABLED=true
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/ORG1_DOMAIN/users/Admin@ORG1_DOMAIN/msp
CORE_PEER_ID=cli
SHLVL=1
HOME=/root
GOROOT=/opt/go
CORE_LOGGING_LEVEL=DEBUG
FABRIC_CFG_PATH=/etc/hyperledger/fabric
CORE_PEER_ADDRESS=peer0.ORG1_DOMAIN:7051
LESSOPEN=| /usr/bin/lesspipe %s
GOPATH=/opt/gopath
LESSCLOSE=/usr/bin/lesspipe %s %s
_=/usr/bin/env

6.4 链码容器

6.4.1 /etc/hyperledger/fabric/

只有一个 peer.crt 文件,应该是在生成这个镜像的时候,从特定的 peer 上拷贝过来的。

没有 /opt 下的 gopath,也没有 /var 下的 production 目录。

6.4.2 启动命令

只有一个启动命令:

1
2
3
4
"Path": "chaincode",
"Args": [
"-peer.address=peer1.ORG3_DOMAIN:7051"
],

/usr/local/bin/ 下只安装了一个命令 chaincode

6.4.3 环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HOSTNAME=a2886170947f
TERM=xterm
CORE_PEER_TLS_ROOTCERT_FILE=/etc/hyperledger/fabric/peer.crt
# 省略 LS_COLORS
CORE_CHAINCODE_ID_NAME=mycc:1.0
CORE_CHAINCODE_LOGGING_LEVEL=info
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
CORE_PEER_TLS_ENABLED=true
CORE_CHAINCODE_BUILDLEVEL=1.0.6
CORE_CHAINCODE_LOGGING_FORMAT=%{color}%{time:2006-01-02 15:04:05.000 MST} [%{module}] %{shortfunc} -> %{level:.4s} %{id:03x}%{color:reset} %{message}
SHLVL=1
HOME=/root
CORE_CHAINCODE_LOGGING_SHIM=warning
_=/usr/bin/env

七、chaincode 示例:保险合约

以下是一段改编自 IBM 源码示例的保险合约链码,演示 Init / Invoke / Get / Set / GetHistory 的写法。

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
/*
* 改编自 IBM 的源码示例。
*/

package main

import (
"bytes"
"fmt"
"github.com/hyperledger/fabric/core/chaincode/shim"
"github.com/hyperledger/fabric/protos/peer"
"encoding/json"
"strconv"
"time"
)


// Define the insurance structure, with 2 properties. Structure tags are used by encoding/json library
type Insurance struct {
Owner string `json:"owner"`
Money string `json:"money"`
}

// 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 *Insurance) 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 owner and money")
}

// create new insurance object and marshal it into json.
insuranceAsBytes, _ := json.Marshal(Insurance{args[0], args[1]})
// 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], insuranceAsBytes)
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 *Insurance) 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 if fn == "getHistory" {
result, err = getHistory(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")
}

insuranceAsBytes, _ := json.Marshal(Insurance{args[0], args[1]})
err := stub.PutState(args[0], insuranceAsBytes)
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
}

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

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

defer resultsIterator.Close()

// buffer is a JSON array containing historic values for the marble
var buffer bytes.Buffer
buffer.WriteString("[")

bArrayMemberAlreadyWritten := false
for resultsIterator.HasNext() {
response, err := resultsIterator.Next()
if err != nil {
return "iterating error", err
}
// Add a comma before array members, suppress it for the first array member
if bArrayMemberAlreadyWritten == true {
buffer.WriteString(",")
}
buffer.WriteString("{\"TxId\":")
buffer.WriteString("\"")
buffer.WriteString(response.TxId)
buffer.WriteString("\"")

buffer.WriteString(", \"Value\":")
// if it was a delete operation on given key, then we need to set the
//corresponding value null. Else, we will write the response.Value
//as-is (as the Value itself a JSON marble)
if response.IsDelete {
buffer.WriteString("null")
} else {
buffer.WriteString(string(response.Value))
}

buffer.WriteString(", \"Timestamp\":")
buffer.WriteString("\"")
buffer.WriteString(time.Unix(response.Timestamp.Seconds, int64(response.Timestamp.Nanos)).String())
buffer.WriteString("\"")

buffer.WriteString(", \"IsDelete\":")
buffer.WriteString("\"")
buffer.WriteString(strconv.FormatBool(response.IsDelete))
buffer.WriteString("\"")

buffer.WriteString("}")
bArrayMemberAlreadyWritten = true
}
buffer.WriteString("]")

fmt.Printf("- getHistoryForMarble returning:\n%s\n", buffer.String())

return buffer.String(), nil
}



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

八、周边工具

8.1 Cello 在 macOS 上的注意事项

  1. /opt/cello/opt/cello2 以及下面的 mongo 和 fabric-1.0 都尽量把 owner 和 group 改成当前用户名和当前用户名组名。然后把这两个子文件夹用 docker file sharing 打开

  2. docker 会对 cello 下的 file sharing 有很麻烦的冲突影响。解决方法是建立一个 cello2 文件夹,然后在 cello 项目下用以下脚本把 fabric-1.0 的文件夹迁移过去:

1
2
3
4
#!/usr/bin/env bash
ARTIFACTS_DIR=/opt/cello2
sudo cp -r ./src/agent/docker/_compose_files/fabric-1.0 ${ARTIFACTS_DIR}
sudo chown -R ${USER}:${USERGROUP} ${ARTIFACTS_DIR}

然后修改 vi .//src/agent/docker/docker_swarm.py,把 COMPOSE_PROJECT_PATH 改为:'COMPOSE_PROJECT_PATH': '/opt/cello2/fabric-1.0/local'。以保证 ./src/agent/docker/_compose_files/fabric-1.0/local/docker-compose-base.yamlvi ./src/agent/docker/_compose_files/fabric-1.0/local/fabric-solo-4.yaml 可以正确地重新 mount 上该目录。

  1. 修改防火墙打开 ip 转发:
1
sudo sysctl -w net.inet.ip.forwarding=1
  1. 通常,Cello 网络的 channel id 是 businesschannel 或者 testchainid

8.2 把 composer 锚定到 e2e 的复杂网络

  1. 下载并进入 /blockchain-explorer 项目
  2. cd fabric-docker-compose-svt
  3. download_images.sh(不要让 start.sh 代劳,要用特定的版本)
  4. ./start.sh
  5. 建立这个隐藏文件夹 /Users/magicliang/.composer-credentials,如果里面有内容要先清空
  6. 把公私钥导入这个文件夹,注意最后一个 keystore 的私钥可能会变化:
1
composer identity import -p explorer -u PeerAdmin -c /Users/magicliang/Desktop/Programming/git/blockchain-explorer-cp/fabric-docker-compose-svt/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/signcerts/Admin@org1.example.com-cert.pem -k /Users/magicliang/Desktop/Programming/git/blockchain-explorer-cp/fabric-docker-compose-svt/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/keystore/b28b23d7887f9257884953470d04b6207c9187878ad758807505cf3cfe9ea8ee_s
  1. 在已经制作好的网络的 dist 文件夹下执行:
1
composer network deploy -a charity-network.bna -p explorer -i PeerAdmin -s randomString
  1. 开启 compose-rest-server:
1
2
3
4
5
6
7
8
9
$ composer-rest-server
? Enter your Connection Profile Name: explorer
? Enter your Business Network name : charity-network
? Enter your enrollment ID : admin
? Enter your enrollment secret : adminpw
? Specify if you want namespaces in the generated REST API: never use namespaces
? Specify if you want to enable authentication for the REST API using Passport: No
? Specify if you want to enable event publication over WebSockets: Yes
? Specify if you want to enable TLS security for the REST API: No

九、其他要点

9.1 CA 问题

CA 是可选组件。

但如果 CA server 跑起来,我们可以向它发送 REST 请求给组织的 CA 来完成用户注册和登记。

CA 是 PKI 的实现。PKI 可以抗女巫攻击。

CA 可以像 cryptogen 一样,先生成组织里的节点证书和私钥(组织里一定会有 root-ca),然后生成成员(admin 和普通成员)的证书和私钥。

9.2 常见端口号

这些端口号有先后顺序,适合在同一个 host 编排一系列容器。

  • orderer:7050
  • peer:7051
  • peer event:7053
  • ca:7054

9.3 SDK 问题

区块链天然可以用 gRPC 来通信。

因为各种语言都支持 Protocol Buffers,基于 Protocol Buffers over gRPC,我们可以制造各种语言的 SDK。

所以理论上我们可以直接通过 gRPC 跟区块链通信。

9.4 configuration block

有 configtx,就有 configuration block,这不奇怪。两者都要经过 orderer。

可以通过命令行来获取 configuration(peer channel fetch)。peer channel fetch 可以用来获取任意序号的区块。

9.5 packaging 与签名

1
peer chaincode package -n mycc -p github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02 -v 0 -s -S -i "AND('OrgA.admin')" ccpack.out

这一行命令生成了一个 signedCDS。

-s(signing 的意思)应该有 -S,否则再也没有机会让 owner 来签署这个 package 了。-S 指出了需要本地 MSP 来 sign 这个 package。

-i 是 intantiate policy,指的是只有 OrgA 的 admin 可以初始化这个 package。

实际上不用 package 和 sign 最好了,反正每个 peer 默认可以初始化链码也只有本 Member 的 admin(这种 admin 在默认的情况下是大多数需要产生变化的 writer)。

可见这个初始化策略是针对 package 而不是针对 channel 的。我们只可以组织一个 package 被初始化,不能阻止别人在一个 channel 上安装任意的 chaincode。

这个初始化策略还会进一步限制 upgrade。只有符合策略的 MSP 才能 upgrade 整个链码。

9.6 停止链码

目前只能通过停止和删除容器,并删除 peer 上 CDS 的方式来做到,对其他组织的机器是个考验呢。

1
2
docker rm -f <container id>
rm /var/hyperledger/production/chaincodes/<ccname>:<ccversion>

9.7 系统链码

逻辑上和普通链码是一样的(应该也是通过各种事务驱动的),但只跑在 peer 进程内。

系统链码的目的是为了减少 peer 和 chaincode 容器之间的 gRPC。

要 upgrade 链码只有 upgrade peer 二进制文件(实际上就是跟着 Fabric 版本走)。

9.8 架构流程图