[关闭]
@czyczk 2022-05-20T07:57:08.000000Z 字数 78997 阅读 2356

Hyperledger Fabric 全流程慢通

Hyperledger_Fabric


2022-05-19 免责声明:

文中为确保网络连通性和访问速度,部分采用依赖 Gitee 仓库的方式。由于 Gitee 变更开源策略,该部分仓库可能已变更为仅限登录 Gitee 的用户查看,本人将不确保这些仓库的可用性。



0. 大纲

本流程将包含以下内容:


1. Linux 环境准备

Hyperledger Fabric 虽然官方说可以支持 Windows,但其支持只是支持表面。配置和运行过程中用到的各种可执行文件还都是为 Linux 写的,因此就算按着官方在 Windows 下的教程,最终还是需要经历模拟,使用便利性和运行效率都大打折扣。

因此还是需要准备一个 Linux 环境,本流程可适用于在实机和虚拟机中安装的 Linux,以及 WSL 2。Linux 发行版将覆盖 Ubuntu 18.04, Ubuntu 20.04 以及 Manjaro 20.0.3。

对于 Windows 10 用户推荐使用 WSL 2 方法。WSL 全称是 Windows Subsystem Linux,即在 Windows 中作为子系统的 Linux。它在拥有一个完整的 Linux 内核的同时与 Windows 整合良好,因此可以利用完整的 Linux 与 Windows 生态应用共同帮助开发。举例来说,它可以与 Windows 之间可以进行无缝的文件互访,方便文件在 Windows 和子系统 Linux 之间传输(未来版本甚至可以直接在“此电脑”里看到 WSL 映射出来的驱动器);可以使用 VSCode 编辑 WSL 中的工程文件;WSL 开放的端口可以被 Windows 使用等等。此外,由于省去图形界面,不会占用多余的内存,对计算机额外的性能要求很低。综上,WSL 2 在开发和测试上相比于使用 VMWare 等虚拟机安装 Linux 系统的方法有着更大的便利性。

需要使用 WSL 2 的进行到下面的子节,已经有可用的 Linux 环境的可跳过,直接进入 1.2 子节。

1.1 WSL 2

注:使用 WSL 2 需要开启 Windows 自带的虚拟机功能,可能导致 VMWare Workstation 和 Virtualbox 不可用,将 VMWare Workstation 升级到最新版本可免除此影响。若因情况无法依此法解决请慎用 WSL 2。

 

注:WSL 2 不同于 WSL,前者才拥有完整的 Linux 内核,可以顺利运行 Docker。使用 WSL 2 需要 Windows 10 18362.1049 及以上。该构建号可以通过运行 winver 查看。在写作时(2020 年 9 月 10 日)建议通过安装 Windows 10 Update Assistant 将系统升级到 build 19041。升级时长根据计算机性能与当前运行的版本不同而异,通常在 15 分钟到 1 小时之间。

首先确保 Windows 版本是 Windows 10 18362.1049 及以上,例如 19041,以确保 Docker 可以正常运行。

打开拥有管理员权限的 PowerShell 窗口(Win + X,选择“Windows Powershell (管理员)”)。

执行以下命令:
启用 WSL 功能:

  1. dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart

启用 Windows 自带的虚拟机平台:

  1. dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

重启电脑以确保可以正常进行。

再次打开带管理员权限的 PowerShell,输入以下命令以使新安装的 Linux 发行版以 WSL 2 安装而非 WSL。

  1. wsl --set-default-version 2

注:此步执行后出现的提示若包含网址 https://aka.ms/wsl2kernel,访问并下载一个 Linux 内核的更新包,很快。

注:当前(2020 年 9 月 17 日)该链接的中文页面存在问题,请直接访问英文页面 https://docs.microsoft.com/en-us/windows/wsl/install-win10#step-4---download-the-linux-kernel-update-package)。

完成后进入微软商店,搜索 Ubuntu,选择 Ubuntu(写作时间 2020 年 9 月 10 日亦为 20.04)、Ubuntu 18.04 或 Ubuntu 20.04 安装均可。

若是此前使用 WSL 安装过 Ubuntu 的,可以通过

  1. wsl -l -v
  2. wsl --set-version <发行版名称> 2

将指定实例转换为 WSL 2。

安装完成后在开始菜单搜索“Ubuntu”即可打开。第一次运行需要简单的配置,记住自己的用户名和密码即可。

1.2 配置 Linux 环境

此小节包含国内镜像的配置,以及前置软件的安装,此外还有些和主线无关的可选内容也是为了尽可能提高下载速度。若有办法在 Linux 环境中翻墙的可以选择跳过镜像配置的部分以及与主线无关的可选内容。

以下是针对不同的发行版所需要的前置程序清单,在后续的小节中将会带领一步步进行安装。

对于 Ubuntu 发行版,需要的前置程序包有:

其中 golang 需要使用额外的 PPA 来保证版本足够新(特别是对于 Ubuntu 18.04 来说是必需的)。

对于 Manjaro 与其他基于 Arch 的发行版,需要的前置程序包有:

已经装有相应程序包的可以跳过相关小节的安装部分,但若存在有配置(特别是国内镜像配置)的部分请尽量阅读。

1.2.1 (Ubuntu)设置 apt-get 的国内镜像

这一小节是给 Ubuntu 发行版专用,后续标题上有这类括号指示的也是如此。

这步毋庸置疑是第一步。许多软件都需要通过 apt 来安装,这步没做干啥都慢。

使用 vim 或 nano 修改文件 /etc/apt/sources.list,删除其中已有的内容并根据 Ubuntu 版本加入下文代码框内的内容。

若是初次使用 vim 可依照下列步骤进行操作。

  1. sudo vim /etc/apt/sources.list 以打开文件
  2. 直接敲击 ggdG 以清空所有内容(注意大小写)
  3. :set paste 以设置为粘贴模式(会在最后一行显示正在打的命令)
  4. i 进入插入模式(左下角应显示“INSERT (paste)”)
  5. 复制下面相应版本的代码框内的内容
  6. 粘贴进 vim(根据使用的终端不同,可能是 Ctrl + Shift + V 也可能是右键)
  7. esc 回到命令模式
  8. :wq 保存并退出

 

注:不确定 Ubuntu 版本可使用 lsb_release -a 查看。
注:本示例使用阿里云源,在有 IPv6 访问的情况下校园网免流。

18.04:

  1. deb http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse
  2. deb-src http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse
  3. deb http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse
  4. deb-src http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse
  5. deb http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse
  6. deb-src http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse
  7. deb http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse
  8. deb-src http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse
  9. deb http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse
  10. deb-src http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse

20.04:

  1. deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
  2. deb-src http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
  3. deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
  4. deb-src http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
  5. deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
  6. deb-src http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
  7. deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
  8. deb-src http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
  9. deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
  10. deb-src http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse

修改完成保存退出后,使用 sudo apt update 确保应用了最新的配置。
使用 sudo apt upgrade -y 进行一次整体更新,确保后续不会出问题。

1.2.2 (Manjaro)设置 pacman 与 archlinuxcn 镜像

这一小节是给 Manjaro 发行版专用,后续标题上有这类括号指示的也是如此。

  1. sudo pacman-mirrors -i -c China -m rank

然后在 /etc/pacman.d/mirrorlist 里再调整优先级或注释掉不用的,例如 ustc 不好使时,可用 tsinghua 或 sjtu 源。

mirrorlist 可用源(可根据需要注释或反注释相应源):

  1. ## Country : China
  2. #Server = https://mirrors.ustc.edu.cn/manjaro/stable/$repo/$arch
  3. ## Country : China
  4. #Server = https://mirrors.tuna.tsinghua.edu.cn/manjaro/stable/$repo/$arch
  5. ## Country : China
  6. Server = https://mirrors.aliyun.com/manjaro/stable/$repo/$arch

/etc/pacman.conf 文件末尾添加以下行(可根据需要注释和反注释相应的源):

  1. [archlinuxcn]
  2. SigLevel = Optional TrustedOnly
  3. #Server = https://mirrors.ustc.edu.cn/archlinuxcn/$arch
  4. #Server = https://repo.archlinuxcn.org/$arch
  5. #Server = https://mirrors.zju.edu.cn/archlinuxcn/$arch
  6. Server = https://mirrors.aliyun.com/archlinuxcn/$arch

然后安装 archlinuxcn-keyring 防止 GPG 密钥错误。

  1. sudo pacman -S archlinuxcn-keyring

以上大学和阿里云提供的镜像在有 IPv6 访问的情况下,校园网免流。
使用 sudo pacman -Syyu 进行一次整体更新,确保后续步骤不出问题。

1.2.3 安装 Vim

WSL Ubuntu 通常已自带,可跳过。

Ubuntu:

  1. sudo apt install vim -y

Manjaro:

  1. sudo pacman -S vim --needed

  1. yay -S vim --needed

后续不再指出包管理器使用上的差异,以 pacman 为主。

初次使用者看

这是一个强大的命令行编辑器,操作方式跟普通编辑器不太一样。
初次使用者需要记住的最基础的就是进入时处于命令模式,这时 j, k 分别是光标下、上,h, l 分别是光标左、右。
ai进入插入模式,这时输入方式和一般编辑器大体相同。
处在插入模式时按 esc退回命令模式
保存并退出是在命令模式下输入 :wq(注意全程半角英文),不保存直接退出是 :q!

1.2.4 (Ubuntu 可选)安装 apt-fast

此小节是针对 Ubuntu 的可选小节,为下一小节安装 Go 作准备。因为计划使用额外的 PPA 来安装 Go,在国内直连该境外服务器速度极慢,而 apt-fast 可以多线程下载。

  1. sudo add-apt-repository ppa:apt-fast/stable
  2. sudo apt update
  3. sudo apt install apt-fast -y

安装过程中会有三个提问。
第一个是使用的包管理器,选择“apt-get”或“apt”均可。

第二个是加速的关键,在这里指定一个最大连接数使得它可以向同一个地址发起多个连接同时下载。例如填入 16、24 或 32 均可。

这里是在调用具体包管理器之前额外的确认对话框,选择“YES”来禁止弹出即可。

1.2.5 安装 Go

Ubuntu:
由于官方源自带的 Go 版本过旧(特别是 18.04),为避免在后续编码过程中遇到不支持的新特性,建议使用第三方 PPA 源来安装。由于是从第三方源而来,没有国内镜像,故使用 apt-fast 来多线程加速下载。

  1. sudo add-apt-repository ppa:longsleep/golang-backports
  2. sudo apt update
  3. sudo apt-fast install golang-go -y

注意第三步使用了 apt-fast 来下载。

Manjaro:

  1. sudo pacman -S go --needed

安装完成后运行

  1. go version

来确定安装成功。

设置 GOPATH 与 Go 的国内镜像

使用 vim 修改 ~/.bashrc(对于 ZSH 用户是 ~/.zshrc),在文件最后添加上以下行。

  1. # Golang
  2. export GOPATH=$HOME/go
  3. export PATH=$PATH:$GOPATH/bin
  4. export GO111MODULE=on
  5. export GOPROXY=https://goproxy.cn

前两项是 Go 语言常需用到的环境变量,后两项用于应用国内镜像,使得下载包时能快些。

1.2.6 安装 Docker

Ubuntu:

  1. sudo apt install docker.io docker-compose -y

Manjaro:

  1. sudo pacman -S docker docker-compose --needed

Ubuntu 事宜

具体情况可根据发行版不同而变化,具体还应该按如下方法来检测。当前(2020 年 9 月 14 日)官方源中提供的 Docker 在 /etc/init.d 目录下缺少 Docker 服务的启动文件,使得无法通过 systemctlservice 启动 Docker 服务。

运行 ls /etc/init.d | grep docker 来检查目录下是否有该文件,若输出结果为空,则需要按下文解决;否则可跳过此小节。

解决方法分为两步:

第一步:/etc/init.d 目录下创建文件 docker,并使用 vim 打开

  1. sudo vim /etc/init.d/docker

并添加如下内容(进入后直接输入 :set paste 进入粘贴模式,以避免粘贴后格式混乱,然后按 i 进入插入模式,将下面内容粘贴后按 esc 回到命令模式,输入 :wq 保存并退出)。

  1. #!/bin/sh
  2. set -e
  3. ### BEGIN INIT INFO
  4. # Provides: docker
  5. # Required-Start: $syslog $remote_fs
  6. # Required-Stop: $syslog $remote_fs
  7. # Should-Start: cgroupfs-mount cgroup-lite
  8. # Should-Stop: cgroupfs-mount cgroup-lite
  9. # Default-Start: 2 3 4 5
  10. # Default-Stop: 0 1 6
  11. # Short-Description: Create lightweight, portable, self-sufficient containers.
  12. # Description:
  13. # Docker is an open-source project to easily create lightweight, portable,
  14. # self-sufficient containers from any application. The same container that a
  15. # developer builds and tests on a laptop can run at scale, in production, on
  16. # VMs, bare metal, OpenStack clusters, public clouds and more.
  17. ### END INIT INFO
  18. export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
  19. BASE=docker
  20. # modify these in /etc/default/$BASE (/etc/default/docker)
  21. DOCKERD=/usr/bin/dockerd
  22. # This is the pid file managed by docker itself
  23. DOCKER_PIDFILE=/var/run/$BASE.pid
  24. # This is the pid file created/managed by start-stop-daemon
  25. DOCKER_SSD_PIDFILE=/var/run/$BASE-ssd.pid
  26. DOCKER_LOGFILE=/var/log/$BASE.log
  27. DOCKER_OPTS=
  28. DOCKER_DESC="Docker"
  29. # Get lsb functions
  30. . /lib/lsb/init-functions
  31. if [ -f /etc/default/$BASE ]; then
  32. . /etc/default/$BASE
  33. fi
  34. # Check docker is present
  35. if [ ! -x $DOCKERD ]; then
  36. log_failure_msg "$DOCKERD not present or not executable"
  37. exit 1
  38. fi
  39. check_init() {
  40. # see also init_is_upstart in /lib/lsb/init-functions (which isn't available in Ubuntu 12.04, or we'd use it directly)
  41. if [ -x /sbin/initctl ] && /sbin/initctl version 2>/dev/null | grep -q upstart; then
  42. log_failure_msg "$DOCKER_DESC is managed via upstart, try using service $BASE $1"
  43. exit 1
  44. fi
  45. }
  46. fail_unless_root() {
  47. if [ "$(id -u)" != '0' ]; then
  48. log_failure_msg "$DOCKER_DESC must be run as root"
  49. exit 1
  50. fi
  51. }
  52. devicemapper_umount() {
  53. # Cleanup any stale mounts left from previous shutdown
  54. # see https://bugs.launchpad.net/ubuntu/+source/docker.io/+bug/1404300
  55. grep "mapper/docker" /proc/mounts | awk '{ print $2 }' | \
  56. xargs -r umount || true
  57. }
  58. case "$1" in
  59. start)
  60. check_init
  61. fail_unless_root
  62. devicemapper_umount
  63. touch "$DOCKER_LOGFILE"
  64. chgrp docker "$DOCKER_LOGFILE"
  65. ulimit -n 1048576
  66. # Having non-zero limits causes performance problems due to accounting overhead
  67. # in the kernel. We recommend using cgroups to do container-local accounting.
  68. if [ "$BASH" ]; then
  69. ulimit -u unlimited
  70. else
  71. ulimit -p unlimited
  72. fi
  73. log_begin_msg "Starting $DOCKER_DESC: $BASE"
  74. start-stop-daemon --start --background \
  75. --no-close \
  76. --exec "$DOCKERD" \
  77. --pidfile "$DOCKER_SSD_PIDFILE" \
  78. --make-pidfile \
  79. -- \
  80. -p "$DOCKER_PIDFILE" \
  81. $DOCKER_OPTS \
  82. >> "$DOCKER_LOGFILE" 2>&1
  83. log_end_msg $?
  84. ;;
  85. stop)
  86. check_init
  87. fail_unless_root
  88. if [ -f "$DOCKER_SSD_PIDFILE" ]; then
  89. log_begin_msg "Stopping $DOCKER_DESC: $BASE"
  90. start-stop-daemon --stop --pidfile "$DOCKER_SSD_PIDFILE" --retry 10
  91. log_end_msg $?
  92. else
  93. log_warning_msg "Docker already stopped - file $DOCKER_SSD_PIDFILE not found."
  94. fi
  95. ;;
  96. restart)
  97. check_init
  98. fail_unless_root
  99. docker_pid=`cat "$DOCKER_SSD_PIDFILE" 2>/dev/null`
  100. [ -n "$docker_pid" ] \
  101. && ps -p $docker_pid > /dev/null 2>&1 \
  102. && $0 stop
  103. $0 start
  104. ;;
  105. force-reload)
  106. check_init
  107. fail_unless_root
  108. $0 restart
  109. ;;
  110. status)
  111. check_init
  112. status_of_proc -p "$DOCKER_SSD_PIDFILE" "$DOCKERD" "$DOCKER_DESC"
  113. ;;
  114. *)
  115. echo "Usage: service docker {start|stop|restart|status}"
  116. exit 1
  117. ;;
  118. esac

