Fabric 系統(tǒng)鏈碼插件研究

寫在前面

在 fabric 里面,有很多模塊支持 plugin 的形式進行替換,實現(xiàn)了在不影響主程序的情況下自由擴展定制自己的關鍵模塊,實現(xiàn)可插拔。
本文針對其中的系統(tǒng)鏈碼插件的功能的部署使用進行研究。

理論研究

什么是系統(tǒng)鏈碼

fabric 自 1.0 版本開始,將鏈碼分為系統(tǒng)鏈碼和普通鏈碼兩種。普通鏈碼(智能合約)用于實現(xiàn)業(yè)務邏輯,而系統(tǒng)鏈碼則是用于系統(tǒng)管理,例如 lscc、qscc等。
與普通鏈碼需要獨立沙盒環(huán)境運行不同,系統(tǒng)鏈碼在 peer 服務啟動時隨 peer 節(jié)點注冊,同 peer 節(jié)點一起運行。
在 fabric 1.0 版本時,系統(tǒng)鏈碼為固定的 5 個:lscc、qscccscc、vsccescc,這 5 個鏈碼功能固定,分別用于鏈碼生命周期管理、區(qū)塊/交易查詢、通道配置管理、交易背書和交易驗證。

什么是系統(tǒng)鏈碼插件

系統(tǒng)鏈碼使用方便,但是由于其功能、邏輯固定,不利于擴展。在 1.1 版本開始,fabric 允許定制自己的 vsccescc,這樣智能合約所能實現(xiàn)的交易模式會更加豐富。
實現(xiàn)這個功能就是 fabric 開始支持系統(tǒng)鏈碼插件,通過插件的形式達到動態(tài)注冊系統(tǒng)鏈碼的目的。

如何開發(fā)系統(tǒng)鏈碼插件

一個系統(tǒng)鏈碼插件(system chaincode plugin)需要使用 go 語言,開發(fā)一個 system chaincode 的 plugin。(關于 go 語言 plugin 的介紹可以參看我之前的文章Golang筆記-Plugin初探

而 system chaincode 和普通 chaincode 一樣,需要實現(xiàn) Chaincode 接口。

type Chaincode interface {
    Init(stub ChaincodeStubInterface) pb.Response
    Invoke(stub ChaincodeStubInterface) pb.Response
}

與正常形式的鏈碼略微不同的是,由于是 go 的插件,所以這個系統(tǒng)鏈碼的 go 源碼必須屬于 main 包。

如何部署系統(tǒng)鏈碼插件

peer 服務中跟系統(tǒng)鏈碼插件相關的有兩塊配置:

  • chaincode.systemPlugin配置節(jié)中,關于系統(tǒng)鏈碼的基本信息
  chaincode:
    systemPlugins:
      - enabled: true
        name: mysyscc
        path: /opt/lib/syscc.so
        invokableExternal: true
        invokableCC2CC: true
  • chaincode.system 配置節(jié)中,關于是否啟用某個系統(tǒng)鏈碼的配置信息
  chaincode:
    system:
      mysyscc: enable

所以我們需要三步操作來進行系統(tǒng)鏈碼插件的部署:

  1. 準備系統(tǒng)鏈碼代碼并以 plugin 的形式進行編譯;
  2. 在 peer 的配置文件的 chaincode.SystemPlugin 項下配置該插件的基本信息;
  3. 在 peer 的配置文件的 chaincode.system 項下啟用該插件;

系統(tǒng)鏈碼插件有什么用

上面說了這么多,對系統(tǒng)鏈碼插件的開發(fā)、部署都有了必要的了解,但是我們似乎忘了一個問題-開發(fā)自己的系統(tǒng)鏈碼有什么用呢?

回顧上面對系統(tǒng)鏈碼的介紹,因為系統(tǒng)鏈碼可以在 peer 節(jié)點中與 peer 進程共同運行,不需要沙盒環(huán)境,所以它可以訪問比普通 chaincode 更多的資源:比如本地數(shù)據、賬本信息、配置信息,甚至鏈外信息。

是的,我們可以通過系統(tǒng)鏈碼來實現(xiàn)業(yè)務鏈碼跟鏈外的交互。這將大大擴展智能合約的能力,而不在受限于其所運行的沙河環(huán)境。

實踐記錄

目標

基于以上認識,我想到了使用系統(tǒng)鏈碼解決一個困擾許久的問題:通過系統(tǒng)鏈碼來實現(xiàn)狀態(tài)數(shù)據的導入導出、備份遷移等。

在這之前,我們的考慮過得數(shù)據備份遷移方案有兩種:

  • 通過鏈碼交易接口,以正常交易的形式進行數(shù)據的導入導出。但是受限于交易數(shù)據大?。▍^(qū)塊大?。┑南拗?,每次的導入導出數(shù)據量有限,數(shù)據量變大之后會很麻煩;
  • 通過文件系統(tǒng),將數(shù)據備份到文件。這種方案由于鏈碼運行于沙盒環(huán)境,其維護難度高,且在 1.0 之后已經沒有途徑可以控制沙盒容器的文件掛載;

而有了系統(tǒng)鏈碼之后,我們可以集合兩個方案并進行改進,將文件操作的部分放到系統(tǒng)鏈碼中實現(xiàn),而導入導出的邏輯在業(yè)務鏈碼中,業(yè)務鏈碼通過InvokeChaincode API 調用系統(tǒng)鏈碼,實現(xiàn)數(shù)據導入導出。而系統(tǒng)鏈碼所在的 peer 容器是很方便維護的。

本次實踐基于這個目標進行模擬探索。

鏈碼開發(fā)

首先準備系統(tǒng)鏈碼插件部分的源碼,一個實現(xiàn)了 Chaincode 接口且在 main 包中的 go 程序。核心代碼如下:

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)
}

