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

相关链接: Tendermint 区块链开发详解 | 本教程源代码下载

1、安装Go开发环境

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

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

1$ go version 2go version go1.12.7 darwin/amd64

确认你正确设置了GOPATH环境变量:

1$ echo $GOPATH 2/Users/melekes/go

2、创建Go项目

首先创建一个新的Go语言项目:

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

example目录创建main.go文件,内容如下:

1package main 2 3import ( 4 "fmt" 5) 6 7func main() { 8 fmt.Println("Hello, Tendermint Core") 9}

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

1$ go run main.go 2Hello, Tendermint Core

3、编写Tendermint Core应用

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

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

1package main 2import ( 3 abcitypes "github.com/tendermint/tendermint/abci/types" 4) 5 6type KVStoreApplication struct {} 7 8var _ abcitypes.Application = (*KVStoreApplication)(nil) 9 10func NewKVStoreApplication() *KVStoreApplication { 11 return &KVStoreApplication{} 12} 13 14func (KVStoreApplication) Info(req abcitypes.RequestInfo) abcitypes.ResponseInfo { 15 return abcitypes.ResponseInfo{} 16} 17 18func (KVStoreApplication) SetOption(req abcitypes.RequestSetOption) abcitypes.ResponseSetOption { 19 return abcitypes.ResponseSetOption{} 20} 21 22func (KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { 23 return abcitypes.ResponseDeliverTx{Code: 0} 24} 25 26func (KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { 27 return abcitypes.ResponseCheckTx{Code: 0} 28} 29 30func (KVStoreApplication) Commit() abcitypes.ResponseCommit { 31 return abcitypes.ResponseCommit{} 32} 33 34func (KVStoreApplication) Query(req abcitypes.RequestQuery) abcitypes.ResponseQuery { 35 return abcitypes.ResponseQuery{Code: 0} 36} 37 38func (KVStoreApplication) InitChain(req abcitypes.RequestInitChain) abcitypes.ResponseInitChain { 39 return abcitypes.ResponseInitChain{} 40} 41 42func (KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { 43 return abcitypes.ResponseBeginBlock{} 44} 45 46func (KVStoreApplication) EndBlock(req abcitypes.RequestEndBlock) abcitypes.ResponseEndBlock { 47 return abcitypes.ResponseEndBlock{} 48}

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

3、CheckTx

当一个新的交易进入Tendermint Core时,它会要求应用先进行检查,比如验证格式、签名等。

1func (app *KVStoreApplication) isValid(tx []byte) (code uint32) { 2 // check format 3 parts := bytes.Split(tx, []byte("=")) 4 if len(parts) != 2 { 5 return 1 6 } 7 8 key, value := parts[0], parts[1] 9 10 // check if the same key=value already exists 11 err := app.db.View(func(txn *badger.Txn) error { 12 item, err := txn.Get(key) 13 if err != nil && err != badger.ErrKeyNotFound { 14 return err 15 } 16 if err == nil { 17 return item.Value(func(val []byte) error { 18 if bytes.Equal(val, value) { 19 code = 2 20 } 21 return nil 22 }) 23 } 24 return nil 25 }) 26 27 if err != nil { 28 panic(err) 29 } 30 31 return code 32} 33 34func (app *KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { 35 code := app.isValid(req.Tx) 36 return abcitypes.ResponseCheckTx{Code: code, GasWanted: 1} 37}

如果进来的交易格式不是{bytes}={bytes},我们将返回代码1。如果指定的key和value已经存在,我们返回代码2。对于其他情况我们返回代码0表示交易有效 —— 注意Tendermint Core会将返回任何非零代码的交易视为无效交易。

有效的交易最终将被提交,我们使用badger 作为底层的键/值库,badger是一个嵌入式的快速KV数据库。

1import "github.com/dgraph-io/badger"type KVStoreApplication struct { 2 db *badger.DB 3 currentBatch *badger.Txn 4}func NewKVStoreApplication(db *badger.DB) *KVStoreApplication { 5 return &KVStoreApplication{ 6 db: db, 7 } 8}

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

当Tendermint Core确定了新的区块后,它会分三次调用应用:

  • BeginBlock:区块开始时调用
  • DeliverTx:每个交易时调用
  • EndBlock:区块结束时调用

注意,DeliverTx是异步调用的,但是响应是有序的。

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

下面的代码创建一个数据操作批次,用来存储区块交易:

1func (app *KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { 2 code := app.isValid(req.Tx) 3 if code != 0 { 4 return abcitypes.ResponseDeliverTx{Code: code} 5 } 6 parts := bytes.Split(req.Tx, []byte("=")) 7 8 key, value := parts[0], parts[1] 9 10 err := app.currentBatch.Set(key, value) 11 if err != nil { 12 panic(err) 13 } 14 15 return abcitypes.ResponseDeliverTx{Code: 0} 16}

如果交易的格式错误,或者已经存在相同的键/值对,那么我们仍然返回非零代码,否则,我们将该交易加入操作批次。

在目前的设计中,区块中可以包含不正确的交易 —— 那些通过了CheckTx检查但是DeliverTx失败的交易,这样做是出于性能的考虑。

注意,我们不能在DeliverTx中提交交易,因为在这种情况下Query可能会由于被并发调用而返回不一致的数据,例如,Query会提示指定的值已经存在,而实际的区块还没有真正提交。

Commit用来通知应用来持久化新的状态。

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

5、查询 - Query

当客户端应用希望了解指定的键/值对是否存在时,它会调用Tendermint Core 的RPC接口 /abci_query 进行查询,该接口会调用应用的Query方法。

基于Tendermint Core的应用可以自由地提供其自己的API。不过使用Tendermint Core 作为代理,客户端应用利用Tendermint Core的统一API的优势。另外,客户端也不需要调用其他额外的Tendermint Core API来获得进一步的证明。

注意在下面的代码中我们没有包含证明数据。

1func (app *KVStoreApplication) Query(reqQuery abcitypes.RequestQuery) (resQuery abcitypes.ResponseQuery) { 2 resQuery.Key = reqQuery.Data 3 err := app.db.View(func(txn *badger.Txn) error { 4 item, err := txn.Get(reqQuery.Data) 5 if err != nil && err != badger.ErrKeyNotFound { 6 return err 7 } 8 if err == badger.ErrKeyNotFound { 9 resQuery.Log = "does not exist" 10 } else { 11 return item.Value(func(val []byte) error { 12 resQuery.Log = "exists" 13 resQuery.Value = val 14 return nil 15 }) 16 } 17 return nil 18 }) 19 if err != nil { 20 panic(err) 21 } 22 return 23}

6、在同一进程内启动Tendermint Core和应用实例

将以下代码加入main.go文件:

1package main 2import ( 3 "flag" 4 "fmt" 5 "os" 6 "os/signal" 7 "path/filepath" 8 "syscall" 9 "github.com/dgraph-io/badger" 10 "github.com/pkg/errors" 11 "github.com/spf13/viper" 12 abci "github.com/tendermint/tendermint/abci/types" 13 cfg "github.com/tendermint/tendermint/config" 14 tmflags "github.com/tendermint/tendermint/libs/cli/flags" 15 "github.com/tendermint/tendermint/libs/log" 16 nm "github.com/tendermint/tendermint/node" 17 "github.com/tendermint/tendermint/p2p" 18 "github.com/tendermint/tendermint/privval" 19 "github.com/tendermint/tendermint/proxy" 20) 21var configFile string 22 23func init() { 24 flag.StringVar(&configFile, "config", "$HOME/.tendermint/config/config.toml", "Path to config.toml") 25} 26 27func main() { 28 db, err := badger.Open(badger.DefaultOptions("/tmp/badger")) 29 if err != nil { 30 fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err) 31 os.Exit(1) 32 } 33 defer db.Close() 34 35 app := NewKVStoreApplication(db) 36 37 flag.Parse() 38 39 node, err := newTendermint(app, configFile) 40 if err != nil { 41 fmt.Fprintf(os.Stderr, "%v", err) 42 os.Exit(2) 43 } 44 45 node.Start() 46 47 defer func() { 48 node.Stop() 49 node.Wait() 50` }() 51 52 c := make(chan os.Signal, 1) 53 signal.Notify(c, os.Interrupt, syscall.SIGTERM) 54 <-c 55 os.Exit(0) 56} 57 58func newTendermint(app abci.Application, configFile string) (*nm.Node, error) { 59 // read config 60 config := cfg.DefaultConfig() 61 config.RootDir = filepath.Dir(filepath.Dir(configFile)) 62 viper.SetConfigFile(configFile) 63 if err := viper.ReadInConfig(); err != nil { 64 return nil, errors.Wrap(err, "viper failed to read config file") 65 } 66 if err := viper.Unmarshal(config); err != nil { 67 return nil, errors.Wrap(err, "viper failed to unmarshal config") 68 } 69 if err := config.ValidateBasic(); err != nil { 70 return nil, errors.Wrap(err, "config is invalid") 71 } 72 73 // create logger 74 logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) 75 var err error 76 logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel()) 77 if err != nil { 78 return nil, errors.Wrap(err, "failed to parse log level") 79 } 80 81 // read private validator 82 pv := privval.LoadFilePV( 83 config.PrivValidatorKeyFile(), 84 config.PrivValidatorStateFile(), 85 ) 86 87 // read node key 88 nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile()) 89 if err != nil { 90 return nil, errors.Wrap(err, "failed to load node's key") 91 } 92 93 // create node 94 node, err := nm.NewNode( 95 config, 96 pv, 97 nodeKey, 98 proxy.NewLocalClientCreator(app), 99 nm.DefaultGenesisDocProviderFunc(config), 100 nm.DefaultDBProvider, 101 nm.DefaultMetricsProvider(config.Instrumentation), 102 logger) 103 if err != nil { 104 return nil, errors.Wrap(err, "failed to create new Tendermint node") 105 } 106 107 return node, nil 108}

这段代码很长,让我们分开来介绍。

首先,初始化Badger数据库,然后创建应用实例:

1db, err := badger.Open(badger.DefaultOptions("/tmp/badger")) 2if err != nil { 3 fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err) 4 os.Exit(1) 5} 6defer db.Close() 7app := NewKVStoreApplication(db)

接下来使用下面的代码创建Tendermint Core的Node实例:

1flag.Parse()node, err := newTendermint(app, configFile) 2if err != nil { 3 fmt.Fprintf(os.Stderr, "%v", err) 4 os.Exit(2) 5} 6 7... 8 9// create node 10node, err := nm.NewNode( 11 config, 12 pv, 13 nodeKey, 14 proxy.NewLocalClientCreator(app), 15 nm.DefaultGenesisDocProviderFunc(config), 16 nm.DefaultDBProvider, 17 nm.DefaultMetricsProvider(config.Instrumentation), 18 logger) 19 20if err != nil { 21 return nil, errors.Wrap(err, "failed to create new Tendermint node") 22}

NewNode方法用来创建一个全节点实例,它需要传入一些参数,例如配置文件、私有验证器、节点密钥等。

注意我们使用proxy.NewLocalClientCreator来创建一个本地客户端,而不是使用套接字或gRPC来与Tendermint Core通信。

下面的代码使用viper来读取配置文件,我们将在下面使用tendermint的init命令来生成。

1config := cfg.DefaultConfig() 2config.RootDir = filepath.Dir(filepath.Dir(configFile)) 3viper.SetConfigFile(configFile) 4if err := viper.ReadInConfig(); err != nil { 5 return nil, errors.Wrap(err, "viper failed to read config file") 6} 7if err := viper.Unmarshal(config); err != nil { 8 return nil, errors.Wrap(err, "viper failed to unmarshal config") 9} 10if err := config.ValidateBasic(); err != nil { 11 return nil, errors.Wrap(err, "config is invalid") 12}

我们使用FilePV作为私有验证器,通常你应该使用SignerRemote链接到一个外部的HSM设备。

1pv := privval.LoadFilePV( 2 config.PrivValidatorKeyFile(), 3 config.PrivValidatorStateFile(), 4)

nodeKey用来在Tendermint的P2P网络中标识当前节点。

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

我们使用内置的日志记录器:

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

最后,我们启动节点并添加一些处理逻辑,以便在收到SIGTERM或Ctrl-C时可以优雅地关闭。

1node.Start() 2defer func() { 3 node.Stop() 4 node.Wait() 5}() 6 7c := make(chan os.Signal, 1) 8signal.Notify(c, os.Interrupt, syscall.SIGTERM) 9<-c 10os.Exit(0)

7、项目依赖管理、构建、配置生成和启动

我们使用go module进行项目依赖管理:

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

上面的命令将解析项目依赖并执行构建过程。

要创建默认的配置文件,可以执行tendermint init命令。但是在开始之前,我们需要安装Tendermint Core。

1$ rm -rf /tmp/example 2$ cd $GOPATH/src/github.com/tendermint/tendermint 3$ make install 4$ TMHOME="/tmp/example" tendermint init 5 6I[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 7I[2019-07-16|18:40:36.481] Generated node key module=main path=/tmp/example/config/node_key.json 8I[2019-07-16|18:40:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json

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

1$ ./demo -config "/tmp/example/config/config.toml" 2 3badger 2019/07/16 18:42:25 INFO: All 0 tables opened in 0s 4badger 2019/07/16 18:42:25 INFO: Replaying file id: 0 at offset: 0 5badger 2019/07/16 18:42:25 INFO: Replay took: 695.227s 6 7E[2019-07-16|18:42:25.818] Couldn't connect to any seeds module=p2p 8I[2019-07-16|18:42:26.853] Executed block module=state height=1 validTxs=0 invalidTxs=0 9I[2019-07-16|18:42:26.865] Committed state module=state height=1 txs=0 appHash=

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

1$ curl -s 'localhost:26657/broadcast_tx_commit?tx="tendermint=rocks"' 2{ 3 "jsonrpc": "2.0", 4 "id": "", 5 "result": { 6 "check_tx": { 7 "gasWanted": "1" 8 }, 9 "deliver_tx": {}, 10 "hash": "1B3C5A1093DB952C331B1749A21DCCBB0F6C7F4E0055CD04D16346472FC60EC6", 11 "height": "128" 12 } 13}

响应中应当会包含交易提交的区块高度。

现在让我们检查指定的键是否存在并返回其对应的值:

1$ curl -s 'localhost:26657/abci_query?data="tendermint"' 2{ 3 "jsonrpc": "2.0", 4 "id": "", 5 "result": { 6 "response": { 7 "log": "exists", 8 "key": "dGVuZGVybWludA==", 9 "value": "cm9ja3M=" 10 } 11 } 12}

“dGVuZGVybWludA==” 和“cm9ja3M=” 都是base64编码的,分别对应于“tendermint” 和“rocks” 。

8、小结

在这个指南中,我们学习了如何使用Go开发一个内置Tendermint Core 共识引擎的区块链应用,源代码可以从github下载,如果希望进一步系统学习Tendermint的应用开发,推荐Tendermint区块链开发详解


原文链接:Tendermint Core应用开发指南 - 汇智网

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )