天天看點

Tendermint Core Go語言鍊碼與應用開發教程

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:

$ go version
go version go1.12.7 darwin/amd64           

确認你正确設定了

GOPATH

環境變量:

$ echo $GOPATH
/Users/melekes/go           

2、建立Go項目

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

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

example

目錄建立

main.go

檔案,内容如下:

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello, Tendermint Core")
}           

運作上面代碼,将在标準輸出裝置顯示指定的字元串:

$ go run main.go
Hello, Tendermint Core           

3、編寫Tendermint Core應用

Tendermint Core與應用之間通過ABCI(Application Blockchain Interface)通信,使用的封包消息類型都定義在protobuf檔案中,是以基于Tendermint Core可以運作任何語言開發的應用。

建立檔案

app.go

,内容如下:

package main
import (
    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{}
}           

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

3、CheckTx

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

func (app *KVStoreApplication) isValid(tx []byte) (code uint32) {
    // check format
    parts := bytes.Split(tx, []byte("="))
    if len(parts) != 2 {
        return 1
    }    
  
  key, value := parts[0], parts[1]    
  
  // check if the same key=value already exists
    err := 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}
}           

如果進來的交易格式不是

{bytes}={bytes}

,我們将傳回代碼

1

。如果指定的key和value已經存在,我們傳回代碼

2

。對于其他情況我們傳回代碼

表示交易有效 —— 注意Tendermint Core會将傳回任何非零代碼的交易視為無效交易。

有效的交易最終将被送出,我們使用

badger

作為底層的鍵/值庫,badger是一個嵌入式的快速KV資料庫。

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

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

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

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

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

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

下面的代碼建立一個資料操作批次,用來存儲區塊交易:

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

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

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

注意,我們不能在DeliverTx中送出交易,因為在這種情況下Query可能會由于被并發調用而傳回不一緻的資料,例如,Query會提示指定的值已經存在,而實際的區塊還沒有真正送出。

Commit

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

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

5、查詢 - Query

當用戶端應用希望了解指定的鍵/值對是否存在時,它會調用Tendermint Core 的RPC接口

/abci_query

進行查詢,該接口會調用應用的

Query

方法。

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

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

func (app *KVStoreApplication) Query(reqQuery abcitypes.RequestQuery) (resQuery abcitypes.ResponseQuery) {
    resQuery.Key = reqQuery.Data
    err := 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 = val
                return nil
            })
        }
        return nil
    })
    if err != nil {
        panic(err)
    }
    return
}           

6、在同一程序内啟動Tendermint Core和應用執行個體

将以下代碼加入

main.go

檔案:

package main
import (
    "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 string

func 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)
    <-c
    os.Exit(0)
}

func newTendermint(app abci.Application, configFile string) (*nm.Node, error) {
    // read config
    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")
    }    
  
  // create logger
    logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))
    var err error
    logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel())
    if err != nil {
        return nil, errors.Wrap(err, "failed to parse log level")
    }    
  
  // read private validator
    pv := privval.LoadFilePV(
        config.PrivValidatorKeyFile(),
        config.PrivValidatorStateFile(),
    )    
  
  // read node key
    nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile())
    if err != nil {
        return nil, errors.Wrap(err, "failed to load node's key")
    }    
  
  // create node
    node, 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")
    }    
  
  return node, nil
}           

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

首先,初始化Badger資料庫,然後建立應用執行個體:

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)           

接下來使用下面的代碼建立Tendermint Core的Node執行個體:

flag.Parse()node, err := newTendermint(app, configFile)
if err != nil {
    fmt.Fprintf(os.Stderr, "%v", err)
    os.Exit(2)
}

...

// create node
node, 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")
}           

NewNode

方法用來建立一個全節點執行個體,它需要傳入一些參數,例如配置檔案、私有驗證器、節點密鑰等。

注意我們使用

proxy.NewLocalClientCreator

來建立一個本地用戶端,而不是使用套接字或gRPC來與Tendermint Core通信。

下面的代碼使用

viper

來讀取配置檔案,我們将在下面使用tendermint的init指令來生成。

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

我們使用

FilePV

作為私有驗證器,通常你應該使用SignerRemote連結到一個外部的HSM裝置。

pv := privval.LoadFilePV(
    config.PrivValidatorKeyFile(),
    config.PrivValidatorStateFile(),
)           

nodeKey

用來在Tendermint的P2P網絡中辨別目前節點。

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

我們使用内置的日志記錄器:

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

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

node.Start()
defer func() {
    node.Stop()
    node.Wait()
}()

c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-c
os.Exit(0)           

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

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

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

上面的指令将解析項目依賴并執行建構過程。

要建立預設的配置檔案,可以執行

tendermint init

指令。但是在開始之前,我們需要安裝Tendermint Core。

$ rm -rf /tmp/example
$ cd $GOPATH/src/github.com/tendermint/tendermint
$ make install
$ TMHOME="/tmp/example" tendermint init

I[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.json
I[2019-07-16|18:40:36.481] Generated node key                           module=main path=/tmp/example/config/node_key.json
I[2019-07-16|18:40:36.482] Generated genesis file                       module=main path=/tmp/example/config/genesis.json           

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

$ ./demo -config "/tmp/example/config/config.toml"

badger 2019/07/16 18:42:25 INFO: All 0 tables opened in 0s
badger 2019/07/16 18:42:25 INFO: Replaying file id: 0 at offset: 0
badger 2019/07/16 18:42:25 INFO: Replay took: 695.227s

E[2019-07-16|18:42:25.818] Couldn't connect to any seeds                module=p2p
I[2019-07-16|18:42:26.853] Executed block                               module=state height=1 validTxs=0 invalidTxs=0
I[2019-07-16|18:42:26.865] Committed state                              module=state height=1 txs=0 appHash=           

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

$ 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"
  }
}           

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

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

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

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

8、小結

在這個指南中,我們學習了如何使用Go開發一個内置Tendermint Core 共識引擎的區塊鍊應用,源代碼可以

從github下載下傳

,如果希望進一步系統學習Tendermint的應用開發,推薦

Tendermint區塊鍊開發詳解

原文連結:

Tendermint Core應用開發指南 - 彙智網