這個系統(tǒng)鏈碼主要實現(xiàn)的功能是將外部傳進來的數(shù)據寫入一個備份文件,完整代碼請參考 databackscc.go

然后是普通的業(yè)務鏈碼,調用系統(tǒng)鏈碼中的功能實現(xiàn)數(shù)據的導出。核心代碼如下:

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. 系統(tǒng)鏈碼進行編譯:
go build -buildmode=plugin
  1. 準備 peer 的配置文件 core.yaml 系統(tǒng)鏈碼插件相關的配置信息:
...
    systemPlugins:
      - enabled: true
        name: databackscc
        path: /etc/hyperledger/fabric/plugin/databackscc.so
        invokableExternal: true
        invokableCC2CC: true
...
    system:
        ...
        databackscc: enable
  1. 準備 fabric 啟動相關配置、腳本等

萬事具備只欠東風,啟動網絡等待成功時刻。

...

這么順利是不可能的,這是常識。如果真的這么順利就成功了,那說明一定有哪個地方弄錯了。

排雷記錄

  • 插件功能未啟用

第一個問題就是,默認的 peer 鏡像是不支持系統(tǒng)鏈碼插件功能,分析相關源碼(github.com/hyperledger/fabric/core/scc/register_pluginsenabled.go)可知:

// +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
}

這段加載系統(tǒng)鏈碼插件的核心代碼是加了 build tag 的,而 makefile 中的默認編譯方式均未有 pluginsenabled 的編譯標簽,因此我們需要重新編譯鏡像,增加 pluginsenabled

的標簽,以啟用插件功能。

GO_TAGS+=" pluginsenabled" make peer-docker
  • 插件編譯失敗

編譯出新的 peer 鏡像后,重新啟動網絡,終于在日志中看到加載系統(tǒng)鏈碼插件的相關信息了,可是加載失敗。這時回想起編譯鏡像時有警告信息:

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
...

在靜態(tài)編譯方式下使用了動態(tài)鏈接庫,所以這個插件實際上是不OK的。經過艱苦的研(goo)究(gle)之后,終于找到原因并解決。

peer 的鏡像編譯方式中,采用了靜態(tài)編譯的模式,但是其原生可執(zhí)行文件卻不是這樣。所以需要在編譯 peer 鏡像時指定采用動態(tài)編譯的方式:

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

成功生成新的具備系統(tǒng)鏈碼插件功能的 peer 鏡像。

  • 插件依賴包版本不一致問題

根據官方文檔說明,插件所用的依賴包必須和主程序所用的依賴包版本相同。

為了編譯方便,最開始我將插件的依賴包導入了 vendor 包中,雖然引用的依賴包和所編譯的 peer 為同一版本,但是 go 識別為 $GOPATH 下 vendor 中的為不同版本。因此放棄 vendor,同時將插件源碼和 peer 編譯環(huán)境放在一起,在 peer 編譯環(huán)境下編譯插件,這樣保證 peer 和 plugin 的依賴包一致。

運行之后,依然是依賴包版本不一致的問題:

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 允許多個,為了避免混淆,會記錄每個依賴包的完整路徑;

而我編譯插件的環(huán)境的 GOPATH 與 peer 運行環(huán)境的GOPATH 不一樣,導致 go 認為兩者使用了不同版本的依賴包。(有點奇怪)

一種解決方案是,將插件的編譯直接放到 peer 容器中,即 peer 的運行環(huán)境,這樣兩者的依賴包信息絕對一樣,這個方案稍顯麻煩(啟動一個用于編譯的 peer 容器略微麻煩)。

我采用的解決方案是,仿造一個跟 peer 運行環(huán)境一個 $GOPATH 出來即可。

在原插件編譯環(huán)境中構建一個 peer 的 OGPATH 相同的路徑,然后將插件源碼和依賴包拷貝到這個臨時的GOPATH 下,臨時指定 $GOPATH 變量進行編譯。

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

實踐結果

系統(tǒng)鏈碼加載日志:

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

調用系統(tǒng)鏈碼日志:

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'

數(shù)據備份結果:

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

b5660446c2ebdc43c113e6ffe09965e6ff7707ee711157332427d10da5ae3c91

總結

  1. 系統(tǒng)鏈碼插件功能的啟用,需要添加 pluginsenabled 編譯標簽重新編譯;
  2. 插件的編譯需要保證合適的環(huán)境,保證依賴包相同;
  3. 系統(tǒng)鏈碼的調用生效是在鏈碼模擬執(zhí)行階段,跟業(yè)務鏈碼略微不同;
  4. 系統(tǒng)鏈碼插件的功能能大大拓展 fabric 中合約的能力,它能夠打破業(yè)務鏈碼的運行環(huán)境,實現(xiàn)更多資源的訪問,甚至于鏈外資源進行交互。(比如ipfs等)

參考資料

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容