第二步:赋予文件可执行权限。

  1. sudo chmod u+x,g+x,o+x /etc/init.d/docker

至此 Ubuntu 可以正常识别并启动 Docker 服务。

国内镜像

创建或修改 /etc/docker/daemon.json

  1. sudo mkdir /etc/docker
  2. sudo vim /etc/docker/daemon.json

写入如下内容以加速 Docker 镜像的下载(使用 Vim 粘贴时记得提前用 :set paste 设置粘贴模式避免粘贴后格式混乱)。

  1. {
  2. "registry-mirrors": ["https://t3yqbami.mirror.aliyuncs.com"]
  3. }

免 sudo 运行

此步骤是为了之后运行与 Docker 相关的命令时可以免除 sudo
首先检查当前用户是否在组 docker 中:

  1. groups $USER | grep docker

若输出为空,则按如下方法将当前用户添加到组 docker 中;否则可跳过此小节。

  1. sudo usermod -aG docker $USER

对于 WSL 2 用户,此设置只在之后新开的终端窗口生效。完成后,退出当前终端窗口并重新打开。
对于 Linux 桌面用户,需要注销后再登录才可生效。

启动服务

运行 Fabric 需要使用 Docker,其中在第二大节的下载 Docker 镜像环节就需要 Docker 服务处于开启状态,因此在这里提前打开。由于系统启动方式不同,服务启动方式也略有不同。

非 WSL 2 用户:

  1. sudo systemctl start docker

类似地,还有

  1. sudo systemctl status docker
  2. sudo systemctl restart docker
  3. sudo systemctl stop docker

等命令。

对于 WSL 2 用户(其他亦可使用):

  1. sudo service docker start

类似地,还有

  1. sudo service docker status
  2. sudo service docker restart
  3. sudo service docker stop

注意:使用 systemctl 与 service 语法略有不同,可以观察到宾语和动词的位置是相反的。

验证安装状态

经过以上步骤,Docker 应该已能正常运作。尝试以下命令:

  1. docker run hello-world

若是第一次运行,则会尝试从国内镜像站拉取镜像,然后打印出“Hello from Docker!”字样。
运行命令:

  1. docker images

应该能看到刚刚拉取的 hello-world 镜像。

至此,Hyperledger Fabric 所需的软件环境已准备就绪。


1.2.7 安装 IPFS

IPFS 将被用在项目中作为区块链外的数据存储地。与 Hyperledger Fabric 没有关系。

1.2.7.1 Ubuntu 用户(非 WSL)使用 Snap 安装

此小节供非 WSL 的 Ubuntu 用户。WSL 用户请看后面的小节。

  1. snap install ipfs

注:可能由于 Snap 的限制无法自定义 IPFS_PATH。后续需要使用该环境变量,故请勿使用 Snap 安装。

1.2.7.2 Manjaro 用户使用 AUR 安装

通过安装 ipfs-desktop(带 GUI 的版本)得到 ipfs

  1. sudo pacman -S ipfs-desktop

1.2.7.3 WSL 使用 ipfs-update 安装(Linux 通用方法)(需翻墙)

WSL 中暂(2021-05-09)无法使用 Snap,故无法用 Snap 法安装。使用此法需要确保终端中能够翻墙。
在 Linux 终端中运行

  1. go get -u -v github.com/ipfs/ipfs-update

完成后得到 ipfs-update 工具,它可用来辅助我们安装 IPFS。运行

  1. ipfs-update -v

看到版本号以验证已正确安装。

运行

  1. ipfs-update versions

查看可供安装的版本。

运行

  1. ipfs-update install latest

安装最新版本。

1.2.7.4 验证安装

运行

  1. ipfs -h

可以看到帮助则安装正常。


2. 下载 Fabric 文件

为保持文件目录清楚有序,本篇以 ~/src/fabric_1.4 位置为例,过程所需工程均安放在此目录下。此非硬性需求,可根据个人习惯调整。

  1. cd ~
  2. mkdir -p ./src/fabric_1.4
  3. cd $_

2.1 下载 Fabric 所需文件

注意:进行下一步命令之前需要 Docker 服务处于开启状态。

在目录 ~/src/fabric_1.4 中执行下列命令:

  1. curl -sSL https://gitee.com/czyczk/fabric-bootstrap/raw/master/bootstrap_v1.4.8.sh | bash -s

此步骤载入并运行基于官方原版修改的一键部署脚本。若一切正常,所有文件均可以可观的速度下载而不需翻墙。
当看到最后列出所有 Docker 镜像时代表已完成。

进入 fabric-samples 目录后运行 ls 应当看到这些文件夹,其中包含 binconfigfirst-network 等。

2.2 测试网络

进入 first-network 目录,运行

  1. ./byfn.sh generate

会发现目录中多出了 channel-artifactscrypto-config 两个文件夹。简单来说,这些是脚本根据目录中已存在的配置文件生成的文件,在后续章节中将具体介绍和使用。

运行

  1. ./byfn.sh up

在确认对话中输入“Y”。

接下来它会开始一系列测试,包括启动节点、创建通道、加入通道、安装链码和调用链码等,整个过程可持续一两分钟。

当最终出现这样的标志时表明一切正常。

运行

  1. ./byfn.sh down

以关闭网络。

注意:无论运行是否顺利,在 up 后都需要运行 down 关闭网络,否则会影响下次启动。


3. Hyperledger Fabric 基础操作

这一节简单讲一些关于二进制程序(上一节提到的 bin 目录下的可执行文件)的用途,以及 Hyperledger Fabric 框架中各种基础功能的用法。后续要使用 SDK 来自动化的操作本质也是由这些动作组成的,只不过操作方式不同,这一节从在终端中敲命令的角度来完成这些操作。

关于框架中的角色、交易流程以及其他一些概念和操作更正式的定义和描述详见官方文档。

3.1 ./byfn.sh generate

这个步骤具体包含两个部分,分别与证书文件和通道配置有关,都是在网络启动前做的准备工作。我们来手动重现这些部分,这对于了解 Fabric 网络的配置和启动很有帮助。

首先将之前脚本为我们在 first-network 中生成的 crypto-configchannel-artifacts 两个文件夹删除,删除后我们就基本还原到了执行脚本之前的状态。
确保当前在 first-network 目录中,然后执行以下命令以删除生成的文件夹和文件。

  1. rm -rf crypto-config channel-artifacts

然后便可开始第一部分,调用程序 cryptogen 来生成身份证书文件。

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

crypto-config.yaml 文件指定了整个 Fabric 网络的组织和身份结构,若查看 crypto-config.yaml 文件,可以看到其中为 example.com 这个域名分别定义了 5 个排序节点以及 2 个组织(org1.example.comorg2.example.com),每个组织有 2 个成员节点。留有大致印象就行,后续若涉及到自创的网络可参照这里的格式进行自定义。

执行上面的命令之后,就产生了 crypto-config 文件夹,其中包含了这 9 个节点的身份证书文件。这些身份证书文件将在各节点间通信和交易时发挥签名和验证等作用。在后续执行的很多操作中就需要 crypto-config 文件夹中的不同文件来配合不同的通信节点对象。

这便是第一部分了,从中我们大致知道了 cryptogen 这个工具的作用。

第二部分与创世区块通道配置文件有关,通过 configtx.yaml 文件提供配置。

观察这个文件的内容,可以看到这个文件进一步为参与了 Fabric 网络的组织指定了 MSP 证书(在第一部分中生成的证书文件中的 MSP 的文件位置)和权限(在“Organizations”节中)、排序节点使用的排序算法(solo、kafka 或 etcdraft)(在“Orderer”节中)、每个节点的 IP 地址与端口(在“Orderer”节中)以及我们要说的重点——“Profiles”节。

观察“Profiles”节,节中定义了三套档案,包含“TwoOrgsOrdererGenesis”、“TwoOrgsChannel”等。以一会要用的“TwoOrgsOrdererGenesis”为例,在其“Consortiums”小节定义了通道的联盟(即包含了我们之前所定义的组织 1 和组织 2),从而,我们若稍后使用这套档案来创建通道,该通道将视组织 1 和组织 2 为准入组织。

这些配置文件(前面提到的 crypto-config.yaml 和现在的 configtx.yaml)作为定义一个 Fabric 网络和其中的通道及其联盟结构等方面有着极大的参考价值。

了解了这个配置文件之后,我们使用 configtxgen 工具来生成创世块和通道。首先确保处在 first-network 目录,执行:

  1. mkdir channel-artifacts
  2. ../bin/configtxgen -profile TwoOrgsOrdererGenesis -channelID byfn-sys-channel -outputBlock ./channel-artifacts/genesis.block

在这些命令中,我们新建了文件夹 channel-artifacts,调用工具 configtxgen,指定使用“TwoOrgsOrdererGenesis”这个档案(它默认会在 configtxgen.yaml 中寻找档案),指定通道名称为“byfn-sys-channel”,并将这个结果写入创世块,放在刚建的 channel-artifacts 文件夹中。

现在查看文件夹 channel-artifacts 可以看到一个名为 genesis.block 的文件,稍后启动网络时就会以它为这个区块链的创世块。

接下来我们要创建通道配置文件。假设我们要创建的通道名字叫“mychannel”。先定义一个临时的环境变量以便后面经常用到。

  1. export CHANNEL_NAME=mychannel

再次使用 configtxgen 工具来创建通道配置文件。

  1. ../bin/configtxgen -profile TwoOrgsChannel -channelID $CHANNEL_NAME -outputCreateChannelTx ./channel-artifacts/channel.tx

再看 channel-artifacts 可以看到生成的通道配置文件。

再往下,生成锚节点配置文件(当一个组织内的节点要找另一个组织内的节点就需要用到锚节点,具体这里不加阐述)。

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

再度观察 channel-artifacts 目录,又多出了两个 .tx 结尾的文件,分别是组织 1 和组织 2 的锚节点的配置文件。

3.2 ./byfn.sh up

提示:这里开始需要使用 Docker。运行命令前确保 Docker 服务处于开启状态。

如下命令使用配置文件 docker-compose-cli.yaml 开启 Docker 容器。

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

观察 docker-compose-cli.yaml 文件,可以看出其中定义了 6 个容器,分别 1 个 orderer 容器、4 个 peer 容器和 1 个 cli 容器,其中 cli 容器要求其余容器都启动完成后才启动。

使用命令 docker ps 可看到正在运行的容器,数量和种类正如配置文件所指定。这 6 个容器若状态均为“UP”则说明一切正常。

在这里我们可以想象一个 Docker 容器实例就像一个轻量虚拟机实例,对于主机来说,虚拟机在共享主机的计算资源,主机很清楚地知道虚拟机的存在;反之,虚拟机不必知道主机的存在,主机上的其他虚拟机在逻辑上对于该虚拟机来说就像是其他的计算机(例如在那个 cli 容器看来,orderer 就像是运行在另一台机器上的节点)。

在这里使用 Docker 的原因之一就是为了制造访问隔离。
首先是主机与容器之间的隔离:主机可以指定某个路径作为容器进入时的工作目录,可以通过权限阻止容器内访问它所被允许访问的目录的上级目录,或者其他不被允许访问的位置,从而避免恶意链码或通过其他渠道的方式造成对主机文件的恶意访问和修改。关于在这个示例中是怎样将主机的部分路径开放给 Docker 容器实例的,可以参照 first-network 下的 base/docker-compose-base.yaml 文件。
然后是容器互相之间的隔离:因为对于一个容器而言,其他容器就像在其他计算机上,因此无法像访问本机文件一样轻松访问和修改其他容器中的文件。在 Fabric 中,主要依靠内置的 grpc 通信模块进行基于网络的通信方式来进行限定范围内的交互。这就像我们上网时用一个端口向其他主机上的端口发送请求和响应一样,让信息在两个计算机的端口之间进行交互,正常情况下我们只能调用有限的接口来获取被允许访问的资源,而无法肆意对对方的主机造成破坏。
这样的特性很好地保证了每个节点上数据的安全,以及运行该节点的主机不受影响。

接下来我们要使用在 generate 阶段得到的那些生成物来创建通道,并将节点加入通道。

首先执行

  1. docker exec -it cli bash

来进入 cli 容器的 shell。无论之前用什么 shell,进入后我们只能在 cli 容器的 bash 中操作。

在 cli 容器中,我们可以选择向不同的节点通信。节点信息可通过这四个变量来定义:
CORE_PEER_MSPCONFIGPATHCORE_PEER_ADDRESSCORE_PEER_LOCALMSPIDCORE_PEER_TLS_ROOTCERT_FILE
在 cli 容器刚打开时,我们默认的通信对象是 example.com 域名下的组织 1 的节点 0(在后文中我们称它为 peer0.org1.example.com,这也是在 Fabric 配置文件中常见的一种形式)。使用 echo 打印这 4 个环境变量可以看到它们已经有了默认定义。

如果之后我们要向其他节点通信,只要相应地修改这 4 个变量就可以。

如果在容器中没有到处乱跑,在初始位置下有三个文件夹,其中 channel-artifactscrypto 实际就是主机中 first-network 目录下 channel-artifactscrypto-config 这两个文件夹的映射。(因此在 cli 容器中若在这些文件夹下创建文件或修改文件,在主机的相应位置也能看到变动。)那么接下来命令中的 channel-artifacts 文件夹中实际就有了我们在 generate 阶段中生成的那些文件。

Docker 容器中的 bash 有独立的环境变量,因此常用的变量还需要重设。

  1. export CHANNEL_NAME=mychannel

在容器内,使用 peer 工具利用之前生成的通道配置文件创建通道。
(前方长命令警告,一行内写完。)

  1. peer channel create -o orderer.example.com:7050 -c $CHANNEL_NAME -f ./channel-artifacts/channel.tx --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem

其实它的本质就这些内容:

  1. peer channel create -o <排序节点的端口> -c <通道名称> -f <通道配置文件>

后面的 --tls--cafile 参数提供的是排序节点的身份证书,是为了验证我们所连接的排序节点的身份,避免被内鬼排序节点获取交易内容(和上网需要 HTTPS 同理)。

first-network 这个示例提供的是一个完整的网络环境,因此也启用了身份认证这块的要求。在实际开发测试中,可以使用类似 chaincode-docker-devmode 示例中的配置来免除这些长长的身份认证文件路径,在后续编写和测试链码时会使用到。



看到回应只有 INFO 而没有 ERROR 则一切正常。


ls 看当前目录,可以看到出现一个 mychannel.block。之后在 cli 容器的 bash 中,指定这个区块文件就可以进行与通道有关的操作。例如马上要进行的将 peer0.org1.example.com 加入通道的操作:

  1. peer channel join -b mychannel.block

正常的回应也应类似于上图,只有 INFO 信息,显示 Successfully submitted proposal to join channel

同理,修改前文提到的 4 个变量变更通信节点后,运行 peer channel join 命令可以将新的节点加入通道。
例如,为了接下来能测试指定锚节点的功能,只将来自另一个组织的 peer0.org2.example.com 加入通道。

首先覆盖这 4 个环境变量以代表组织 2 的节点 0。

  1. CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
  2. CORE_PEER_ADDRESS=peer0.org2.example.com:9051
  3. CORE_PEER_LOCALMSPID="Org2MSP"
  4. 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

然后再执行

  1. peer channel join -b mychannel.block

至此,已创建了名为“mychannel”的通道,并将来自两个组织的各一个节点加入了通道。下一步是更新锚节点。

更新锚节点是在利用之前生成的两个通道配置文件来对当前链上的通道定义作出修改。区块链只增不加,它没有修改创世块,只是用新发起的交易添加了一些变更。我们要做的,就是更新这个链上的通道定义,把 peer0.org1.example.com 设为组织 1 的锚节点,把 peer0.org2.example.com 设为组织 2 的锚节点。

由于那 4 个环境变量,我们仍在向 peer0.org2.example.com 通信,因此先设置组织 2 的锚节点(否则这个交易在背书阶段无法通过签名验证)。

  1. peer channel update -o orderer.example.com:7050 -c $CHANNEL_NAME -f ./channel-artifacts/Org2MSPanchors.tx --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem

正确的结果应显示 Successfully submitted channel update

然后切换环境变量更改通信对象为 peer0.org1.example.com,用类似的命令设置组织 1 的锚节点。关于该节 的环境变量上文截图中已给出,可自行尝试,若出问题可参考以下答案。

  1. CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
  2. CORE_PEER_ADDRESS=peer0.org1.example.com:7051
  3. CORE_PEER_LOCALMSPID="Org1MSP"
  4. 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
  1. peer channel update -o orderer.example.com:7050 -c $CHANNEL_NAME -f ./channel-artifacts/Org1MSPanchors.tx --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem

小知识:
Linux shell 中在设置环境变量的方法上,一些细微的差异会导致不同的结果。
假设我们有个脚本文件叫 test.sh,其中内容很简单,就是打印变量 $Aecho $A
第一种是像这样 A=123 ./test.sh,得到的结果是 123,但是再在 shell 里 echo $A 时会发现是空的(或者之前设置的值)。这是因为这种一行式的环境变量只能作用于一条命令;
第二种是
A=123; echo $A(分号相当于换行,相当于在实操时免去分号,用两行写。)
这样设置的变量 $A 可以作用于当前 shell 的所有命令,但是无法被其生成的子进程继承;
而第三种
export A=123; echo $A
这样设置的不仅作用于当前 shell,还可以作用于子进程,例如再启动一个 bash,或者运行 ./test.sh,在其中还可以得到 $A 的设定值。
在一些 Fabric 教程中(例如官方文档中),可能使用第一种的一行式来设定那 4 个环境变量,那样就只是在那一行命令中向另一个节点对象通信。

脚本中的 up 接下来测试链码的安装和调用。这里暂不自己写链码,而是利用示例中提供的链码来测试。

链码可以是用 Go、Java 或 Javascript 写成,本篇只以部署 Go 语言链码为例,其他语言参见官方文档。
一个名称 + 版本的组合唯一定义一个链码。所以可以安装同名的链码的两个版本,可以安装版本号相同但名称不同的链码,但不允许出现名称和版本相同的链码(即使使用的语言或内容不同)。

回忆 Fabric 交易的流程:对于链码这块,普通的交易就像是一个函数调用,包含要调用的链码名称以及参数。背书节点对交易提案进行模拟执行,提交节点对交易提案进行应用,因此这两类节点上都需要安装链码才能执行交易,即链码需要在所有的提交和背书节点上安装。

示例的链码位于 fabric-samples 中的 chaincode 文件夹,有好几个示例链码。在 cli 容器中,这个位置被映射到 /opt/gopath/src/github.com/chaincode,稍后我们就要在容器内使用这个目录中的链码。

在官方教程中对于 first-network 这个示例计划使用 2 个组织的节点共同背书的策略,因此可以将链码安装在 peer0.org1.example.compeer0.org2.example.com 两个节点上。

若按顺序执行,现在 cli 容器还在向 peer0.org1.example.com 通信。使用如下命令在这个节点上安装链码。

  1. peer chaincode install -n mycc -v 1.0 -p github.com/chaincode/chaincode_example02/go/

-n 指定了链码名称为“mycc”,-v 指定版本为“1.0”,-p 后指定的位置是相对于 /opt/gopath/src 的相对位置(或者也可以理解为省略这一段),指定了 chaincode_example02 这个链码的 Go 语言版本。
(若想了解这个链码的源码,可以打开 chaincode 目录中的文件 chaincode_example02/go/chaincode_example02.go 大致查看。)

正常的安装结果应该显示 Installed remotely response:<status:200 payload:"OK" >

完成后,修改 4 个环境变量以变更通信节点为 peer0.org2.example.com,然后再执行一次链码安装。

  1. CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
  2. CORE_PEER_ADDRESS=peer0.org2.example.com:9051
  3. CORE_PEER_LOCALMSPID="Org2MSP"
  4. 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
  1. peer chaincode install -n mycc -v 1.0 -p github.com/chaincode/chaincode_example02/go/

接下在通道上实例化链码。在这一步指定了背书策略,并指定了链码中变量的初始值,使得链码在该通道上可用。

可以想象在面向对象的语言中,之所以在定义类后要实例化使用(典型的实例化方法例如使用 new 来创建对象),一个重要原因就是为了让每个这个类的实例中的变量拥有不同的值——类更像是模板,而把实例化后得到的对象视为独一无二的数据容器。在这里,安装后的链码就类比于类,而在每个通道上实例化后的链码就类比于对象,每个链码实例在每个通道上都可以拥有独一无二的变量值。

  1. peer chaincode instantiate -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n mycc -v 1.0 -c '{"Args":["init","a", "100", "b","200"]}' -P "AND ('Org1MSP.peer','Org2MSP.peer')"

又是一长串,忽略身份验证的那一块,其本体是

  1. peer chaincode instantiate -o <排序节点端口> -C <要实例化该链码的通道名> -n <要实例化的链码名> -v <版本> -c <传给链码 init 函数的变量> -P <背书策略>

这个链码中有 ab 两个变量(从实际用途考虑可以假装成是两个账户)。在上面那行命令中,为这个通道上的 ab 赋予了初始值(就像加入系统时账户拥有的初始余额)。具体变量这个结构体该长什么样是由链码相应函数的定义决定(即输入由接口决定)。

关于背书策略,在这里的指定实现了官方教程中想要两个组织的节点共同背书的需求。

完成链码的实例化后,可以通过 docker ps 看到又有两个容器。

接下来测试链码的其他功能,首先从查询开始。使用以下命令来查询变量 a 的值。与前一个一样,peer chaincode 系列的命令均使用 -c 来指定传给链码函数的参数。

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

如果用面向对象中调用方法的语法来做类比,这就像是 channel.mycc.query("a"),主题是链码在通道上的实例,而非“链码模板”。
从细节上来说,上述命令出现了两个 query,前一个指的是我们只从区块链账本上查数据,并不发生写入,因此也不会产生交易;后一个指的是我们对链码上的某个函数传入的第一个参数是 query,如果这个链码上还有其他可以查询数据的功能,那后面的名称可以不是 query。关于这一点,在下一节编写链码的过程中会更清楚。

这步完成后得到返回值 100 正是我们在实例化过程中调用初始化函数赋给 a 的原始值。

如果我们使用 peer chaincode invoke,在参数中给予“从 ab 数值 10 的语义”,再度使用 peer chaincode query,就可以观察到数值的变化。
peer chaincode invoke 的执行会触发系统中一些部分的第一次执行,因此可能花费比之前其他命令更长一些时间,是正常现象。)

  1. peer chaincode invoke -o orderer.example.com:7050 --tls true --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n mycc --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"Args":["invoke","a","b","10"]}'

这个命令也出现了两次 invoke,前者指的是我们进行的操作涉及到写入,需要发起一个交易、收集背书并提交给排序节点;后者指的是传入的第一个参数为 invoke,在链码支持的情况下,自然也可以通过传入其他参数来执行其他功能。
由于要发起交易、要涉及背书和排序,因此在这个命令中除了之前与 -o 配套出现的 --tls--cafile,还需要提供两个背书节点地址的参数 --peerAddresses 和他们他们的身份证书参数 --tlsRootCertFiles

再一次使用 peer chaincode query,这次应该显示 90

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

关于脚本中的 up 大致就是这样的内容,它确保了通道创建、节点加入、安装链码和调用链码这些关键功能的正常运行。关于更多的细节,例如容器具体是在什么时候被创建、如何查看每个容器上的交易日志等内容,详见官方文档。

3.3 ./byfn.sh down

对于我们之前的启动方式,大体上来说,这步是在做两个事情。

首先是关闭在刚才过程中启动的所有 Docker 容器:

  1. docker-compose -f docker-compose-cli.yaml down

