Tendermint Core Golang應用開發教程「含源代碼」

Tendermint Core是一個用Go語言開發的支持拜占庭容錯/BFT的區塊鏈中間件, 用於在一組節點之間安全地複製狀態機/FSM。Tendermint Core的出色之處 在於它是第一個實現BFT的區塊鏈共識引擎,並且始終保持這一清晰的定位。 這個指南將介紹如何使用Go語言開發一個基於Tendermint Core的區塊鏈應用。

Tendermint Core為區塊鏈應用提供了極其簡潔的開發接口,支持各種開發語言, 是開發自有公鏈/聯盟鏈/私鏈的首選方案,例如Cosmos、Binance Chain、 Hyperledger Burrow、Ethermint等均採用Tendermint Core共識引擎。

雖然Tendermint Core支持任何語言開發的狀態機,但是如果採用Go之外的 其他開發語言編寫狀態機,那麼應用就需要通過套接字或gRPC與Tendermint Core通信,這會造成額外的性能損失。而採用Go語言開發的狀態機可以和 Tendermint Core運行在同一進程中,因此可以得到最好的性能。

相關鏈接: Tendermint 區塊鏈開發詳解 | 本教程源代碼下載


1、安裝Go開發環境

請參考官方文檔安裝Go開發環境。

確認你已經安裝了最新版的Go:

<code>$ go versiongo version go1.12.7 darwin/amd64/<code>

確認你正確設置了GOPATH環境變量:

<code>$ echo $GOPATH/Users/melekes/go/<code>


2、創建Go項目

首先創建一個新的Go語言項目:

<code>$ mkdir -p $GOPATH/src/github.com/me/kvstore$ cd $GOPATH/src/github.com/me/kvstore/<code>

在example目錄創建main.go文件,內容如下:

<code>package mainimport ("fmt")func main() {fmt.Println("Hello, Tendermint Core")}/<code>

運行上面代碼,將在標準輸出設備顯示指定的字符串:

<code>$ go run main.goHello, Tendermint Core/<code>


3、編寫Tendermint Core應用

Tendermint Core與應用之間通過ABCI(Application Blockchain Interface)通信, 使用的報文消息類型都定義在protobuf文件中,因此基於Tendermint Core可以運行任何 語言開發的應用。

創建文件app.go,內容如下:

<code>package mainimport (abcitypes "github.com/tendermint/tendermint/abci/types")type KVStoreApplication struct {}var _ abcitypes.Application = (*KVStoreApplication)(nil)func NewKVStoreApplication() *KVStoreApplication {return &KVStoreApplication{}}func (KVStoreApplication) Info(req abcitypes.RequestInfo) abcitypes.ResponseInfo {return abcitypes.ResponseInfo{}}func (KVStoreApplication) SetOption(req abcitypes.RequestSetOption) abcitypes.ResponseSetOption {return abcitypes.ResponseSetOption{}}func (KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx {return abcitypes.ResponseDeliverTx{Code: 0}}func (KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx {return abcitypes.ResponseCheckTx{Code: 0}}func (KVStoreApplication) Commit() abcitypes.ResponseCommit {return abcitypes.ResponseCommit{}}func (KVStoreApplication) Query(req abcitypes.RequestQuery) abcitypes.ResponseQuery {return abcitypes.ResponseQuery{Code: 0}}func (KVStoreApplication) InitChain(req abcitypes.RequestInitChain) abcitypes.ResponseInitChain {return abcitypes.ResponseInitChain{}}func (KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock {return abcitypes.ResponseBeginBlock{}}func (KVStoreApplication) EndBlock(req abcitypes.RequestEndBlock) abcitypes.ResponseEndBlock {return abcitypes.ResponseEndBlock{}}/<code>

接下來我們逐個解讀上述方法並添加必要的實現邏輯。


3、CheckTx

當一個新的交易進入Tendermint Core時,它會要求應用先進行檢查,比如驗證格式、簽名等。

<code>func (app *KVStoreApplication) isValid(tx []byte) (code uint32) {// check formatparts := bytes.Split(tx, []byte("="))if len(parts) != 2 {return 1}    key, value := parts[0], parts[1]    // check if the same key=value already existserr := app.db.View(func(txn *badger.Txn) error {item, err := txn.Get(key)if err != nil && err != badger.ErrKeyNotFound {return err}if err == nil {return item.Value(func(val []byte) error {if bytes.Equal(val, value) {code = 2}return nil})}return nil})  if err != nil {panic(err)}    return code}func (app *KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx {code := app.isValid(req.Tx)return abcitypes.ResponseCheckTx{Code: code, GasWanted: 1}}/<code> 

如果進來的交易格式不是{bytes}={bytes},我們將返回代碼1。如果指定的key和value 已經存在,我們返回代碼2。對於其他情況我們返回代碼0表示交易有效 —— 注意Tendermint Core會將返回任何非零代碼的交易視為無效交易。

有效的交易最終將被提交,我們使用badger 作為底層的鍵/值庫,badger是一個嵌入式的快速KV數據庫。

<code>import "github.com/dgraph-io/badger"type KVStoreApplication struct {db           *badger.DBcurrentBatch *badger.Txn}func NewKVStoreApplication(db *badger.DB) *KVStoreApplication {return &KVStoreApplication{db: db,}}/<code>


4、BeginBlock -> DeliverTx -> EndBlock -> Commit

當Tendermint Core確定了新的區塊後,它會分三次調用應用:

  • BeginBlock:區塊開始時調用
  • DeliverTx:每個交易時調用
  • EndBlock:區塊結束時調用

注意,DeliverTx是異步調用的,但是響應是有序的。

<code>func (app *KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock {app.currentBatch = app.db.NewTransaction(true)return abcitypes.ResponseBeginBlock{}}/<code>

下面的代碼創建一個數據操作批次,用來存儲區塊交易:

<code>func (app *KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx {code := app.isValid(req.Tx)if code != 0 {return abcitypes.ResponseDeliverTx{Code: code}}  parts := bytes.Split(req.Tx, []byte("="))  key, value := parts[0], parts[1]    err := app.currentBatch.Set(key, value)if err != nil {panic(err)}    return abcitypes.ResponseDeliverTx{Code: 0}}/<code>

如果交易的格式錯誤,或者已經存在相同的鍵/值對,那麼我們仍然返回 非零代碼,否則,我們將該交易加入操作批次。

在目前的設計中,區塊中可以包含不正確的交易 —— 那些通過了CheckTx檢查 但是DeliverTx失敗的交易,這樣做是出於性能的考慮。

注意,我們不能在DeliverTx中提交交易,因為在這種情況下Query可能會 由於被併發調用而返回不一致的數據,例如,Query會提示指定的值已經存在, 而實際的區塊還沒有真正提交。

Commit用來通知應用來持久化新的狀態。

<code>func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit {app.currentBatch.Commit()return abcitypes.ResponseCommit{Data: []byte{}}}/<code>


5、查詢 - Query

當客戶端應用希望瞭解指定的鍵/值對是否存在時,它會調用Tendermint Core 的RPC接口 /abci_query 進行查詢, 該接口會調用應用的Query方法。

基於Tendermint Core的應用可以自由地提供其自己的API。不過使用Tendermint Core 作為代理,客戶端應用利用Tendermint Core的統一API的優勢。另外,客戶端也不需要 調用其他額外的Tendermint Core API來獲得進一步的證明。

注意在下面的代碼中我們沒有包含證明數據。

<code>func (app *KVStoreApplication) Query(reqQuery abcitypes.RequestQuery) (resQuery abcitypes.ResponseQuery) {resQuery.Key = reqQuery.Dataerr := app.db.View(func(txn *badger.Txn) error {item, err := txn.Get(reqQuery.Data)if err != nil && err != badger.ErrKeyNotFound {return err}if err == badger.ErrKeyNotFound {resQuery.Log = "does not exist"} else {return item.Value(func(val []byte) error {resQuery.Log = "exists"resQuery.Value = valreturn nil})}return nil})if err != nil {panic(err)}return}/<code>


6、在同一進程內啟動Tendermint Core和應用實例

將以下代碼加入main.go文件:

<code>package mainimport ("flag""fmt""os""os/signal""path/filepath""syscall"  "github.com/dgraph-io/badger""github.com/pkg/errors""github.com/spf13/viper"  abci "github.com/tendermint/tendermint/abci/types"cfg "github.com/tendermint/tendermint/config"tmflags "github.com/tendermint/tendermint/libs/cli/flags""github.com/tendermint/tendermint/libs/log"nm "github.com/tendermint/tendermint/node""github.com/tendermint/tendermint/p2p""github.com/tendermint/tendermint/privval""github.com/tendermint/tendermint/proxy")var configFile stringfunc init() {flag.StringVar(&configFile, "config", "$HOME/.tendermint/config/config.toml", "Path to config.toml")}func main() {db, err := badger.Open(badger.DefaultOptions("/tmp/badger"))if err != nil {fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err)os.Exit(1)}defer db.Close()  app := NewKVStoreApplication(db)    flag.Parse()    node, err := newTendermint(app, configFile)  if err != nil {fmt.Fprintf(os.Stderr, "%v", err)os.Exit(2)}    node.Start()  defer func() {node.Stop()node.Wait()`}()  c := make(chan os.Signal, 1)signal.Notify(c, os.Interrupt, syscall.SIGTERM)/<code>

這段代碼很長,讓我們分開來介紹。

首先,初始化Badger數據庫,然後創建應用實例:

<code>db, err := badger.Open(badger.DefaultOptions("/tmp/badger"))if err != nil {fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err)os.Exit(1)}defer db.Close()app := NewKVStoreApplication(db)/<code>

接下來使用下面的代碼創建Tendermint Core的Node實例:

<code>flag.Parse()node, err := newTendermint(app, configFile)if err != nil {fmt.Fprintf(os.Stderr, "%v", err)os.Exit(2)}...// create nodenode, err := nm.NewNode(config,pv,nodeKey,proxy.NewLocalClientCreator(app),nm.DefaultGenesisDocProviderFunc(config),nm.DefaultDBProvider,nm.DefaultMetricsProvider(config.Instrumentation),logger)  if err != nil {return nil, errors.Wrap(err, "failed to create new Tendermint node")}/<code>

NewNode方法用來創建一個全節點實例,它需要傳入一些參數,例如配置文件、 私有驗證器、節點密鑰等。

注意我們使用proxy.NewLocalClientCreator來創建一個本地客戶端,而不是使用 套接字或gRPC來與Tendermint Core通信。

下面的代碼使用viper來讀取配置文件,我們 將在下面使用tendermint的init命令來生成。

<code>config := cfg.DefaultConfig()config.RootDir = filepath.Dir(filepath.Dir(configFile))viper.SetConfigFile(configFile)if err := viper.ReadInConfig(); err != nil {return nil, errors.Wrap(err, "viper failed to read config file")}if err := viper.Unmarshal(config); err != nil {return nil, errors.Wrap(err, "viper failed to unmarshal config")}if err := config.ValidateBasic(); err != nil {return nil, errors.Wrap(err, "config is invalid")}/<code>

我們使用FilePV作為私有驗證器,通常你應該使用SignerRemote鏈接到 一個外部的HSM設備。

<code>pv := privval.LoadFilePV(config.PrivValidatorKeyFile(),config.PrivValidatorStateFile(),)/<code>

nodeKey用來在Tendermint的P2P網絡中標識當前節點。

<code>nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile())if err != nil {return nil, errors.Wrap(err, "failed to load node's key")}/<code>

我們使用內置的日誌記錄器:

<code>logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))var err errorlogger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel())if err != nil {return nil, errors.Wrap(err, "failed to parse log level")}/<code>

最後,我們啟動節點並添加一些處理邏輯,以便在收到SIGTERM或Ctrl-C時 可以優雅地關閉。

<code>node.Start()defer func() {node.Stop()node.Wait()}()c := make(chan os.Signal, 1)signal.Notify(c, os.Interrupt, syscall.SIGTERM)/<code>


7、項目依賴管理、構建、配置生成和啟動

我們使用go module進行項目依賴管理:

<code>$ go mod init hubwiz.com/tendermint-go/demo$ go build/<code>

上面的命令將解析項目依賴並執行構建過程。

要創建默認的配置文件,可以執行tendermint init命令。但是 在開始之前,我們需要安裝Tendermint Core。

<code>$ rm -rf /tmp/example$ cd $GOPATH/src/github.com/tendermint/tendermint$ make install$ TMHOME="/tmp/example" tendermint initI[2019-07-16|18:40:36.480] Generated private validator                  module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.jsonI[2019-07-16|18:40:36.481] Generated node key                           module=main path=/tmp/example/config/node_key.jsonI[2019-07-16|18:40:36.482] Generated genesis file                       module=main path=/tmp/example/config/genesis.json/<code>

現在可以啟動我們的一體化Tendermint Core應用了:

<code>$ ./demo -config "/tmp/example/config/config.toml"badger 2019/07/16 18:42:25 INFO: All 0 tables opened in 0sbadger 2019/07/16 18:42:25 INFO: Replaying file id: 0 at offset: 0badger 2019/07/16 18:42:25 INFO: Replay took: 695.227sE[2019-07-16|18:42:25.818] Couldn't connect to any seeds                module=p2pI[2019-07-16|18:42:26.853] Executed block                               module=state height=1 validTxs=0 invalidTxs=0I[2019-07-16|18:42:26.865] Committed state                              module=state height=1 txs=0 appHash=/<code>

現在可以打開另一個終端,嘗試發送一個交易:

<code>$ curl -s 'localhost:26657/broadcast_tx_commit?tx="tendermint=rocks"'{  "jsonrpc": "2.0",  "id": "",  "result": {    "check_tx": {      "gasWanted": "1"    },    "deliver_tx": {},    "hash": "1B3C5A1093DB952C331B1749A21DCCBB0F6C7F4E0055CD04D16346472FC60EC6",    "height": "128"  }}/<code>

響應中應當會包含交易提交的區塊高度。

現在讓我們檢查指定的鍵是否存在並返回其對應的值:

<code>$ curl -s 'localhost:26657/abci_query?data="tendermint"'{  "jsonrpc": "2.0",  "id": "",  "result": {    "response": {      "log": "exists",      "key": "dGVuZGVybWludA==",      "value": "cm9ja3M="    }  }}/<code>

“dGVuZGVybWludA==” 和“cm9ja3M=” 都是base64編碼的,分別對應於 “tendermint” 和“rocks” 。


8、小結

在這個指南中,我們學習瞭如何使用Go開發一個內置Tendermint Core 共識引擎的區塊鏈應用,源代碼可以從github下載,如果希望進一步系統 學習Tendermint的應用開發,推薦匯智網的Tendermint區塊鏈開發詳解。


原文鏈接:Tendermint Core應用開發指南 —— 匯智網


分享到:


相關文章: