千家信息网

Go语言Tendermint Core开发方法是什么

发表于:2024-11-23 作者:千家信息网编辑
千家信息网最后更新 2024年11月23日,本篇内容主要讲解"Go语言Tendermint Core开发方法是什么",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"Go语言Tendermint Core
千家信息网最后更新 2024年11月23日Go语言Tendermint Core开发方法是什么

本篇内容主要讲解"Go语言Tendermint Core开发方法是什么",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"Go语言Tendermint Core开发方法是什么"吧!

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运行在同一进程中,因此可以得到最好的性能。

1、安装Go开发环境

请参考官方文档安装Go开发环境。

确认你已经安装了最新版的Go:

$ go versiongo 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 mainimport (        "fmt")func main() {        fmt.Println("Hello, Tendermint Core")}

运行上面代码,将在标准输出设备显示指定的字符串:

$ go run main.goHello, Tendermint Core

3、编写Tendermint Core应用

Tendermint Core与应用之间通过ABCI(Application Blockchain Interface)通信,使用的报文消息类型都定义在protobuf文件中,因此基于Tendermint Core可以运行任何语言开发的应用。

创建文件app.go,内容如下:

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

接下来我们逐个解读上述方法并添加必要的实现逻辑。

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。对于其他情况我们返回代码0表示交易有效 -- 注意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 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)        <-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 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")}

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 errorlogger, 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)<-cos.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 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

现在可以启动我们的一体化Tendermint Core应用了:

$ ./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=

现在可以打开另一个终端,尝试发送一个交易:

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

到此,相信大家对"Go语言Tendermint Core开发方法是什么"有了更深的了解,不妨来实际操作一番吧!这里是网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

0