然后删除 generate 阶段的生成物:

  1. rm -rf crypto-config channel-artifacts/*

若我们只想重启 Docker 容器而不删除 generate 生成物,可以只手动运行上面的命令。


4. 链码

4.1 准备工作

前一节展现了链码是如何安装和被使用的,现在我们跳出框架操作者的视角,从链码开发者的角度来看。考虑篇幅,还是仅以 Go 语言为例。

工欲善其事,必先利其器。首先要准备一个好用的 Go 语言开发环境,在这里推荐的三种编辑器 / IDE:

在此出于通用性与时间成本的考量,以 Visual Studio Code 为例进行最基础的配置,其他编辑器 / IDE 使用者灵活变通。

4.1.1 安装 Visual Studio Code

WSL Ubuntu 用户:
只需要在 Windows 上安装 Visual Studio Code(可从官网下载)即可。WSL 中的 Ubuntu 自带一个 Visual Studio Code 服务器,因此使用时只需要在 WSL Ubuntu 的终端中运行 code,即可在 Windows 中打开,并编辑 Ubuntu 上的文件。

打开后观察窗口左下角,若是有 WSL Ubuntu 等字样则是正确的,否则无法访问 WSL 中的文件。

其他 Ubuntu 用户:
若 Snap 可用,可使用 Snap 安装:

  1. sudo snap install --classic code

否则,添加 PPA,再用 apt-fast 安装:

  1. wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg
  2. sudo install -o root -g root -m 644 packages.microsoft.gpg /etc/apt/trusted.gpg.d/
  3. sudo sh -c 'echo "deb [arch=amd64 signed-by=/etc/apt/trusted.gpg.d/packages.microsoft.gpg] https://packages.microsoft.com/repos/vscode stable main" > /etc/apt/sources.list.d/vscode.list'
  1. sudo apt-fast install apt-transport-https -y
  2. sudo apt update
  3. sudo apt-fast install code -y

Manjaro 用户:

  1. sudo pacman -S visual-studio-code-bin --needed

4.1.2 GoLand 与 VS Code 安装后事宜

Linux 在使用 VS Code 或者 IntelliJ 系列的 IDE 时可能会遇到 watches 上限的问题。
Ubuntu 需要将以下内容写入 /etc/sysctl.conf,而 Manjaro 需要写入 /etc/sysctl.d/99-sysctl.conf(或在更新版本中是 50-max_user_watches.conf,若此文件存在)。

  1. fs.inotify.max_user_watches = 524288

写入完成后,Ubuntu 执行

  1. sudo sysctl -p --system

Manjaro 执行(根据文件名调整)

  1. sudo sysctl -p /etc/sysctl.d/99-sysctl.conf

4.1.3 VS Code 的 Go 语言插件

提示:确保 $GOPROXY 变量的设置可以提高插件安装的速度与成功率。若还未妥善设置 $GO111MODULE$GOPROXY 请回到前面的章节进行设置。

在 VS Code 窗口的左侧点击“插件”按钮,在搜索框中输入“Go”并安装。

完成后,需要进一步进行安装 Go 的语言服务器套装。

使用快捷键 Ctrl + Shift + P 调出命令框,在其中输入 Go Install Tools,选择第一项,并全选所有的工具,点击“OK”。

在窗口下方的输出框中可以看到安装进度,等待一分钟左右,当所有组件均以“SUCCEEDED”结束,并最终显示“All good to Go”的时候,所有的组件都成功安装。

若有组件失败,检查网络连接情况以及 $GO111MODULE$GOPROXY 的设置,重新从 Ctrl + Shift + P 开始再试一次。

新建并打开一个文件,以 .go 结尾(如 main.go),以此触发 VS Code 激活 Go 组件。在此期间右下角出现的与 Go 有关的工具的提示,均点击“更新”。

全部完成后,测试一下是否可以正常使用。

例如在 main.go 中仅输入以下内容:

  1. package main
  2. func main() {
  3. fmt.Println("Hello world.")
  4. }

在保存的瞬间,它应当自动导入包 fmt,因此在 package 行和 func 行之间还能看到 import "fmt"。若情况与此有异,则说明 Go 语言插件并未正确配置,应当在继续之前,根据具体情况进行调整。

为了进一步加强使用体验,在设置中搜索“Go Use Language Server”,勾选复选框以启用 gopls,这将使得查看函数签名和语义重命名更加方便。

4.1.4 关于 Go 语言的须知

对于已有 Go 语言使用经历和看过其他教程的读者,这篇文章中可能有一点在其他教程中不会特别强调,就是在新建任何一个 Go 工程的目录之后,第一步的事情是在目录中使用 go mod init 来创建 Go 模块。这是因为使用 Go 国内镜像设置的 $GOPROXY 变量必须配合 $GO111MODULE 变量为“on”,后者导致了对目录下 go.mod 文件的强制要求。对于不需要使用 $GO111MODULE$GOPROXY 变量的用户可以跳过后文的这个步骤。

4.2 观察链码

在上一节中介绍了 fabric-samples/chaincode 中的链码 chaincode_example02。我们可以先从观察这个链码的代码开始。

这次我们使用 VS Code。首先按照各自的方法打开 VS Code,按快捷键 Ctrl + k Ctrl + o(连着按两套,可以一直按住 Ctrl 然后 k o)以打开文件夹。

可以打开 fabric-samples/chaincode 文件夹,然后在窗口左侧进一步导航至 chaincode_example02/go/chaincode_example02.go

可以看到主体部分有一个名为 SimpleChaincodestruct 定义,剩下对它的指针类型的实现部分中,最重要的是这两个签名:

Go 虽然也有继承和实现的概念,但不同于 Java 等语言需要用关键词来定义,而是自动地隐式地继承和实现。例如上面列出的这两个签名,使得类型 *SimpleChaincode 实现了接口 Chaincode

这样,无论具体的业务逻辑是怎样,只要一个类型实现了这个接口,Fabric 框架就知道当链码被安装和升级的时候,就去调用这个链码的 Init 函数,当被使用 peer chaincode invokepeer chaincode query 的时候,就去调用 Invoke 函数。

这两个签名的共同点是,它们都将 shim.ChaincodeStubInterface 作为输入参数类型,将 pb.Response 作为返回类型。使用过 Java 的 Servlet 框架或者一些其他背景的请求与响应框架的用户对这种形态一定不陌生,简而言之,就是把接收到的请求参数包装在输入参数中,在具体实现中从这个封装中取出请求参数;然后通过框架提供的函数来完成区块链账本的操作,并生成符合签名要求的返回值。

Invoke 函数为例,我们在上一节中遇到了两个命令会触发这个函数,分别是(简单起见,只列出关键部分,非完整命令):

  1. peer chaincode query ... -c '{"Args":["query","a"]}'

  1. peer chaincode invoke ... -c '{"Args":["invoke","a","b","10"]}'

Invoke 函数中,首先通过

  1. function, args := stub.GetFunctionAndParameters()

将封装在入参中的请求参数抽取出来。然后根据 function 来决定具体的分支。

  1. if function == "invoke" {
  2. ...
  3. } else if function == "delete" {
  4. ...
  5. } else if function == "query" {
  6. ...
  7. }

在具体的逻辑实现上,shim 封装了账本的增改和查询等操作的具体复杂实现,暴露出 shim.PutState()shim.GetState() 等函数,在需要操作区块链账本的时候带来极大便利;在完成业务逻辑之后,通过由 shim 提供的 shim.Success()shim.Error() 函数根据实际执行情况生成返回值。

Init 函数也是类似,只不过它不再区分 function,因此用 _, args := stub.GetFunctionAndParameters() 来无视前者。

最后,由于链码在容器中是作为独立程序运行的(实际上甚至在独立的容器中运行),并不是被程序调用的函数,因此它也需要自己的 main 函数。
观察这个示例链码的 main 函数,做得事情不多,但必须存在。

一个链码程序的大致结构就是如此:定义一个 struct,为它的指针类型写两个严格符合 Chaincode 接口签名要求的函数(InitInvoke),再加上代表独立程序基本尊严的 main 函数。剩下的部分是为了完成具体的逻辑实现而又不让一个函数过长而存在。

4.3 写一个链码

还是仿照官方的例子写一个,在过程中讲解编码思路。

我们把要写的链码放在 fabric-samples/chaincode 文件夹中:新建一个文件夹 screw_example,在其中新建一个文件 screw_example.go

Ctrl + j 在 VS Code 中调出终端(使用其他终端也一样),cdscrew_example 目录,运行

  1. go mod init screw_example

完成后可以在 VS Code 窗口左侧看到 screw_example 文件夹中多出了 go.mod 文件。

使用快捷键 Ctrl + k Ctrl + o 打开文件夹 screw_example。现在可以开始写代码了。

打开文件 go.mod,在其中加入

  1. require github.com/hyperledger/fabric v1.4.8

这个意思是在我们这个工程中,将使用这个第三方库,且限定了版本。否则之后在链码中我们会无法使用第三方库,或使用错误(与 Fabric 1.4.8 版本不匹配的)的第三方库造成包名不同和接口不同等情况。

完成后保存文件

打开文件 screw_example.go,套路化地写下这些内容:

  1. package main
  2. import (
  3. "github.com/hyperledger/fabric/core/chaincode/shim"
  4. "github.com/hyperledger/fabric/protos/peer"
  5. )

注意:其中一行依赖是 github.com/hyperledger/fabric/protos/peer 而非 fabric-protos-go/peer,小心避免 IDE 在保存造成非预期的修改。这是 Fabric 1.4 和 2.0 在编写链码时的依赖区别之一。

这次假装这个链码是用来记录公司 CorpA 和 CorpB 的螺丝库存量和移交情况的,所以我们可以定义一个名为 ScrewInventorystruct,让其指针类型实现 Chaincode 接口。

  1. // ScrewInventory implements interface Chaincode.
  2. type ScrewInventory struct {
  3. }
  4. // Init specifies the initial amount of available screws in each company.
  5. func (si *ScrewInventory) Init(stub shim.ChaincodeStubInterface) peer.Response {
  6. }
  7. // Invoke handles query and transfer requests.
  8. func (si *ScrewInventory) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
  9. }

提示:如果使用 VS Code,其中的 go-lint 组件要求所有包外可见的(大写开头的)声明(包括变量、函数、结构体等)都要有注释,且这个注释必须以所声明的名称开头,对于不满足此要求的注释多有抱怨。这是为了保证代码(包括注释)风格的一致性。如果没有使用 go-lint 组件,可以不按此要求。

先完成前一个函数 Init

要做的第一件事就是知道 / 设计实例化链码的时候我们要初始化的内容。例如我们可以设计,在实例化时,指定 CorpA 有 200 个螺丝,CorpB 有 100 个。那么,我们在操作时就会用类似

  1. peer chaincode instantiate ... -c '{"Args":["init","CorpA","200","CorpB","100"]} ...'

这样的命令,因此在链码中,我们只要照这个解析出参数就好了。

首先从请求中抽取参数,并检查参数的个数。

  1. // Extract the parameters and check the number of them
  2. _, args := stub.GetFunctionAndParameters()
  3. if len(args) != 4 {
  4. return shim.Error(fmt.Sprintf("Incorrect number of arguments. Got %v. Expecting 4", len(args)))
  5. }

抽取出的参数都是字符串,而我们需要初始数量的值为非负整数,因此需要进一步把这些参数分离出来,并将一部分进行类型转换和非负检查。(在 VS Code 中,以下代码中用到的字符串与数字互转的包 strconv 在保存时应该会自动导入,若没有,在文件头部手动导入。)

  1. // Check the validity of the parameters
  2. invalidValErrorStr := "Expecting an integer value >= 0 for asset holding"
  3. corp1Name := args[0]
  4. corp1Amnt, err := strconv.Atoi(args[1])
  5. if err != nil || corp1Amnt < 0 {
  6. return shim.Error(invalidValErrorStr)
  7. }
  8. corp2Name := args[2]
  9. corp2Amnt, err := strconv.Atoi(args[3])
  10. if err != nil || corp2Amnt < 0 {
  11. return shim.Error(invalidValErrorStr)
  12. }

任何非法的参数都已经提前通过返回错误解决了(从命令使用者的角度,他们若提交了包含非法参数的命令之后,收到的反馈中会包含“ERROR”级的错误)。

剩下就是将这些实始参数写入账本,完成初始化,并以“成功”状态返回。在写入账本时,PutState 函数的两个参数分别代表键值型数据库的键和值,分别接受 string[]byte 类型,因此要把刚才的数字类型的几个变量转换成 []byte

  1. // Write the state to the ledger
  2. err = stub.PutState(corp1Name, []byte(strconv.Itoa(corp1Amnt)))
  3. if err != nil {
  4. return shim.Error(err.Error())
  5. }
  6. err = stub.PutState(corp2Name, []byte(strconv.Itoa(corp2Amnt)))
  7. if err != nil {
  8. return shim.Error(err.Error())
  9. }
  10. return shim.Success(nil)

Init 函数完成,往后是 Invoke。我们计划实现两个功能:
1. 移交(transfer):从一个公司将一定数量的螺丝移交到另一公司。
2. 查询(query):查询一个公司现有的螺丝库存量。

还是像之前一样,先考虑用户应该要怎样使用。我们可以规定这个接口以这样的形式使用:

  1. peer chaincode invoke ... -c '{"Args":["transfer","CorpA","CorpB","10"]}'
  1. peer chaincode query ... -c '{"Args":["query","CorpA"]}'

那么相应地,在 Invoke 中,我们先抽取用户想调用的功能以及参数,然后根据功能名进一步调用函数,对于不存在的功能以错误返回。由于参数是否合法具体取决于实际功能的需求,因此检查参数这步交由具体的功能函数来处理。

  1. // Extract the function name and the parameters
  2. function, args := stub.GetFunctionAndParameters()
  3. // Act according to the function name
  4. if function == "transfer" {
  5. // Transfers the spceified amount of asset between the specified two corporations
  6. return si.transfer(stub, args)
  7. } else if function == "query" {
  8. // Queries the amount of asset of the specified corporation
  9. return si.query(stub, args)
  10. }
  11. return shim.Error("Invalid invoke function name. Expecting \"transfer\" or \"query\"")

接下来,对每个小函数作具体实现。大体思路见注释。

transfer

  1. // This function will receive parameters as `peer chaincode invoke ... -c '{"Args":["transfer","CorpA","CorpB","10"]}'`
  2. func (si *ScrewInventory) transfer(stub shim.ChaincodeStubInterface, args []string) peer.Response {
  3. // Check if the parameters are valid
  4. if len(args) != 3 {
  5. return shim.Error("Incorrect number of arguments. Expecting 3")
  6. }
  7. invalidAmntErrorStr := "Expecting an integer value >= 0 for asset holding"
  8. sourceCorpName := args[0]
  9. destCorpName := args[1]
  10. transferAmnt, err := strconv.Atoi(args[2])
  11. if err != nil {
  12. return shim.Error(invalidAmntErrorStr)
  13. }
  14. // Check if the asset source has enough amount to transfer
  15. sourceRemainingAmntBytes, err := stub.GetState(sourceCorpName)
  16. if err != nil {
  17. return shim.Error(fmt.Sprintf("Failed fetching the asset status of \"%v\"", sourceCorpName))
  18. }
  19. sourceRemainingAmnt, _ := strconv.Atoi(string(sourceRemainingAmntBytes))
  20. if sourceRemainingAmnt < transferAmnt {
  21. return shim.Error(fmt.Sprintf("Failed to transfer: \"%v\" does not have the enough amount of assets", sourceCorpName))
  22. }
  23. // Perform the transfer and update the ledger
  24. destRemainingAmntBytes, err := stub.GetState(destCorpName)
  25. if err != nil {
  26. return shim.Error(fmt.Sprintf("Failed fetching the asset status of \"%v\"", destCorpName))
  27. }
  28. sourceRemainingAmnt -= transferAmnt
  29. destRemainingAmnt, _ := strconv.Atoi(string(destRemainingAmntBytes))
  30. destRemainingAmnt += transferAmnt
  31. err = stub.PutState(sourceCorpName, []byte(strconv.Itoa(sourceRemainingAmnt)))
  32. if err != nil {
  33. return shim.Error(err.Error())
  34. }
  35. err = stub.PutState(destCorpName, []byte(strconv.Itoa(destRemainingAmnt)))
  36. if err != nil {
  37. return shim.Error(err.Error())
  38. }
  39. return shim.Success(nil)
  40. }

query

  1. // This function will receive parameters as `peer chaincode query ... -c '{"Args":["query","CorpA"]}'`
  2. func (si *ScrewInventory) query(stub shim.ChaincodeStubInterface, args []string) peer.Response {
  3. // Check if the parameters are valid
  4. if len(args) != 1 {
  5. return shim.Error("Incorrect number of arguments. Expecting 1")
  6. }
  7. // Extract the query target
  8. targetCorpName := args[0]
  9. // Get the current state of the specified corporation
  10. amntBytes, err := stub.GetState(targetCorpName)
  11. if err != nil {
  12. return shim.Error(err.Error())
  13. }
  14. return shim.Success(amntBytes)
  15. }

最后别忘了基本尊严。

  1. func main() {
  2. err := shim.Start(new(ScrewInventory))
  3. if err != nil {
  4. fmt.Printf("failed to start ScrewInventory: %s", err)
  5. }
  6. }

这样链码的基本要素都完备了,然而存在两个问题。第一是代码没经过测试,无法保障正确性,这一点将在下一小节中解决;第二是代码中一些部分较为冗余,在实际工程中应考虑将可复用地部分进一步封装,增强代码的可读性和可维护性。

4.4 测试链码

4.4.1 使用 MockStub 进行单元测试

链码就是大厦的底基,写完链码是一定要做测试的。测试要和功能配套(已经写的功能都尽可能要覆盖,所传给函数的参数要与最新的接口一致),如果有条件甚至可以按照 TDD (Test-Driven Development) 的思想指导,测试先行,以保证功能的正确实现。

首先,回到 go.mod 文件,在 require 块中补充上测试所需要的包 "github.com/stretchr/testify latest"。正常情况下在保存后,VS Code 将自动解析并下载包(表现为 latest 会被换成真实版本号),若没有,可以用 go mod download 来刺激一下。

若不熟悉这个测试包,建议在继续之前在其官方页面了解下基本功能和用法。

screw_example 文件夹中,新建文件 screw_example_test.go(就是比之前的文件多了 _test)。
编辑文件,在开头写下

  1. package main
  2. import (
  3. "testing"
  4. "github.com/hyperledger/fabric/core/chaincode/shim"
  5. "github.com/hyperledger/fabric/protos/peer"
  6. "github.com/stretchr/testify/assert"
  7. )

之后,我们会用到 shim 中的 MockStubassert 中的 FatalNow 等功能。
作为示例,我们只覆盖链码中的两个调用 / 分支:InitInvoke 中的 query。实际工程中一定要尽量多地覆盖。

(代码先上,有时间再补充解释。不复杂,用的也基本是 Fabric 中提供的功能。)

  1. var testLogger = shim.NewLogger("screw_example_test")
  2. // Creates a MockStub bound to the chaincode struct ScrewInventory.
  3. func createMockStub(stubName string) *shim.MockStub {
  4. si := new(ScrewInventory)
  5. mockStub := shim.NewMockStub(stubName, si)
  6. return mockStub
  7. }
  8. // Initializes the chaincode with the specified parameters using mockStub.MockInit.
  9. func initChaincode(mockStub *shim.MockStub, arguments [][]byte) *peer.Response {
  10. resp := mockStub.MockInit("1", arguments)
  11. return &resp
  12. }
  13. func TestInit(t *testing.T) {
  14. mockStub := createMockStub("Test: Init")
  15. // Expect the chaincode to be initialized normally
  16. arguments := [][]byte{[]byte("init"), []byte("CorpA"), []byte("200"), []byte("CorpB"), []byte("100")}
  17. resp := initChaincode(mockStub, arguments)
  18. if resp.Status != shim.OK {
  19. testLogger.Infof("Initialization failed: %v", string(resp.Message))
  20. t.FailNow()
  21. }
  22. }
  23. func TestInovkeQuery(t *testing.T) {
  24. mockStub := createMockStub("Test: Invoke Query")
  25. // Expect the chaincode to be initialized normally
  26. initArguments := [][]byte{[]byte("init"), []byte("CorpA"), []byte("200"), []byte("CorpB"), []byte("100")}
  27. initResp := initChaincode(mockStub, initArguments)
  28. if initResp.Status != shim.OK {
  29. testLogger.Infof("Initialization failed: %v", string(initResp.Message))
  30. t.FailNow()
  31. }
  32. // Prepare the information needed to invoke the function
  33. invokeFunction := []byte("query")
  34. invokeArgument := []byte("CorpA")
  35. expectedValue := "200"
  36. // Invoke query and expect the payload to be correct
  37. invokeResp := mockStub.MockInvoke("1", [][]byte{invokeFunction, invokeArgument})
  38. if invokeResp.Status != shim.OK {
  39. testLogger.Infof("%s failed: %v", invokeFunction, string(invokeResp.Message))
  40. t.FailNow()
  41. }
  42. if invokeResp.Payload == nil {
  43. testLogger.Infof("%s failed: cannot get the value", invokeFunction)
  44. t.FailNow()
  45. }
  46. payload := string(invokeResp.Payload)
  47. if payload != expectedValue {
  48. testLogger.Infof("%s failed: value was %v instead of %v as expected", invokeFunction, payload, expectedValue)
  49. t.FailNow()
  50. }
  51. testLogger.Infof("%s invoked. Got %v as expected", invokeFunction, payload)
  52. }

完成文件并保存后,在终端的该目录下使用 go test 来开始一次测试。测试会自动检测所有 Test 开头的函数作为测试项目,不分先后地开始(因此需要在测试 Invoke query 的时候也先执行初始化)。
若一切正常,应当看到输出中一个“PASS”(可以去修改测试中的参数以假装一个错误,以确保它可以正常地报错)。若出现“FAIL”可以观察到是哪项测试出的错,可以是链码出的错,也可以是测试出的错。若是编译错误则先在 screw_example.goscrew_example_test.go 中修正错误。

作为练习,可以考虑自行完成对 Invoketransfer 分支的测试。
同上一小节一样,代码存在许多重复的地方,实际项目中应进一步将可复用地部分按需抽取为不同等级的帮助函数,以增加可读性和可维护性。

4.4.2 在 chaincode-docker-devmode 中手动测试

如果单元测试和更高层的测试已经完备,在环境中的手动测试主要还是为了在有多个节点参与背书时能不出错,功能性方面已经不需要过多的担心了。

(待写)


5. 使用 Fabric Go SDK 的工程

5.1 小工程需求拟定

域名:
lab805.com

组织:
3 个,分别为 org1org2ordererOrg。前两者模拟两个公司(或任何可能的两个实体),最后的组织是整个网络的排序节点组织。

节点:
org1org2 的节点情况如下:

类型 CA ID 名称 证书文件夹名
peer peer0 peer0.org*.lab805.com
peer peer1 peer1.org*.lab805.com
client user1 User1@org*.lab805.com
admin org*admin Admin@org*lab805.com

其中 org* 将在不同组织中有具体指代。例如 org1 的 admin 在 CA 中的 ID 为 org1admin

ordererOrg 的节点情况如下:

类型 CA ID 名称 证书文件夹名
orderer orderer orderer.lab805.com
orderer orderer2 orderer2.lab805.com
admin ordererOrgadmin Admin@lab805.com

共识:
使用 EtcdRaft 以利用大于 1 个的排序节点。

业务逻辑:
使用第 4 节中编写的螺丝链码,稍作修改。

功能:
能够通过浏览器代表组织中的用户发出螺丝资产转移的交易请求以及查询某个组织的螺丝库存。

大体实现步骤:
1. 使用 Fabric CA 生成所有节点的证书并整理成与示例工程中相仿的文件结构。
2. 修改 configtx.yaml 并生成创世块与通道配置文件。
3. 编写 docker-compose 文件并启动网络。
4. 编写 SDK 配置文件并添加部分代码,确保应用通道配置文件、加入节点等功能可用。
5. 使用 SDK 打包并安装链码,编写事件相关逻辑,将至此的功能封装作为服务层,向上暴露 API。
6. 编写简易网页前端以方便可视化操作。

说明 1:
在上述步骤中,第 1 步和第 2 步是为了生成 Fabric 网络启动所需的文件。这些文件可以按照 5.3、5.4 小节中的命令手动生成。若时间紧迫也可以跳过这两个小节,通过仓库 https://gitee.com/czyczk/fabric-sdk-tutorial 直接得到文件,从而进入 5.4 小节。

说明 2:
通过 5.3.3(纯手动方法)或 5.3.4(使用脚本)均可进行至 5.4 小节,具体详见 5.3 小节的说明。

5.2 项目预览

该项目分为两个子项目,分别负责后端和前端。
后端包含了 Fabric 网络的搭建所需的脚本、链码、使用了 SDK 的函数、封装了 SDK 调用的服务层与封装了服务层的控制器层。
前端项目包含两个页面,分别用于转移资产和查询资产。

5.2.1 后端项目启动方式

  1. cd ~/src/fabric_1.4
  2. git clone https://gitee.com/czyczk/fabric-sdk-tutorial
  3. cd fabric-sdk-tutorial
  4. make

运行 make 之后,将启动 Fabric 网络(数个 Docker 节点),然后将自动完成创建通道、将节点加入通道、安装和实例化链码等操作。

链码的实例化被设置为公司 Org1 有 200 个螺丝,公司 Org2 有 100 个。因为在实例化后还调用了转移功能进行测试,从 Org1 转移了 10 个螺丝给 Org2,所以等到我们可以操作时 Org1 有 190 个,而 Org2 有 110 个。

在之后若看到如下图的字样即表示服务器已经在监听请求了,这时就可以去启动前端项目了。



想要关闭后端程序,只需在程序所运行的终端中按 Ctrl + C 终止程序后,输入 make env-down 关闭网络即可。

5.2.2 前端项目启动方式

在运行之前确保机器上装有 nodewhich node)。若未安装可自行安装,推荐使用 NVM 安装。
此外确保有 npm 或者 yarn(二选一即可)。

5.2.2.1 NVM 方法安装 Node 与 npm

运行

  1. curl -sSL https://gitee.com/czyczk/nvm-bootstrap/raw/master/bootstrap.sh | bash -s

若出现新开窗口生效的提示则成功。

运行 nvm install 14.15.4 以安装当前最新的 LTS 版本(2021 年 1 月 26 日)。

5.2.2.2 启动前端项目

接下来可以按喜好使用 NPM 或 Yarn 来安装 Angular 二进制。
(对于第一次使用者,以下命令包含了国内源的设置,请自行辨别。)

注:不要使用 sudo 运行 NPM 或 Yarn。

NPM:

  1. npm config set registry https://registry.npm.taobao.org --global
  2. npm install -g @angular/cli

Yarn:

  1. yarn config set registry https://registry.npm.taobao.org --global
  2. yarn global add @angular/cli

前置要求得到保证后,可以克隆项目。

  1. mkdir -p ~/src/Angular_Projects
  2. cd $_
  3. git clone https://gitee.com/czyczk/fabric-sdk-tutorial-web
  4. cd fabric-sdk-tutorial-web

先运行 npm installyarn install 来安装项目所需依赖。

然后(之后的运行也是只需要)运行 npm start 或者 yarn start 启动前端服务器,等待编译完成后,使用浏览器打开 http://localhost:4200

Ctrl + C 可以停止该服务器。

5.3 从 Fabric CA 开始

5.3.1 安装 Fabric CA

在一切开始前我们要确保有可用的 fabric-ca-serverfabric-ca-client 这两个可执行文件在 $PATH 路径中。总体上 Fabric CA 有两种方法可以安装。
1. 直接用官方编译好的版本(Ubuntu 用户可能不能使用这种方法)。
2. 从源码编译。

5.3.1.1 方法一:使用官方编译的二进制文件(Ubuntu 不可用)

无论是从 Fabric CA 的 GitHub 仓库的 Release 页面中下载,还是用 fabric-samplesbin 文件夹带有的,只要确保 fabric-ca-serverfabric-ca-client 这两个文件可正常运行(可尝试在二进制所在目录下用 ./fabric-ca-server -h 这样的方法测试),就可以将这两个文件复制到 $GOPATH/bin 下即可。

注:Ubuntu 无论是 18.04 还是 20.04 都不可以使用官方编译的 fabric-ca-server。原因主要是官方依赖的 GLIBC 库最低版本是 2.28,而 Ubuntu 均只有 2.27 版本(可使用 ldd --version 命令确认)。不要为此更新系统的 GLIBC 版本,这可能会影响其他功能的正常运行,建议 Ubuntu 用户使用从源码编译的方法。

5.3.1.2 方法二:从源码编译

能够翻墙的可以运行命令:

  1. GO111MODULE=off go get -u -v github.com/hyperledger/fabric-ca/cmd/...

无法翻墙的可运行命令:

  1. mkdir $GOPATH/src/github.com/hyperledger
  2. git clone https://gitee.com/czyczk/fabric-ca $GOPATH/src/github.com/hyperledger/fabric-ca
  3. GO111MODULE=off go get -u -v github.com/hyperledger/fabric-ca/cmd/...

注:由于要编译并放置到 $GOPATH/bin 下,而不是作为 Go 语言项目的依赖库,所以必须暂时禁用 $GO111MODULE,而这将导致 $GOPROXY 功能暂时失效。若遇到网络问题可多试几次。

安装完成后尝试 fabric-ca-server -hfabric-ca-client -h 确保这两个二进制文件可正常运行。

5.3.2 阅读理解

在继续之前,强烈建议阅读这篇文章,其中很好地阐述了和接下来的步骤有关的 3 个重点问题:
1. 示例网络中 crypto-config 文件夹里主要文件的作用
2. cryptogen 工具的局限性
3. 如何使用 Fabric CA 替代 cryptogen 工具来得到和示例网络相同的证书文件结构

5.3.3 手动使用 Fabric CA 生成工程中所需的证书

此小节将手动地通过命令完成证书的生成,同样的结果也可以通过 5.2.4 小节的脚本实现。对于实现细节有兴趣的可以阅读本小节。

大纲:为两个组织分别初始化并启动一个 CA 服务器。使用 CA 客户端为网络中所有的角色,包括 peer, admin, client 和 orderer 等都生成 MSP 和 TLS 证书。最后通过移动和重命名文件来将生成物调整为与示例工程相仿的目录结构,方便后续的使用。
这一小节将手动地通过命令来完成以上任务,若在过程中遇到问题,可以仔细观察文件夹的区别。这部分并非重点,也可以跳过直接进入 5.2.4 小节。

第 1 步:准备文件夹。

首先我们在 ~/src 目录下建立文件夹 fabric-ca,所有的生成物都放在这里面。

  1. mkdir -p ~/src/fabric-ca
  2. cd $_

在这里建立两个文件夹,分别用于放置服务器的生成物和客户端的生成物。

  1. mkdir server client

第 2 步:为两个组织创建并启动 Fabric CA 服务器。
进入 server 文件夹,在其中创建 org1org2 两个文件夹。

  1. cd server
  2. mkdir org1 org2

我们在哪个文件夹下运行命令,就可在哪个文件夹下初始化和启动 CA 服务器。先准备启动组织 1 的 CA 服务器。
进入 org1,使用几个环境变量来配置即将生成的 CA 服务器。

  1. cd org1
  2. export FABRIC_CA_SERVER_CA_NAME=ca-org1
  3. export FABRIC_CA_SERVER_CSR_CN=ca.org1.lab805.com
  4. export FABRIC_CA_SERVER_TLS_ENABLED=true

这两个环境变量分别指定了 CA 服务器的名称、CSR 名称和启用 TLS 特性。

运行以下命令来初始化服务器。

  1. fabric-ca-server init -b admin:adminpw

命令在 org1 文件夹中初始化了第一个服务器,通过 -b 来指定用户名和密码,在实际工程中务必使用强密码

通过 ls 可以看到多出了几个文件。其中 fabric-ca-server.db 文件是这个服务器的数据库,所有后续经过客户端的 register 的新用户都将记录于此。而 ca-cert.pem 文件是这个 CA 服务器的证书文件。可运行如下命令来查看这个证书的发行者和自身信息。

  1. openssl x509 -in ca-cert.pem -noout -subject -issuer

通过 CN (CSR common name)字段可以看到发行者和持证者是同一个实体,这就是所谓的“自签名证书”。由于不是真实项目,我们没有实际证书,还是使用有自签名证书的根服务器来模拟。

运行以下命令来启动服务器。

  1. fabric-ca-server start -b admin:adminpw

这样就启动了 org1 中的那个服务器。终端已被这个服务器进程的输出占用,后续的操作需要保持服务器开启,所以我们用一个新的终端来执行后续的操作。
由于启用了 TLS,在启动后还会生成 TLS 证书 tls-cert.pem,有兴趣也可以仿照上面的命令用 openssl 工具看一下。

对于组织 2 也作类似地操作,相同部分不再解释。

  1. cd ~/src/fabric-ca/server/org2
  2. export FABRIC_CA_SERVER_CA_NAME=ca-org2
  3. export FABRIC_CA_SERVER_CSR_CN=ca.org2.lab805.com
  4. export FABRIC_CA_SERVER_TLS_ENABLED=true
  5. export FABRIC_CA_SERVER_PORT=8054
  6. export FABRIC_CA_SERVER_OPERATIONS_LISTENADDRESS=127.0.0.1:10443
  7. fabric-ca-server init -b admin:adminpw
  8. fabric-ca-server start -b admin:adminpw

由于都是在单机上运行服务器,为避免和组织 1 的服务器冲突,加了两行更换端口的环境变量。只对于当前有用的信息是,要联系组织 1 的 CA 服务器找 localhost:7054,组织 2 找 localhost:8054

第 3 步:为组织 1 的实体发放证书。组织 1 总共有 4 个角色需要证书,分别是 peer0、peer1、user1(类型为 client)和 org1admin。

注:在下文有关第 3 步和第 4 步的所有命令,均以 fabric-ca 文件夹作为当前工作目录为准(即保持 cd ~/src/fabric-ca 后不动)。

第 3.1 步: Enroll 组织 1 的 admin。
进入 ~/src/fabric-ca 目录,为这个组织中实体的证书准备文件夹。Fabric CA 客户端与服务器在使用上一个不同点在于,客户端依赖环境变量 $FABRIC_CA_CLIENT_HOME,将其当作配置文件和生成物的存放点。

  1. mkdir -p ./client/peerOrganizations/org1.lab805.com
  2. export FABRIC_CA_CLIENT_HOME=${PWD}/client/peerOrganizations/org1.lab805.com

准备好环境变量后,用客户端的 enroll 命令来从服务器获取签发的证书。由于和之后会遇到的在服务器上新增实体条目的操作“register”的单词语义相近,后文对于“从服务器获取证书”这一行为还是保留英文动词“enroll”及其名词“enrollment”,对于“在服务器上新增实体”这一行为用“register”指代。

  1. fabric-ca-client enroll -u https://admin:adminpw@localhost:7054 --caname ca-org1 --tls.certfiles ${PWD}/server/org1/tls-cert.pem

服务器启用了 TLS 功能,因此在 enrollment 中,需要通过 https 开头的 URL 来联系服务器,并使用服务器的 TLS 证书 tls-cert.pem 来确保所联系的是真实的服务器。

得到的证书产物与服务器的证书类似,可以在 fabric-ca 目录下的 client/peerOrganizations/org1.lab805.com/msp 中看到。尝试使用 openssl 工具查看刚刚得到的证书。

  1. openssl x509 -in client/peerOrganizations/org1.lab805.com/msp/signcerts/cert.pem -noout -subject -issuer

可以看到发行者是 ca.org1.lab805.com,持证者是 admin

Enrollment 之后,每次运行 fabric-ca-client 就会以 $FABRIC_CA_CLIENT_HOME 所代表的那个实体行动。接下来我们就以组织 1 的 admin 的身份来 register 组织 1 中的其余身份。

为了还原示例里证书中的 OU(组织单位)信息,我们还需要在 client/peerOrganizations/org1.lab805.com/msp 中新建一个 config.yaml,以便在后续“register”命令中指定实体的 OU。往 config.yaml 中写入以下信息。

注:在 vim 中可以使用 :set ts=2 sw=2 来让缩进变为 2 个空格宽度以使得格式更符合 YAML 文件的风格。

  1. NodeOUs:
  2. Enable: true
  3. ClientOUIdentifier:
  4. Certificate: cacerts/localhost-7054-ca-org1.pem
  5. OrganizationalUnitIdentifier: client
  6. PeerOUIdentifier:
  7. Certificate: cacerts/localhost-7054-ca-org1.pem
  8. OrganizationalUnitIdentifier: peer
  9. AdminOUIdentifier:
  10. Certificate: cacerts/localhost-7054-ca-org1.pem
  11. OrganizationalUnitIdentifier: admin
  12. OrdererOUIdentifier:
  13. Certificate: cacerts/localhost-7054-ca-org1.pem
  14. OrganizationalUnitIdentifier: orderer

第 3.2 步: Register 并 enroll peer0.org1.lab805.com
接下来先以创建 peer0.org1.lab805.com 为例,先以组织 1 的 admin 身份在组织 1 的服务器上 register 一个新的身份。

  1. fabric-ca-client register --caname ca-org1 --id.name peer0 --id.secret peer0pw --id.type peer --id.attrs '"hf.Registrar.Roles=peer"' --tls.certfiles ${PWD}/server/org1/tls-cert.pem

--id.name 指定了身份的名称,--id.secret 为密码,--id.type 对应了上面 config.yaml 中的一种,--id.attrs 指定了属性。

执行完后不会看到什么变化,只是组织 1 的服务器文件夹中的数据库文件 fabric-ca-server.db 中新增了一个条目。

然后就可以用下面的命令为 peer0 取得身份证书。

  1. fabric-ca-client enroll -u https://peer0:peer0pw@localhost:7054 --caname ca-org1 -M ${PWD}/client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/msp --csr.hosts peer0.org1.lab805.com --tls.certfiles ${PWD}/server/org1/tls-cert.pem

这次使用了 -M 参数来指定取得的证书放置的位置,模仿了示例工程,将 peer0 的证书放在 .../peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/msp 之下。通过 --csr.hosts 来指定证书中的 CSR(证书签名请求)主机名。

为了验证“CSR Hosts”字段的变化,这次需要用下面的命令来查看更完整的信息。留意“X509v3 Subject Alternative Name”字段(原来是本地主机名,现在变成了指定名)。

  1. openssl x509 -in client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/msp/signcerts/cert.pem -noout -text

接下来要获取 peer0 的 TLS 证书(需要的理由和 CA 服务器需要 TLS 证书一样)。与获取身份证书类似,还是通过向组织 1 的 CA 服务器发送“enroll”请求来完成。稍有不同之处在于 -M 指定的放置位置,--csr.hosts 的指定以及 --enrollment.profile 参数的设置。

  1. fabric-ca-client enroll -u https://peer0:peer0pw@localhost:7054 --caname ca-org1 -M ${PWD}/client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/tls --enrollment.profile tls --csr.hosts peer0.org1.lab805.com --csr.hosts localhost --tls.certfiles ${PWD}/server/org1/tls-cert.pem

与之有关的核心证书坐落于 client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/tls/signcerts/cert.pem,有兴趣可以观察一下与身份证书有什么不同。

第 3.3 步:整理成与示例中相同的结构。
就以我们刚刚的 peer0 为例,观察 client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com 文件夹,先来看看我们生成的文件和示例文件的结构有何异同。



新增的文件以灰色底标出,可以通过复制或、移动和重命名得到的文件遵循箭头和对应颜色指示。下面进行具体说明。

  1. config.yamlpeer0msp 文件夹中是缺失的,后面我们会从 org1.lab805.com/msp 中复制这个文件。
  2. msp/tlscacerts/ 文件夹和 tls/ca.crt/ 是缺失的,要从 fabric-ca/server/org1 中复制 tls-cert.pem实际工程中若使用与发放身份证书不同的 TLS CA 服务器,这一步则要以实际为准。
  3. tls/keystore 中的文件变更为 tls/server.keytls/signcerts 中的文件变更为 tls/server.crt

具体的命令如下:

  1. cp client/peerOrganizations/org1.lab805.com/msp/config.yaml client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/msp/
  1. mkdir client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/msp/tlscacerts
  2. cp client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/tls/tlscacerts/* client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/msp/tlscacerts/tlsca.org1.lab805.com-cert.pem
  1. cp client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/tls/tlscacerts/* client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/tls/ca.crt
  1. mv client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/tls/keystore/* client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/tls/server.key
  1. mv client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/tls/signcerts/* client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/tls/server.crt
  1. rm -rf client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/tls/cacerts
  2. rm -rf client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/tls/keystore
  3. rm -rf client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/tls/signcerts
  4. rm -rf client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/tls/tlscacerts
  5. rm -rf client/peerOrganizations/org1.lab805.com/peers/peer0.org1.lab805.com/tls/user

至此,peer0 的证书结构和示例程序中的基本相同。

第 3.4 步: Register 并 enroll peer1.org1.lab805.comUser1@org1.lab805.comAdmin@org1.lab805.com
peer1 所用命令与上面的命令基本相同,不再给出,可自行参考完成。接下来以注册 User1@org1.lab805.comAdmin@org1.lab805.com 为例,相同部分不再加以解释。

Register user1org1admin

  1. fabric-ca-client register --caname ca-org1 --id.name user1 --id.secret user1pw --id.type client --id.attrs '"hf.Registrar.Roles=client"' --tls.certfiles ${PWD}/server/org1/tls-cert.pem
  2. fabric-ca-client register --caname ca-org1 --id.name org1admin --id.secret org1adminpw --id.type admin --id.attrs '"hf.Registrar.Roles=admin"' --tls.certfiles ${PWD}/server/org1/tls-cert.pem

user1 的身份证书和 TLS 证书分别放在 client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/(位置与 peer 身份的证书有异)之下的 msptls 中,不需要提供 --csr.hosts 参数。

  1. fabric-ca-client enroll -u https://user1:user1pw@localhost:7054 --caname ca-org1 -M ${PWD}/client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/msp --tls.certfiles ${PWD}/server/org1/tls-cert.pem
  2. fabric-ca-client enroll -u https://user1:user1pw@localhost:7054 --caname ca-org1 -M ${PWD}/client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/tls --enrollment.profile tls --tls.certfiles ${PWD}/server/org1/tls-cert.pem

整理 user1 的证书。

  1. cp client/peerOrganizations/org1.lab805.com/msp/config.yaml client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/msp/
  1. mkdir client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/msp/tlscacerts
  2. cp client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/tls/tlscacerts/* client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/msp/tlscacerts/tlsca.org1.lab805.com-cert.pem
  1. cp client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/tls/tlscacerts/* client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/tls/ca.crt
  1. mv client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/tls/keystore/* client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/tls/server.key
  1. mv client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/tls/signcerts/* client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/tls/server.crt
  1. rm -rf client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/tls/cacerts
  2. rm -rf client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/tls/keystore
  3. rm -rf client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/tls/signcerts
  4. rm -rf client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/tls/tlscacerts
  5. rm -rf client/peerOrganizations/org1.lab805.com/users/User1@org1.lab805.com/tls/user

Enroll org1admin 的身份证书与 TLS 证书。与 user1 类似,要放在 users 文件夹中而非 peers 文件夹中,不需要指定 --csr.hosts 参数。

  1. fabric-ca-client enroll -u https://org1admin:org1adminpw@localhost:7054 --caname ca-org1 -M ${PWD}/client/peerOrganizations/org1.lab805.com/users/Admin@org1.lab805.com/msp --tls.certfiles ${PWD}/server/org1/tls-cert.pem
  2. fabric-ca-client enroll -u https://org1admin:org1adminpw@localhost:7054 --caname ca-org1 -M ${PWD}/client/peerOrganizations/org1.lab805.com/users/Admin@org1.lab805.com/tls --enrollment.profile tls --tls.certfiles ${PWD}/server/org1/tls-cert.pem

整理 org1admin 的证书。

  1. cp client/peerOrganizations/org1.lab805.com/msp/config.yaml client/peerOrganizations/org1.lab805.com/users/Admin@org1.lab805.com/msp/
  1. mkdir client/peerOrganizations/org1.lab805.com/users/Admin@org1.lab805.com/msp/tlscacerts
  2. cp client/peerOrganizations/org1.lab805.com/users/Admin@org1.lab805.com/tls/tlscacerts/* client/peerOrganizations/org1.lab805.com/users/Admin@org1.lab805.com/msp/tlscacerts/tlsca.org1.lab805.com-cert.pem
  1. cp client/peerOrganizations/org1.lab805.com/users/Admin@org1.lab805.com/tls/tlscacerts/* client/peerOrganizations/org1.lab805.com/users/Admin@org1.lab805.com/tls/ca.crt
  1. mv client/peerOrganizations/org1.lab805.com/users/Admin@org1.lab805.com/tls/keystore/* client/peerOrganizations/org1.lab805.com/users/Admin@org1.lab805.com/tls/server.key
  1. mv client/peerOrganizations/org1.lab805.com/users/Admin@org1.lab805.com/tls/signcerts/* client/peerOrganizations/org1.lab805.com/users/Admin@org1.lab805.com/tls/server.crt
  1. rm -rf client/peerOrganizations/org1.lab805.com/users/Admin@org1.lab805.com/tls/cacerts
  2. rm -rf client/peerOrganizations/org1.lab805.com/users/Admin@org1.lab805.com/tls/keystore
  3. rm -rf client/peerOrganizations/org1.lab805.com/users/Admin@org1.lab805.com/tls/signcerts
  4. rm -rf client/peerOrganizations/org1.lab805.com/users/Admin@org1.lab805.com/tls/tlscacerts
  5. rm -rf client/peerOrganizations/org1.lab805.com/users/Admin@org1.lab805.com/tls/user

第 4 步:为组织 2 的实体分发证书。组织 2 也是 4 个角色需要证书,CA ID 分别是 peer0peer1user1(类型为 client)和 org2admin。所用命令的思路与组织 1 完全相同,只需修改其中的组织指代、文件夹位置以及 OU 配置文件(组织 1 的 CA 服务器端口号 7054 在组织 2 中是 8054,因此发放者证书名应当为 localhost-8054-ca-org2.pem)。

第 5 步:为排序节点生成 CA 服务器,用客户端取得 orderer1orderer2ordererOrgadmin 的证书。
由于组织名称与前两个组织有少许不同,此次的命令还是完整给出,以供参考。

第 5.1 步:初始化并启动 lab805.comorderer 组织的 CA 服务器。

在一个新的终端中输入以下命令。

  1. cd ~/src/fabric-ca
  2. mkdir server/ordererOrg && cd $_
  3. export FABRIC_CA_SERVER_CA_NAME=ca-orderer
  4. export FABRIC_CA_SERVER_CSR_CN=ca.orderer.lab805.com
  5. export FABRIC_CA_SERVER_TLS_ENABLED=true
  6. export FABRIC_CA_SERVER_PORT=9054
  7. export FABRIC_CA_SERVER_OPERATIONS_LISTENADDRESS=127.0.0.1:11443
  8. fabric-ca-server init -b admin:adminpw
  9. fabric-ca-server start -b admin:adminpw

第 5.2 步: Enroll 排序节点组织的 admin。这次不在 peerOrganizations 中而在 ordererOrganizations 中。

  1. mkdir -p ./client/ordererOrganizations/lab805.com
  2. export FABRIC_CA_CLIENT_HOME=${PWD}/client/ordererOrganizations/lab805.com
  3. fabric-ca-client enroll -u https://admin:adminpw@localhost:9054 --caname ca-orderer --tls.certfiles ${PWD}/server/ordererOrg/tls-cert.pem

将以下内容写入 client/ordererOrganizations/lab805.com/msp/config.yaml 中。

  1. NodeOUs:
  2. Enable: true
  3. ClientOUIdentifier:
  4. Certificate: cacerts/localhost-9054-ca-orderer.pem
  5. OrganizationalUnitIdentifier: client
  6. PeerOUIdentifier:
  7. Certificate: cacerts/localhost-9054-ca-orderer.pem
  8. OrganizationalUnitIdentifier: peer
  9. AdminOUIdentifier:
  10. Certificate: cacerts/localhost-9054-ca-orderer.pem
  11. OrganizationalUnitIdentifier: admin
  12. OrdererOUIdentifier:
  13. Certificate: cacerts/localhost-9054-ca-orderer.pem
  14. OrganizationalUnitIdentifier: orderer

第 5.3 步: Register 两个排序节点。orderer.lab805.comorderer2.lab805.com 和管理员 ordererAdmin@lab805.com

  1. fabric-ca-client register --caname ca-orderer --id.name orderer --id.secret ordererpw --id.type orderer --tls.certfiles ${PWD}/server/ordererOrg/tls-cert.pem
  2. fabric-ca-client register --caname ca-orderer --id.name orderer2 --id.secret orderer2pw --id.type orderer --tls.certfiles ${PWD}/server/ordererOrg/tls-cert.pem
  3. fabric-ca-client register --caname ca-orderer --id.name ordererAdmin --id.secret ordererAdminpw --id.type admin --tls.certfiles ${PWD}/server/ordererOrg/tls-cert.pem

第 5.4 步: Enroll orderer.lab805.com 的身份证书和 TLS 证书并整理结构。
Enroll

  1. fabric-ca-client enroll -u https://orderer:ordererpw@localhost:9054 --caname ca-orderer -M ${PWD}/client/ordererOrganizations/lab805.com/orderers/orderer.lab805.com/msp --csr.hosts orderer.lab805.com --csr.hosts localhost --tls.certfiles ${PWD}/server/ordererOrg/tls-cert.pem
  2. fabric-ca-client enroll -u https://orderer:ordererpw@localhost:9054 --caname ca-orderer -M ${PWD}/client/ordererOrganizations/lab805.com/orderers/orderer.lab805.com/tls --enrollment.profile tls --csr.hosts orderer.lab805.com --csr.hosts localhost --tls.certfiles ${PWD}/server/ordererOrg/tls-cert.pem

整理

  1. cp client/ordererOrganizations/lab805.com/msp/config.yaml client/ordererOrganizations/lab805.com/orderers/orderer.lab805.com/msp/
  1. mkdir client/ordererOrganizations/lab805.com/orderers/orderer.lab805.com/msp/tlscacerts
  2. cp client/ordererOrganizations/lab805.com/orderers/orderer.lab805.com/tls/tlscacerts/* client/ordererOrganizations/lab805.com/orderers/orderer.lab805.com/msp/tlscacerts/tlsca.lab805.com-cert.pem
  1. cp client/ordererOrganizations/lab805.com/orderers/orderer.lab805.com/tls/tlscacerts/* client/ordererOrganizations/lab805.com/orderers/orderer.lab805.com/tls/ca.crt
  1. mv client/ordererOrganizations/lab805.com/orderers/orderer.lab805.com/tls/keystore/* client/ordererOrganizations/lab805.com/orderers/orderer.lab805.com/tls/server.key
  1. mv client/ordererOrganizations/lab805.com/orderers/orderer.lab805.com/tls/signcerts/* client/ordererOrganizations/lab805.com/orderers/orderer.lab805.com/tls/server.crt
  1. rm -rf client/ordererOrganizations/lab805.com/orderers/orderer.lab805.com/tls/cacerts
  2. rm -rf client/ordererOrganizations/lab805.com/orderers/orderer.lab805.com/tls/keystore
  3. rm -rf client/ordererOrganizations/lab805.com/orderers/orderer.lab805.com/tls/signcerts
  4. rm -rf client/ordererOrganizations/lab805.com/orderers/orderer.lab805.com/tls/tlscacerts
  5. rm -rf client/ordererOrganizations/lab805.com/orderers/orderer.lab805.com/tls/user

第 5.5 步: Enroll orderer2.lab805.com 的身份证书和 TLS 证书并整理结构。
Enroll

  1. fabric-ca-client enroll -u https://orderer2:orderer2pw@localhost:9054 --caname ca-orderer -M ${PWD}/client/ordererOrganizations/lab805.com/orderers/orderer2.lab805.com/msp --csr.hosts orderer2.lab805.com --csr.hosts localhost --tls.certfiles ${PWD}/server/ordererOrg/tls-cert.pem
  2. fabric-ca-client enroll -u https://orderer2:orderer2pw@localhost:9054 --caname ca-orderer -M ${PWD}/client/ordererOrganizations/lab805.com/orderers/orderer2.lab805.com/tls --enrollment.profile tls --csr.hosts orderer2.lab805.com --csr.hosts localhost --tls.certfiles ${PWD}/server/ordererOrg/tls-cert.pem

整理

  1. cp client/ordererOrganizations/lab805.com/msp/config.yaml client/ordererOrganizations/lab805.com/orderers/orderer2.lab805.com/msp/
  1. mkdir client/ordererOrganizations/lab805.com/orderers/orderer2.lab805.com/msp/tlscacerts
  2. cp client/ordererOrganizations/lab805.com/orderers/orderer2.lab805.com/tls/tlscacerts/* client/ordererOrganizations/lab805.com/orderers/orderer2.lab805.com/msp/tlscacerts/tlsca.lab805.com-cert.pem
  1. cp client/ordererOrganizations/lab805.com/orderers/orderer2.lab805.com/tls/tlscacerts/* client/ordererOrganizations/lab805.com/orderers/orderer2.lab805.com/tls/ca.crt
  1. mv client/ordererOrganizations/lab805.com/orderers/orderer2.lab805.com/tls/keystore/* client/ordererOrganizations/lab805.com/orderers/orderer2.lab805.com/tls/server.key
  1. mv client/ordererOrganizations/lab805.com/orderers/orderer2.lab805.com/tls/signcerts/* client/ordererOrganizations/lab805.com/orderers/orderer2.lab805.com/tls/server.crt
  1. rm -rf client/ordererOrganizations/lab805.com/orderers/orderer2.lab805.com/tls/cacerts
  2. rm -rf client/ordererOrganizations/lab805.com/orderers/orderer2.lab805.com/tls/keystore
  3. rm -rf client/ordererOrganizations/lab805.com/orderers/orderer2.lab805.com/tls/signcerts
  4. rm -rf client/ordererOrganizations/lab805.com/orderers/orderer2.lab805.com/tls/tlscacerts
  5. rm -rf client/ordererOrganizations/lab805.com/orderers/orderer2.lab805.com/tls/user

第 5.6 步: Enroll ordererAdmin.lab805.com 的身份证书和 TLS 证书并整理结构。
Enroll

  1. fabric-ca-client enroll -u https://ordererAdmin:ordererAdminpw@localhost:9054 --caname ca-orderer -M ${PWD}/client/ordererOrganizations/lab805.com/users/Admin@lab805.com/msp --tls.certfiles ${PWD}/server/ordererOrg/tls-cert.pem
  2. fabric-ca-client enroll -u https://ordererAdmin:ordererAdminpw@localhost:9054 --caname ca-orderer -M ${PWD}/client/ordererOrganizations/lab805.com/users/Admin@lab805.com/tls --enrollment.profile tls --tls.certfiles ${PWD}/server/ordererOrg/tls-cert.pem

整理

  1. cp client/ordererOrganizations/lab805.com/msp/config.yaml client/ordererOrganizations/lab805.com/users/Admin@lab805.com/msp/
  1. mkdir client/ordererOrganizations/lab805.com/users/Admin@lab805.com/msp/tlscacerts
  2. cp client/ordererOrganizations/lab805.com/users/Admin@lab805.com/tls/tlscacerts/* client/ordererOrganizations/lab805.com/users/Admin@lab805.com/msp/tlscacerts/tlsca.lab805.com-cert.pem
  1. cp client/ordererOrganizations/lab805.com/users/Admin@lab805.com/tls/tlscacerts/* client/ordererOrganizations/lab805.com/users/Admin@lab805.com/tls/ca.crt
  1. mv client/ordererOrganizations/lab805.com/users/Admin@lab805.com/tls/keystore/* client/ordererOrganizations/lab805.com/users/Admin@lab805.com/tls/server.key
  1. mv client/ordererOrganizations/lab805.com/users/Admin@lab805.com/tls/signcerts/* client/ordererOrganizations/lab805.com/users/Admin@lab805.com/tls/server.crt
  1. rm -rf client/ordererOrganizations/lab805.com/users/Admin@lab805.com/tls/cacerts
  2. rm -rf client/ordererOrganizations/lab805.com/users/Admin@lab805.com/tls/keystore
  3. rm -rf client/ordererOrganizations/lab805.com/users/Admin@lab805.com/tls/signcerts
  4. rm -rf client/ordererOrganizations/lab805.com/users/Admin@lab805.com/tls/tlscacerts
  5. rm -rf client/ordererOrganizations/lab805.com/users/Admin@lab805.com/tls/user

小结:这一小节特别长,主要是因为命令多,但大部分内容都是大同小异的。从归纳的角度来说需要注意的点如下:
1. 要在使用 init 命令初始化和 start 命令启动 Fabric CA 服务器之前,设定好服务器名称、启动 TLS 特性等环境变量。
2. 在没有专门的环境变量的影响下,Fabric CA 服务器的初始化和启动的位置就是当前的工作目录。
3. Register 操作指的是在服务器上注册一个身份,除了服务器的数据库发生改动,没有其他文件变动。Enroll 操作指的是从服务器取得身份证书文件。
4. 若要在一个 CA 中 register 其他身份,必须以这个 CA 的 admin 身份(或其他有同等权限的身份)来进行。为此,必须先设置 $FABRIC_CA_CLIENT_HOME 并用客户端 enroll 这个 CA 的 admin 身份。
5. CA 服务器若启用了 TLS 特性,客户端必须在进行 register、enroll 等操作的时候提供这个服务器的 TLS 证书。
6. 在用 CA 客户端为一个组织中的实体通过 enroll 身份取得证书时,通过 -M 来指定存放的位置。若是仿照示例工程中的结构,身份类型为节点的实体的目录是 peerOrganizations/<组织名>/peers/<节点名.组织名>,而身份类型为用户(包括客户端和管理员)的实体的目录是 peerOrganizations/<节织名>/users/<节点名@组织名>。对于每个身份,身份证书放在其目录下的 msp 文件夹中,TLS 证书放 tls 文件夹中。
7. Enroll 一个身份之后,msp 文件夹中的文件已经与示例结构相仿,而 tls 中的结构差异较大,可以相应进行调整。
8. 示例结构仅仅是一种参考结构,具体使用时在后续的 configtx.yaml、docker compose 文件以及 SDK 配置文件中会加以指定。若能够对这些证书的用途和对应关系清晰明了,不按照示例结构来摆放也是完全可以的。

5.3.4 通过调用脚本生成证书

脚本完成的任务与 5.2.3 小节的任务完全一致,但免除了命令的手动输入。若对命令细节和操作原理有兴趣,可以阅读脚本或手动地按照 5.2.3 小节完成。

首先使用命令克隆脚本所在仓库。

  1. cd ~/src/fabric_1.4
  2. git clone https://gitee.com/czyczk/fabric-sdk-tutorial-materials
  3. cd fabric-sdk-tutorial-materials

在这个仓库的 ca-scripts 中有利用 CA 服务器与客户端生成证书的脚本。具体使用方法如下。

运行 ./initServers.sh 分别为 org1org2ordererOrg 初始化 CA 服务器。

  1. ./initServers.sh

完成后可见 server 文件夹中有三个文件夹,与 5.2.3 手动初始化 CA 服务器之后的结果相同。

之后,通过 ./startServer.sh 分别启动这三个 CA 服务器。每次运行需要带组织名作为参数,每次运行开启一个 CA 服务器,其输出将占用当前终端。因此在启动一个服务器之后要新开一个终端容器或标签页以运行其余的命令。
在三个不同的终端容器/标签页中运行这三条命令。

  1. ./startServer.sh org1
  2. ./startServer.sh org2
  3. ./startServer.sh ordererOrg

这三个 CA 服务器将分别监听端口 705480549054,与 5.2.3 小节中的手动命令结果相同。

接下来运行 ./registerIdentities.sh 来 enroll 这 3 个组织的 admin,并各自为这 3 个组织中的身份完成 register 操作。

  1. ./registerIdentities.sh

可以在 client 文件夹中看到 enrollment 之后三个组织的 admin 证书。

最后,运行 ./enrollCerts.sh 来 enroll 所有节点的证书,在其中还会整理证书文件的结构为示例工程中的结构。

  1. ./enrollCerts.sh

若所有过程中均只有 Done 开头的提示则一切顺利,否则会以 Error 开头的提示指示哪个组织/节点的 enrollment 出现问题。

5.4 修改 configtx.yaml 并生成通道配置文件

前注:若是从 5.2.3 小节而来,需要克隆以下仓库,若是通过 5.2.4 小节可跳过此步。

  1. cd ~/src/fabric_1.4
  2. git clone https://gitee.com/czyczk/fabric-sdk-tutorial-materials
  3. cd fabric-sdk-tutorial-materials

——————————

这一小节将使用从示例工程中修改的 configtx.yaml 文件来生成创世块和通道配置文件。由于我们有 2 个排序节点,因此选择使用 EtcdRaft 共识服务。
这个修改的 configtx.yaml 文件在仓库的 config 文件夹中。关于这个修改的配置文件是如何得到的,不感兴趣者可以跳过。

这个配置文件基于示例工程中的做了哪些修改:
1. 把里面 example.com 都换成 lab805.com
2. 将 Orderer 小节中的 OrdererType 换为 etcdraft,并且把所有 EtcdRaft 相关的排序节点的定义和配置从 5 个删减至 2 个(ordererorderer2)。
3. 删除与 Kafka 有关的内容。

经过 5.2 小节,我们已经在 fabric-ca 或者 fabric-sdk-tutorial-materials/ca-scripts 中有了 client 文件夹,这个文件夹就完全可以类比于示例工程中的 crypto-config 文件夹,包含了所有节点的证书文件。
为了接下来的步骤可以顺利执行,将这个 client 文件夹移动至仓库的 config 文件夹中,并更名为 crypto-config,使其与 configtx.yaml 的层级关系与示例工程中的结构相仿。
以从 5.2.4 小节而来的情况为例,相应命令为:

  1. cd ~/src/fabric_1.4/fabric-sdk-tutorial-materials/ca-scripts
  2. mv client ../config/crypto-config
  3. cd ../config

接下来需要使用 configtxgen 工具,可以从 fabric-samples 中复制 bin 文件夹过来。
然后,执行以下命令,创建创世块和交易配置文件,使用 configtx.yaml 中的 EtcdRaft 相关的配置档案。

  1. mkdir channel-artifacts
  2. ./bin/configtxgen -profile SampleMultiNodeEtcdRaft -channelID byfn-sys-channel -outputBlock ./channel-artifacts/genesis.block
  3. export CHANNEL_NAME=mychannel && ./bin/configtxgen -profile TwoOrgsChannel -outputCreateChannelTx ./channel-artifacts/channel.tx -channelID $CHANNEL_NAME
  4. ./bin/configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./channel-artifacts/Org1MSPanchors.tx -channelID $CHANNEL_NAME -asOrg Org1MSP
  5. ./bin/configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./channel-artifacts/Org2MSPanchors.tx -channelID $CHANNEL_NAME -asOrg Org2MSP

5.5 编写/解读 docker-compose.yaml 文件

若没有克隆过仓库的先克隆下仓库,确保有下文所述的文件。

  1. cd ~/src/fabric_1.4
  2. git clone https://gitee.com/czyczk/fabric-sdk-tutorial
  3. cd fabric-sdk-tutorial

项目的几个文件夹中 fixtures 目录是与剩下相对独立的,它包含了 Fabric 网络启动所需要的文件,就像我们之前看到的 first-network 文件夹。

在这个目录中,已经有了 crypto-configchannel-artifacts 这两个文件夹,得到它们的过程正如 5.2 和 5.3 小节所述。当然,仓库里的现成品也可以直接使用。

此外还有 docker-compose.yamldocker-compose-peer-base.yaml 文件,这就是这个小节要讲的重点。

注:docker-compose-peer-base.yaml 文件实际就是复制了 first-network/basepeer-base.yaml 文件,方便 docker-compose.yaml 的编写。

首先,docker-compose 文件是一个 YAML 文件,和 JSON 格式不同,但发挥的作用相似,起到配置文件的作用。与 JSON 不同的地方在于,它依靠冒号和严格的缩进来表示层级关系,用 - 表示数组。

提示:缩进对于 YAML 文件很重要。

其次,一个 docker compose 文件的作用是,可以同时控制多个 Docker 容器。例如在接下来的描述中,将看到使用 docker-compose 命令按照这个配置文件来启动,将会一次性批量地启动 6 个 Docker 容器;同样地,也可以一次性地关闭这 6 个容器。

就内容上而言,versionnetworks 不需要过多介绍。简单说,版本当前有 2 和 3。我们使用的是版本 2(官方的配置参考文档)。networks 就留个心眼那个 default 是在这个文件中指代这个网络。所以在一个 docker compose 文件中也可以指定多个网络,不同的网络也可以有一部分共同的参与者。

重头戏是整块的 services。在 services 中定义了 6 个节点,分别是 2 个排序节点、2 个属于 org1 的节点和 2 个属于 org2 的节点。每个节点都将作为一个独立的 Docker 容器运行。

orderer.lab805.comorderer2.lab805.com 两个条目基本相似,放在一块说,以 orderer.lab805.com 为例。

container_name 指定这个容器的名称,在容器运行后用 docker ps 查看容器列表时,NAMES 那一栏就是这个名称。同样可以用 docker logs -f <container_name> 来查看这个容器的日志,这个方法在 debug 时很有帮助。

extends 和面向对象的编程语言中的继承有着类似的作用,在这里就是复用一段配置。文件中指定的 file 和这个文件中的一个 service 相当于把 docker-compose-peer-base.yaml 文件中的 orderer-base 这一段内容复制粘贴了进来,对于不适用的部分只要重新编写,覆盖掉继承值即可。

volumes 是一个列表,负责目录和文件的映射。其中的每项可以冒号为分界线,冒号前是宿主机器中的位置,之后是 Docker 容器中的位置。宿主机在那个文件夹中的变更和 Docker 容器中的变更是双向互通的。
这里面值得一提的是,因为 orderer-base 中的指定,排序节点的 Docker 容器会在 /var/hyperledger/orderer/msp/var/hyperledger/orderer/tls 这两个固定位置找 MSP 和 TLS 证书,因此只要将宿主机器中 orderer.lab805.com 这一角色的相应证书正确地映射到这两个位置即可(反过来,也可以修改 orderer-base 的设置以适应其它的位置)。

ports 类似地,将前半部分宿主机的端口映射到 Docker 容器中的端口,即在宿主机中开的是前面的端口,而在 Docker 容器看来是后面的端口。

networks 中指定将这一服务应用到 default 这个网络,如果再写别的网络亦可应用在别的网络。

剩下的 4 个节点之间也很相似,因此仅以 peer0.org1.lab805.com 为例。

container_name 与之前相同,用于标识这个容器。

extends 这次复用了 docker-compose-peer-base.yaml 文件中的 peer-base 方案。

environment 设置了容器启动时自带的环境变量。一系列 CORE_PEER 开头的配置让容器在启动时,知道自己在 Fabric 网络中的角色,以及特别地,需要和哪个节点通信(CORE_PEER_GOSSIP 系列变量给了这个容器初始绑定的通信对象)。

volumes 作用相同,不过 MSP 和 TLS 证书位置分别变成了 /etc/hyperledger/fabric/msp/etc/hyperledger/fabric/tls,有别于 orderer.lab805.com 的设置。

ports 作用相同。

depends_on 如字面意思,需要等待前置容器启动完毕后,才会启动。

networks 中的设置将这一容器加入 default 网络。

如果在使用 docker-compose 命令启动 Docker 容器之前没有设置 COMPOSE_PROJECT_NAME 环境变量,则这个 Docker 网络的名称将是 docker-compose.yaml 所在文件夹加上 _default 后缀,就本例来说将会是 fixtures_default

docker-compose-peer-base.yaml 中的 peer-base 里,有一个环境变量叫 CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE。这个变量在后面实例化链码时会用到,其值必须与实际 Docker 网络名称相同才可以运行链码。具体将在后续这一环节再作介绍。

在与 docker-compose.yaml 同级的目录下:

  1. docker-compose up -d

  1. docker-compose -f docker-compose.yaml up -d

可按配置启动网络。因为 docker-compose.yamldocker-compose.yml 是这个命令的默认文件名,所以可以省略。

同理,关闭网络的命令如下。

  1. docker-compose down

  1. docker-compose -f docker-compose.yaml down

5.6 SDK 配置文件

指的是工程根目录下的 config-network.yaml 文件。关键信息都在一旁有备注,可先行阅读,以下内容仅作为补充使用。

channels 中定义了每个通道的成员节点以及通道中与连接有关的策略。

organizations 中定义了这个网络中参与的组织及它们的证书路径。特别需要注意的是,在例如 Org1users 的定义中,每个 user 的私钥需要按照实际私钥文件名进行手动设置(工程中的配置已与实际文件匹配)。

ordererspeers 中分别具体配置每个排序节点和普通节点的属性,包括其 URL 和 TLS 证书位置。

entityMatchers 的机制我也不很清楚,但是能确定的是没有不行。

5.7 SDK 工程

将解读仓库 https://gitee.com/czyczk/fabric-sdk-tutorial。以下小节将记录写作过程和思路,争取使得重现者也可以写出相同功能的内容。

这一节的所有路径都是相对于工程根目录的路径,如 go.modmain.go 位于工程根目录,文件夹中的文件会以如 internal/appinit/initInfo.go 的形式出现。

关键的函数和变量都附有注释,在阅读下文若出现疑惑可以配合注释一起参考。

5.7.1 go.mod

在工程目录下创建 Go 模块。

  1. go mod init gitee.com/czyczk/fabric-sdk-tutorial

最前面的步骤至少需要一个库,在 go.mod 中写:

  1. require github.com/hyperledger/fabric-sdk-go latest

在上面写 latest 字样的,在 Go 安装这些包时会自动替换为当前最新版,若一段时间后仍没有变化可以在终端中运行 go mod download 来手动完成下载。除需要特定版本的,在不出问题的情况下,都可使用 latest 来自动获取最新版。
这个包顾名思义,都是与 Fabric SDK 核心功能相关的。

之后的步骤还会用到其他包,但初始而言这个包足够了。

5.7.2 main 函数与 SDK 初始化

main 函数位于 main.go 文件。

开头的是拿到当前的工作目录 workingDirectory。关于这有两点值得一提:
第一,这里默认了用户的使用方式是从这个工程的根目录启动编译后的程序二进制,而非其他位置。因此,在整个程序中所需的文件,无论是指向 fixtures 中的文件,还是指向根目录下的 config-network.yaml 配置文件,都是基于 workingDirectory 来找的。
第二,通过检查函数的 error 类型返回值是否为 nil 来决定函数运行是否产生错误的方法,是 Go 语言典型的错误处理方式。迥异于 C++ 和 Java 式的 throw and catch 风格,Go 的错误处理风格更偏向 C 式。这种画风的代码块在整个程序中将大量出现。
对于 main 函数而言,这一步出了错就该优雅地退出程序,因此通过 log 包中的 Fatalln() 函数将错误输出到日志(没有额外指定,日志将输出到程序的标准错误通道 stderr),然后退出(而非产生 panic,类比于 Java 的未处理的 RuntimeException)。
对于其他场合,特别是在一些辅助函数中,产生的错误可能不以退出程序作为处理,可能指是生成一个错误消息,返回给调用者另作处理,就如同我们刚调用的 os.Getwd() 一样。

注:实际工程中用到的 log 都是由 github.com/sirupsen/logrus 这个包提供的,其基本用法与 Go 自带的 log 包相同,但多了结构化输出的功能。由于工程暂时没有使用结构化日志,故对此不作强调。使用 Go 自带的 log 包也可以达到相同的目的。

回顾之前启动网络之后的步骤,先是用通道配置文件创建通道,用其他的通道配置文件更新锚节点,然后将节点加入通道,在它们之中安装链码,最后在通道上实例化链码。接下来的程序就是准备做这些事,在调用 SDK 中函数的过程中会需要指定一些信息,如通道的名称、组织管理员账户的名称、排序节点地址等,为了不重复指定这些信息(也为了修改时方便),将这些信息以变量指定或封装在结构体中。
main.go 中第 25 行至 63 行从上往下分别出现负责通道配置的 ChannelInitInfo、负责组织用户名信息的 OrgInitInfo 以及负责链码信息的 ChaincodeInitInfo。这些结构体并非必须,仅仅是因为后续调用经常地需要用到,因此将它们封装起来,其内容和用途在遇到时会再详谈。

因为逻辑较长,将这一段封装在了 initApp() 函数中,接收的参数的含义稍后会从需求出发解释。

首先调用了 appinit 包中的 SetupSDK() 函数(在应用初始化阶段这一系列的 utils 类型的工具函数都位于 internal/appinit/utils.go 中)来创建 SDK 实例,并指定了 5.6 节中的 SDK 配置文件,这样这个 SDK 实例就知道了这个网络的通道和参与者,包括节点和用户的证书情况,从而使得通过 SDK 发出的请求可以包含我们在 3.2 中的那些证书信息,而不需要在每个请求中手动再附上。

SetupSDK() 函数中创建 SDK 实例的调用方法也很简单,即 internal/appinit/utils.go 中的第 22 行 fabsdk.New(configProvider)。在这里错误的处理方式就不是直接退出,而是生成错误字符串交由调用者自行处理了。值得注意的是,在这里的设计中,SDK 实例并不作为函数的返回值,而是放在全局变量 global 包下的 SDKInstance(定义见 internal/global/singletons.go)中作为单例存储起来。
这么做的考量主要是想避免 SDK 实例和后面的许多程序中只需要作为单例存在的实例被当作参数到处传播。更细节地考虑,这里以这种最简单的方式(全局变量)来实现是可行的,因为程序在实例化这些单例时是单线程顺序执行的,因此不需要考虑多线程造成的重复实例化问题。此外,没有使用 DI 或 IoC 工具的原因也仅是因为全局变量的方式对当前的程序逻辑足够了,这也符合 Go 语言简单设计的哲学。当然,当程序成长得更加精巧时,设计模式和容器库该上还是要上的。

小知识:Go 语言在设计上并不是面向对象的语言,因此不像 Java 那样将函数写在类中,然后将类名和文件名对应。Go 语言在实现功能时,如果能通过包级函数解决,可以不需要动用到类/结构体;而在组织文件的时候,鼓励使用简短的包名(要求全小写)来划分功能和决定函数与变量的可见度(如果一个函数和变量只需在包内使用,就不要大写开头),并且将做同一件事的函数放在一个文件中。

回到 initApp(),接下来的部分是为每个组织中的每个用户通过调用 appinit 包中的 InstantiateResMgmtClients() 函数来创建一个资源管理客户端实例,后文中也以类似 ResMgmtClient 的词指代,作用稍后再提。相关的关键 SDK 调用是 internal/appinit/utils.go 中的第 52 行 resmgmt.New(clientContext),这里的 clientContext 就是由参数提供的,每个组织中的每个用户都将走一遍这个流程。创建出来的实例作为全局变量放在 global 包下的 ResMgmtClientInstances
这个变量的类型是 map[string]map[string]*resmgmt.Client,看起来类型复杂,其实很好理解。第一层,提供一个 string 作为 key,可以拿到一个 map[string]*resmgmt.Client。第二层,提供一个 string 作为 key,可以拿到一个 *resmgmt.Client。即需要两个 string 值拿到一个资源管理客户端。注释里也提到了这点,从使用角度,先提供组织名,再提供用户名,可以得到相应的客户端,例如 global.ResMgmtClientInstances["org1"]["Admin"]

接下来便是通过 appinit 包中的 InstantiateMSPClients() 函数创建 MSP 客户端实例,后文中也以类似 MSPClient 的词指代,作用稍后再提。相关的关键 SDK 调用是 internal/appinit/utils.go 中的第 83 行 msp.New(clientCtx, msp.WithOrg(orgName)),与上面类似,也是每个组织的每个用户走一遍这个流程。创建出来的实例作为全局变量放在 global 包下的 MSPClientInstances。其类型与取用方法与 ResMgmtClientInstances 相同。

以上就是 SDK 实例、资源管理客户端实例和 MSP 客户端实例的创建,这一模式在后面的实例创建中还将出现。

回到 main 函数,在调用 initApp() 之后,调用 global.SDKInstance.Close()。这是因为 Go 语言虽然有垃圾回收机制免除了开发者手动释放内存的义务,但是一些资源的申请无法由这一机制回收,所以需要通过手动释放资源来保证安全。然而,程序才刚开始,SDK 实例在后续还要被用到,因此在前面加上 defer,这将使得这一调用在调用所在的函数(这里就 main 函数)结束时才触发。

小知识:Go 语言中的 defer 关键字会让后面跟的语句在函数结束时运行,而不是马上运行,然而语句中所使用的参数是立马估值的。看这两个例子很清晰地解释了它的行为。
https://tour.golang.org/flowcontrol/12
https://tour.golang.org/flowcontrol/13

这一部分就是 main.goconfigureChannel() 函数的内容,其中包括了三个部分:创建通道、应用组织的锚节点和将节点加入通道。
前两者的实质都一样,都是通过 appinit 包中的 ApplyChannelTx() 函数来应用一个通道配置交易文件,差异仅在应用的文件不同。
创建通道时用的是 channelInitInfo 中指定的 "${workingDirectory}/fixtures/channel-artifacts/channel.tx",而锚节点是 .../Org1MSPanchors.tx.../Org2MSPanchors.tx

在应用通道配置文件的时候需要用到 MSP 客户端和 ResMgmtClient,在更新锚节点时,组织使用自己的管理员(Admin)的 MSP 和资源管理客户端来完成操作。

关键 SDK 操作行数为 [113, 126]。这次是先创建一个请求结构体,其中填入参数所提供的信息,然后调用 ResMgmtClient 中的 SaveChannel() 方法来以组织管理员的身份向网络提交请求。

从中,我们可以看到 MSP 客户端在这里被用于得到签名身份(第 108 行),而 ResMgmtClient 用于应用通道配置文件(第 121 行)。本节最后还会有一个总结性的列表。

注:在准备 MSP 客户端的过程中都只用到了 github.com/hyperledger/fabric-sdk-go/pkg/client/msp 这个包。但是在创建通道,准备请求的时候,用到了一个同样叫作 msp 的包,但是来自于 github.com/hyperledger/fabric-sdk-go/pkg/common/providers/msp,因此在前面加个 providersmsp 的别名以示区分。

将节点加入通道的操作在 appinit 包中的 JoinChannel() 函数中。其中用到了 ResMgmtClient 的 JoinChannel() 函数(第 141 行),每个组织使用自己的管理员的客户端将自己的节点加入。

5.7.3 安装与实例化链码

链码是从 screw example 拷过来的,进行一些小修改。具体的修改如下:
chaincode/src/screw_example/screw_example.go 的第 71 行的函数 tranfer() 的第二个参数 args []string 现在接收 4 个 string,而非原来的 3 个。第 4 个 string 用于指定在交易完成后触发的事件。事件简单来说就是一个向所有观察者发送消息的信息源,在稍后用到的时候会再细谈。就这里而言,它意味着在调用链码时,要传的参数会是类似于 ["transfer", "CorpA", "CorpB", "10", "eventTransfer"] 这样。

因此,在第 73 行变更了参数列表长度的判定,在第 [118, 121] 行在返回成功之前触发了参数所指定的事件。

main.goinstallAndInstantiateChaincode() 函数包括了这一部分的内容。

原则上是只需要在交易发起者和背书节点上安装链码。这里为简单起见,会在所有节点上安装链码。
因此调用 appinit 包中的 InstallCC() 函数来安装链码,一次为一个组织内的节点安装。
安装链码过程中也用到了组织管理员的 ResMgmtClient,关键行在 internal/appinit/utils.go 的第 [159, 177] 行。首先将链码打包(第 160 行),然后生成安装链码请求(第 166 行),这两步用到了链码相关信息 ChaincodeInitInfo,所以在参数里提供了这个,最后通过管理员的 ResMgmtClient 来安装链码(第 174 行)。
值得注意的是,链码打包那一步,NewCCPackage() 函数的参数,前者叫 chaincodePath,后者叫 goPath,它会在 ${goPath}/src/${chaincodePath} 这样拼接出的位置去找链码。为什么会有 src 放在中间呢?这是因为 Fabric Go SDK 刚出现的时候 Go 还没有像 1.13 版本开始的 Go Module 这套依赖管理机制,当时的做法是把所有 Go 语言工程的代码写在 ${GOPATH}/src 下的,因此这里参数中的 goPath 对应的就是环境变量 $GOPATH,而 chaincodePath 就是在 ${GOPATH}/src 下的自定义的深层的路径。Go 语言在 1.13 版本之后,是推荐使用 Go Module 的机制来管理依赖的,因而不再需要把项目放在 ${GOPATH}/src 下,只要任何文件夹下有 go.mod 就可以认。而 Fabric SDK Go 在这一方面的设计显然没有很好地考虑,作为一个 workaround,我们只要把链码放在路径中包含 src 这一层的位置就可以,就如这个工程中,链码在 chaincode/src/screw_example 之下,因此往 goPath 里填的值是 ${workingDirectory}/chaincodechaincodePathscrew_examplemain.gochaincodeInitInfo 的定义(第 54 行)就是这么来的。

接下来是在通道上实例化链码。一个链码在通道上只需实例化一次,因此无论是组织 1 还是组织 2 只要一个有权限的代为操作就行。
在 SDK 调用中,还是需要组织的管理员的 ResMgmtClient。关键行在第 [199, 212] 行。还是老作法,先创建一个实例化请求,然后用 ResMgmtClient 的 InstantiateCC() 方法。

光实例化了链码并不够,为了能在通道上发号施令,还需要通道客户端,后文也称 ChannelClient。虽然现在暂时用不着,但在下一小节服务层中将会使用,这里提前准备。创建一个通道客户端包含了通道信息和组织信息,意味着查找一个 ChannelClient 比起之前的 ResMgmtClient 所需的组织名和用户名之外,还需要一个通道名称。因此在 internal/global/singletons.go 中定义的 ChannelClientInstances 的类型为 map[string]map[string]map[string]*channel.Client,查找时形如 global.ChannelClientInstances["mychannel"]["Org1"]["Admin"]

这个通道客户端的创建过程即 appinit 包中的 InstantiateChannelClient() 函数,关键 SDK 调用为第 239 行 channel.New()。得到的客户端放在全局变量中。

吐嘈:Fabric Go SDK 仍在 beta 阶段,包的安排一直在变化,悄无声息,每次一头雾水去翻官方文档,都是在不起眼的 import 堆里,从一大串大同小异的包名中列文虎克出来。

5.7.4 服务层:链码调用逻辑的封装与事件处理

从这一小节开始,就不主要是 SDK 调用,更多的是自定义的功能实现。实现同样功能的方法可以有很多,这里仅仅是提供一种可能的实现。

接下来的考虑是通过一个服务层,将对链码的调用封装起来,以达到对资源的统一管理,也方便对链码调用的复用。

一般而言,会为每一种要管理的资源写一种服务,例如用户和用户银行会有分别对应的服务。在这里我们只有螺丝这单一资源(甚至没对公司进行增删改查的业务),因此只需要针对螺丝这一个服务。它被定义在 service 包中的 ScrewService(位于文件 internal/service/screwService.go)。

这个类在创建时需要一个 *service.Info 来提供一些基本信息,此外它提供两个函数。TransferAndShowEvent() 负责调用链码的 transfer 功能,然后触发一个事件,返回交易 ID。Query() 负责调用链码的 query 功能,返回螺丝资产。

首先,这个 *service.Info 的内容很简单,提供一下这个服务会调用的链码的名称,以及要用哪个通道客户端。这里在设计上做了简化,没有考虑每一次调用可能由不同的用户发出的情况。在实际项目中,若在这一部分要求更精确的控制,则应作出调整。

然后,先讲解下 Query() 函数。本质很简单:接收一个代表公司名称的 string,生成一个通道请求,然后用通道客户端的 Query() 函数来发出查询请求(类比于之前用 cli 的 Docker 容器执行的 peer chaincode query 操作)。有错包在 error 里返回,否则把链码返回的 payload(在这是即螺丝库存量)作为 string 返回。

最后是 TransferAndShowEvent() 函数。这个函数可分为三个部分,先是注册监听一个事件(还记得修改链码 transfer() 函数,在返回之前添加了一个事件触发么?我们要监听的就是这个事件。)之后和上一个函数类似,准备一个通道请求(注意在第 37 行 Args 中,最后一个参数是就是对应传给链码的事件 ID)。然后用通道客户端的 Execute() 函数来发出交易请求(类比于之前执行的 peer chaincode invoke),其返回值中包含交易 ID,这一信息也将作为这个函数的返回值。最后是处理事件。

如果熟悉观察者模式(observer pattern)或者监听者模式(listener pattern)或者类似的词汇,其实就不难理解,事件的本质在这个模式的两方中就是信息源的那一方,它会向所有的注册者(subscribers)或说叫监听者(listeners)发送信息。
在通道管理器中就提供了成为一个事件的监听者的方法:RegisterChaincodeEvent()

internal/service/utils.go 文件中的帮助函数 registerEvent() 其实就是 RegisterChaincodeEvent() 的一个简单封装。从第 26 行可以看出,指定了通道、链码 ID 和事件 ID 后可以得到监听器,在返回的 3 个值中,第二个 notifier 就是。
notifier 是一个 <-chan *fab.CCEvent 类型。在 Go 语言中 chan 就像一个自来水管,有不断运载东西的能力,chan 后面跟着的是它运载的东西的类型,比如可以有 chan string,这里是 chan *fab.CCEvent。而 <-chan 则指明了这个水管的方向,是可能源源不断从里面流出新东西的水管(而 ->chan 就相反,你可以往里面扔新东西)。

这个水管(监听器)的用法在下一个函数 showEventResult() 中就有体现。Go 语言中通过 select 来进行 channel 操作(比起 C++ 和 Java 中的 switch 含义更广,不单可以匹配值,也可以匹配不同信源类型),这个 select 块会监听两个 channels,一个是我们的 notifier,另一个是超时计时器。在 select 块中,若没有 default 分支,一个 Go routine 在收到来自 channel 的信号之前是被阻塞的,因此如果它收到的信号来自 notifier 就会执行上面的 case,反之,若超时了,则它会先收到计时器的信号,则执行下面的 case
这种写法在 Go 语言中也经常出现,用一个计时器来防止无限时长地等待一个 channel 而阻塞 Go routine。

showEventResult() 体现了我们的一种事件处理方式:单纯地把事件打印到日志中去。从实际项目角度考虑,根据需求不同,收到 <-chan *fab.CCEvent 发来的事件之后还可以有更多的操作。举例来说,可以是一个通信模块在监听一个事件,当它收到事件之后,会发出一个 HTTP 请求到另一个服务器,来完成更复杂的操作。

事件的注册是需要手动释放资源的,因此在 internal/service/screwService.go 的第 30 行使用通道客户端的 UnregisterChaincodeEvent() 函数来销毁这一注册。同样用了 defer 来确保在事件被使用完毕后,函数结束的时候销毁。

为了简单起见,这里的写法很多是把东西写死的,实际项目中可能会需要更灵活的设计,在这里将几个可能的点点出。

  1. 链码所触发的/服务层所监听的事件的 ID。如 internal/service/screwService.go 的第 25 行。事实上,链码已经将事件 ID 作为一个参数处理,在服务层也可以使用相同的方法,将要监听的事件作为参数传入。
  2. 对事件的处理方法。如第 46 行。在收到某特定事件后,处理方法不一定总是一样的,可能根据业务需要不同而变更。Go 语言允许函数作为参数,因此需要时这里可以灵活变化。
  3. 可能每次请求来自不同的角色,故通道客户端也可以是动态的参数,而非作为 ScrewService 的一个属性。

5.7.5 阶段性小结

在之前的小节中出现了很多客户端,目前遇到的客户端包括资源管理客户端、MSP 客户端、通道客户端,外加一个 SDK 实例。这些类名字相似,但在整个应用初始化阶段被多次用到且发挥着不同的作用。这里对它们进行一个阶段性的小结。

名称 包.结构体名 函数(不含参数) 作用
资源管理客户端 resmgmt.Client SaveChannel() 应用通道配置文件
JoinChannel() 将组织内的节点加入通道
InstallCC() 为组织内的节点安装链码
InstantiateCC() 在通道上实例化链码
MSP 客户端 msp.Client GetSigningIdentity() 创建签名身份,用于签名(在创建通道时用到)
通道客户端 channel.Client Query() 使用链码查询账本
Execute() 使用链码执行一个发生账本写入的交易
RegisterChaincodeEvent() 得到一个事件的监听器
UnregisterChaincodeEvent() 销毁一个事件的监听器(释放资源)

5.7.6 控制器层:将服务层封装为 API 供前端使用

当前用于浏览器前端和服务器后端的交互方式中,最常用的方法包括 gRPC 和基于 HTTP 的 API。gRPC 较为新式,大量地使用二进制而非文本传输,比起一般的 HTTP 请求削减了请求头部的一些与请求无关的部分,这两点使得一个请求更为精炼,处理起来效率更高。在 Fabric 中节点之间的请求和回应就是用了 gRPC 协议。

gRPC 需要涉及到更多的概念以及工具,就篇幅和时间所限,这里暂不涉及,还是使用传统的 HTTP 方式(RESTful 风格的 API)来完成前后端交互。

RESTful 只是一种 API 风格,它对于提供一个标准化的 API 编排和命名方式起到促进作用。我们见到的 API 可能是形如 user/registerUser 或者 user/register.do 这样,但这些 API 并不符合 RESTful 的标准。

RESTful 的核心要义在于把要管理的中心词当作是一种资源,以用户为例,注册用户则是添加一个用户,查找和更新用户信息也是针对“用户”这个资源,那这个中心词就会成为这一系列 API 的共同部分,而不同的动作则通过 HTTP 的不同方法来区分,并且尽量使用标准的 HTTP 返回值来体现执行结果,一个可能的简易示例如下表所示。

功能 API HTTP 方法 参数 返回值
注册用户 /api/v1/user POST username, password (若要返回分配的用户 ID 则是 OK: 200,并在 payload 中写入用户 ID;若是无需返回则 Created: 201); 参数错误: Bad Request: 400
给定 ID 查询用户 /api/v1/user GET userId OK: 200 (带 payload);用户不存在:Not Found: 404
更新用户信息 /api/v1/user PUT userId, username, password 请求完成:No Content: 204;参数错误:Bad Request: 400

等等。

首先可以看到这些 API 都以 /api/v1 开头,这是为了给版本迭代留空间。如果是第一版可以从 v1 开始,后续版本为 v2v3 等。其次,都以中心词作为 API 对于约束 API 的编排很有帮助,只要通过枚举 HTTP 方法就可以创造出关于这个资源的众多语义,而无需再去探索使用 queryUser 还是 searchUser 更合适于搜寻一个用户。

在 HTTP 方法中,创建新的资源使用 POST,查询资源使用 GET,删除资源用 DELETE,更新资源(完全更新用 PUT,只提供更新部分用 PATCH)。这些方法基本可以满足大部分的语义,如果对资源有完全是其他语义的操作也可以用 POST 替代。此外,要获取一个资源的列表,方法毫无疑问是 GET,而路径可以有两种理解,一是使用这个资源的复数,如 /api/v1/users,也可以理解为这个资源的子资源 /api/v1/user/list。考虑到英文单词中存在单复数同形和其他不规则情况,本人更倾向于后者。

在参数方面,和传统的 HTTP 请求一样,若是 GET 方法则将参数放在 URL 后面,以 ? 作为参数分割线。例如 /api/v1/user?username=testname&address=testaddress,其他方法则将参数放在请求的 payload 中传输。

具体的返回码参考关于 HTTP 返回码的百科词条即可。就大框架而言,要知道的就两点。
第一个是数字的首位:成功的都是 2 开头,客户端错误(如参数错了或者没有权限)是 4 开头,服务器错误是 5 开头。
第二点是常用的返回码:

HTTP 返回码 含义/用途
200 成功(带返回值)
201 成功创建
204 成功(不带返回值)
400 参数错误
403 参数没问题但没权限
405 方法不支持(某资源只能 GET 和 POST 结果发了个 DELETE)
500 服务器内部错误(除超时外一般各种错误都算)
501 服务器还没实现功能(鸽子专用)
504 超时

基本上有这些背景,就可以设计和读懂 RESTful API 规划表格和文档了。

这一小节开始我们会使用 gin 做一个 HTTP 服务器,用来响应用 HTTP 协议发来的 API 请求。

go.mod 中补充上

  1. require github.com/gin-gonic/gin latest

保存后稍等片刻或手动运行 go mod download 来下载依赖。

这是一个高性能的用于 Go 语言的 HTTP 服务器,在其 GitHub 页面上有很详细的使用指引,基本用法都涵盖其中。接下来的内容就是讲述如何把这样一个服务器用在我们的项目中。

要点主要在 internal/controller/screwController.go

5.7.7 编译、运行程序与 makefile

5.8 前端页面

使用 Angular 做个简单的功能展示平台。

Changelog

2021-05-08:
+ 增加了 1.2.7 小节关于 IPFS 的多种安装方式。

2021-02-24:
! 修正了 3.2 小节中关于通信节点对象的概念解释。

2021-02-01:
! 修改了 1.2 小节的排版与新增了提示,避免在阅读时产生歧义。
+ 在 5.3.1.2 小节中增加了墙内编译 Fabric CA 的方法。

2021-01-26:
+ 新增 5.2.2.1 小节以方便在墙内使用 NVM 方式安装 Node 和 npm。

2020-11-05:
+ 释出了 5.5 和 5.6 小节。

2020-11-04:
+ 程序已完成,释出了 5.2 小节。

2020-10-22:
+ 释出了 5.1、5.3 和 5.4 小节。

2020-09-25:
! 修正了 go mod init main 的错误(不能使用 main 作为模块名)。
+ 新增了 4.4。

2020-09-24:
+ 新增了 4.3。
+ 新增了 4.1.3 关于启用 gopls 的内容。

2020-09-23:
! 更换了图床。

2020-09-22:
! 修正了 3.2 中关于 peer chaincode querypeer chaincode invoke 的概念解释的错误。
! 将 1.2.6 中现已不能使用的国内镜像源修改为可用的源。
+ 新增了 4.1 与 4.2。

TODO

2020-09-25:
M 补充 4.4.1 部分的代码解释。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注