Fabric 系统链码插件研究

作者 tinywell 日期 2019-02-27
Fabric 系统链码插件研究

写在前面

在 fabric 里面,有很多模块支持 plugin 的形式进行替换,实现了在不影响主程序的情况下自由扩展定制自己的关键模块,实现可插拔。
本文针对其中的系统链码插件的功能的部署使用进行研究。

理论研究

什么是系统链码

fabric 自 1.0 版本开始,将链码分为系统链码和普通链码两种。普通链码(智能合约)用于实现业务逻辑,而系统链码则是用于系统管理,例如 lsccqscc等。
与普通链码需要独立沙盒环境运行不同,系统链码在 peer 服务启动时随 peer 节点注册,同 peer 节点一起运行。
在 fabric 1.0 版本时,系统链码为固定的 5 个:lsccqscccsccvsccescc,这 5 个链码功能固定,分别用于链码生命周期管理、区块/交易查询、通道配置管理、交易背书和交易验证。

什么是系统链码插件

系统链码使用方便,但是由于其功能、逻辑固定,不利于扩展。在 1.1 版本开始,fabric 允许定制自己的 vsccescc,这样智能合约所能实现的交易模式会更加丰富。
实现这个功能就是 fabric 开始支持系统链码插件,通过插件的形式达到动态注册系统链码的目的。

如何开发系统链码插件

一个系统链码插件(system chaincode plugin)需要使用 go 语言,开发一个 system chaincode 的 plugin。(关于 go 语言 plugin 的介绍可以参看我之前的文章Golang笔记-Plugin初探

而 system chaincode 和普通 chaincode 一样,需要实现 Chaincode 接口。

1
2
3
4
type Chaincode interface {
Init(stub ChaincodeStubInterface) pb.Response
Invoke(stub ChaincodeStubInterface) pb.Response
}

与正常形式的链码略微不同的是,由于是 go 的插件,所以这个系统链码的 go 源码必须属于 main 包。

如何部署系统链码插件

peer 服务中跟系统链码插件相关的有两块配置:

  • chaincode.systemPlugin配置节中,关于系统链码的基本信息
1
2
3
4
5
6
7
chaincode:
systemPlugins:
- enabled: true
name: mysyscc
path: /opt/lib/syscc.so
invokableExternal: true
invokableCC2CC: true
  • chaincode.system 配置节中,关于是否启用某个系统链码的配置信息
    1
    2
    3
    chaincode:
    system:
    mysyscc: enable

所以我们需要三步操作来进行系统链码插件的部署:

  1. 准备系统链码代码并以 plugin 的形式进行编译;
  2. 在 peer 的配置文件的 chaincode.SystemPlugin 项下配置该插件的基本信息;
  3. 在 peer 的配置文件的 chaincode.system 项下启用该插件;

系统链码插件有什么用

上面说了这么多,对系统链码插件的开发、部署都有了必要的了解,但是我们似乎忘了一个问题-开发自己的系统链码有什么用呢?

回顾上面对系统链码的介绍,因为系统链码可以在 peer 节点中与 peer 进程共同运行,不需要沙盒环境,所以它可以访问比普通 chaincode 更多的资源:比如本地数据、账本信息、配置信息,甚至链外信息。

是的,我们可以通过系统链码来实现业务链码跟链外的交互。这将大大扩展智能合约的能力,而不在受限于其所运行的沙河环境。

实践记录

目标

基于以上认识,我想到了使用系统链码解决一个困扰许久的问题:通过系统链码来实现状态数据的导入导出、备份迁移等。

在这之前,我们的考虑过得数据备份迁移方案有两种:

  • 通过链码交易接口,以正常交易的形式进行数据的导入导出。但是受限于交易数据大小(区块大小)的限制,每次的导入导出数据量有限,数据量变大之后会很麻烦;
  • 通过文件系统,将数据备份到文件。这种方案由于链码运行于沙盒环境,其维护难度高,且在 1.0 之后已经没有途径可以控制沙盒容器的文件挂载;

而有了系统链码之后,我们可以集合两个方案并进行改进,将文件操作的部分放到系统链码中实现,而导入导出的逻辑在业务链码中,业务链码通过InvokeChaincode API 调用系统链码,实现数据导入导出。而系统链码所在的 peer 容器是很方便维护的。

本次实践基于这个目标进行模拟探索。

链码开发

首先准备系统链码插件部分的源码,一个实现了 Chaincode 接口且在 main 包中的 go 程序。核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (s *DataBackSCC) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
logger.Info("Invoke")
checkExist()
args := stub.GetStringArgs()
for _, s := range args {
logger.Infof("BackUp String: '%s'\n", s)
filePath := filepath.Join(backpath, backfile)
f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
logger.Error(err)
}
_, err = f.WriteString(s + "\n")
if err != nil {
logger.Error(err)
}
}
return shim.Success(nil)
}

这个系统链码主要实现的功能是将外部传进来的数据写入一个备份文件,完整代码请参考 databackscc.go

然后是普通的业务链码,调用系统链码中的功能实现数据的导出。核心代码如下:

1
2
3
4
5
func (s *DataBackCC) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
logger.Info("Invoke")
rsp := stub.InvokeChaincode("databackscc", [][]byte{[]byte(""), []byte(stub.GetTxID())}, stub.GetChannelID())
return shim.Success(rsp.GetPayload())
}

完整代码请参考 databackcc.go

链码部署

  1. 系统链码进行编译:
1
go build -buildmode=plugin
  1. 准备 peer 的配置文件 core.yaml 系统链码插件相关的配置信息:
1
2
3
4
5
6
7
...
systemPlugins:
- enabled: true
name: databackscc
path: /etc/hyperledger/fabric/plugin/databackscc.so
invokableExternal: true
invokableCC2CC: true
1
2
3
4
...
system:
...
databackscc: enable
  1. 准备 fabric 启动相关配置、脚本等

万事具备只欠东风,启动网络等待成功时刻。

这么顺利是不可能的,这是常识。如果真的这么顺利就成功了,那说明一定有哪个地方弄错了。

排雷记录

  • 插件功能未启用

第一个问题就是,默认的 peer 镜像是不支持系统链码插件功能,分析相关源码(github.com/hyperledger/fabric/core/scc/register_pluginsenabled.go)可知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// +build pluginsenabled,cgo
// +build darwin,go1.10 linux,go1.10 linux,go1.9,!ppc64le

/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package scc

// CreatePluginSysCCs creates all of the system chaincodes which are loaded by plugin
func CreatePluginSysCCs(p *Provider) []SelfDescribingSysCC {
var sdscs []SelfDescribingSysCC
for _, pscc := range loadSysCCs(p) {
sdscs = append(sdscs, &SysCCWrapper{SCC: pscc})
}
return sdscs
}

这段加载系统链码插件的核心代码是加了 build tag 的,而 makefile 中的默认编译方式均未有 pluginsenabled 的编译标签,因此我们需要重新编译镜像,增加 pluginsenabled

的标签,以启用插件功能。

1
GO_TAGS+=" pluginsenabled" make peer-docker
  • 插件编译失败

编译出新的 peer 镜像后,重新启动网络,终于在日志中看到加载系统链码插件的相关信息了,可是加载失败。这时回想起编译镜像时有警告信息:

1
2
3
4
5
6
7
Building .build/docker/bin/peer
# github.com/hyperledger/fabric/peer
/tmp/go-link-488306534/000006.o: In function `pluginOpen':
/workdir/go/src/plugin/plugin_dlopen.go:19: warning: Using 'dlopen' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-488306534/000021.o: In function `mygetgrouplist':
/workdir/go/src/os/user/getgrouplist_unix.go:16: warning: Using 'getgrouplist' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
...

在静态编译方式下使用了动态链接库,所以这个插件实际上是不OK的。经过艰苦的研(goo)究(gle)之后,终于找到原因并解决。

peer 的镜像编译方式中,采用了静态编译的模式,但是其原生可执行文件却不是这样。所以需要在编译 peer 镜像时指定采用动态编译的方式:

1
DOCKER_DYNAMIC_LINK=true GO_TAGS+=" pluginsenabled" make peer-docker

成功生成新的具备系统链码插件功能的 peer 镜像。

  • 插件依赖包版本不一致问题

根据官方文档说明,插件所用的依赖包必须和主程序所用的依赖包版本相同。

为了编译方便,最开始我将插件的依赖包导入了 vendor 包中,虽然引用的依赖包和所编译的 peer 为同一版本,但是 go 识别为 $GOPATH 下 vendor 中的为不同版本。因此放弃 vendor,同时将插件源码和 peer 编译环境放在一起,在 peer 编译环境下编译插件,这样保证 peer 和 plugin 的依赖包一致。

运行之后,依然是依赖包版本不一致的问题:

1
2
panic: Error opening plugin at path /etc/hyperledger/fabric/plugin/databackscc.so: plugin.Open("/etc/hyperledger/fabric/plugin/databackscc"): plugin was built with a different version of package github.com/hyperledger/fabric/vendor/github.com/golang/protobuf/proto
...

再一次经过艰苦的研(goo)究(gle)之后,原理是 go 的 plugin 功能在对标准库和第三方库的依赖包处理上略有不同:

  • 针对标准库的依赖包,plugin 中只会记录 $GOROOT 向下的相对路径;
  • 针对第三方库的依赖包,plugin 会记录完整 $GOPATH 及包的完整路径。这是因为 $GOPATH 允许多个,为了避免混淆,会记录每个依赖包的完整路径;

而我编译插件的环境的 $GOPATH 与 peer 运行环境的 $GOPATH 不一样,导致 go 认为两者使用了不同版本的依赖包。(有点奇怪)

一种解决方案是,将插件的编译直接放到 peer 容器中,即 peer 的运行环境,这样两者的依赖包信息绝对一样,这个方案稍显麻烦(启动一个用于编译的 peer 容器略微麻烦)。

我采用的解决方案是,仿造一个跟 peer 运行环境一个 $GOPATH 出来即可。

在原插件编译环境中构建一个 peer 的 $OGPATH 相同的路径,然后将插件源码和依赖包拷贝到这个临时的 $GOPATH 下,临时指定 $GOPATH 变量进行编译。

1
GOPATH=/opt/gopath go build -buildmode=plugin -o databackscc.so databackscc.go

实践结果

系统链码加载日志:

1
2
3
4
5
6
7
8
019-02-27 03:08:12.342 UTC [sccapi] deploySysCC -> INFO 017 system chaincode lscc/(github.com/hyperledger/fabric/core/scc/lscc) deployed
2019-02-27 03:08:12.342 UTC [cscc] Init -> INFO 018 Init CSCC
2019-02-27 03:08:12.342 UTC [sccapi] deploySysCC -> INFO 019 system chaincode cscc/(github.com/hyperledger/fabric/core/scc/cscc) deployed
2019-02-27 03:08:12.343 UTC [qscc] Init -> INFO 01a Init QSCC
2019-02-27 03:08:12.343 UTC [sccapi] deploySysCC -> INFO 01b system chaincode qscc/(github.com/hyperledger/fabric/core/scc/qscc) deployed
2019-02-27 03:08:12.343 UTC [sccapi] deploySysCC -> INFO 01c system chaincode (+lifecycle,github.com/hyperledger/fabric/core/chaincode/lifecycle) disabled
2019-02-27 03:08:12.343 UTC [DataBackSCC] Info -> INFO 001 Init Success
2019-02-27 03:08:12.343 UTC [sccapi] deploySysCC -> INFO 01d system chaincode databackscc/(/etc/hyperledger/fabric/plugin/databackscc.so) deployed

调用系统链码日志:

1
2
3
4
5
6
2019-02-27 03:10:27.756 UTC [endorser] callChaincode -> INFO 049 [mychannel][b5660446] Entry chaincode: name:"databackcc" 
2019-02-27 03:10:27.759 UTC [DataBackSCC] Info -> INFO 003 Invoke
2019-02-27 03:10:27.759 UTC [DataBackSCC] Infof -> INFO 004 Not Exist, Create Dir /data/backup
2019-02-27 03:10:27.759 UTC [DataBackSCC] Infof -> INFO 005 Create File /data/backup/backup.txt
2019-02-27 03:10:27.759 UTC [DataBackSCC] Infof -> INFO 006 BackUp String: ''
2019-02-27 03:10:27.759 UTC [DataBackSCC] Infof -> INFO 007 BackUp String: 'b5660446c2ebdc43c113e6ffe09965e6ff7707ee711157332427d10da5ae3c91'

数据备份结果:

1
2
3
root@784af02a43be:/opt/gopath/src/github.com/hyperledger/fabric/peer# cat  /data/backup/backup.txt

b5660446c2ebdc43c113e6ffe09965e6ff7707ee711157332427d10da5ae3c91

总结

  1. 系统链码插件功能的启用,需要添加 pluginsenabled 编译标签重新编译;
  2. 插件的编译需要保证合适的环境,保证依赖包相同;
  3. 系统链码的调用生效是在链码模拟执行阶段,跟业务链码略微不同;
  4. 系统链码插件的功能能大大拓展 fabric 中合约的能力,它能够打破业务链码的运行环境,实现更多资源的访问,甚至于链外资源进行交互。(比如ipfs等)

参考资料