Tendermint 应用开发 - 全流程指南
注: 本教程大部分内容根据官方文档翻译得到,并添加少部分讲解.
官方文档: https://docs.tendermint.com/
默认版本为: Tendermint v0.34.24
默认操作系统: Ubuntu Server 20.04 LTS
一. Tendermint概述
1.1 Tendermint 定义
Tendermint: Tendermint is software for securely and consistently replicating an application on many machines.
Tendermint是用于在许多机器上安全、一致地 复制 应用程序 的软件。
- 安全性
多达1/3的机器以任意方式出现故障,Tendermint也能正常工作。- 一致性
每台无故障的机器都会看到相同的事务日志并计算相同的状态。
1.2 Tendermint 组成
Tendermint consists of two chief technical components: a blockchain consensus engine and a generic application interface
**Tendermint组成:**区块链共识引擎(Tendermint Core)+ 通用应用程序接口(ABCI)。
- **Tendermint Core: **Tendermint Core performs Byzantine Fault Tolerant (BFT) State Machine Replication (SMR) for arbitrary deterministic, finite state machines.
Tendermint Core对任意确定的 状态机 执行拜占庭容错(BFT)状态机复制(SMR)。
**有限状态机:**指的是 应用程序.
Tendermint Core另一种定义:
Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a **state transition machine **- written in any programming language - and securely replicates it on many machines.
- **ABCI(Application BlockChain Interface): **
Tendermint is able to decompose the blockchain design by offering a very simple API (i.e. the ABCI) between the application process and consensus process.
ABCI(Application Blockchain Interface)是Tendermint使用的一种接口协议规范,用于Tendermint Core(共识引擎)与应用程序之间的交互。
Tendermint能够通过在应用程序进程和共识进程之间提供一个简单的API(即ABCI)来分解区块链设计。
1.3 Tendermint实现区块链应用, 组件的功能
-
Tendermint Core:
-
要在节点间传播区块和交易
-
建立规范(不可变)的交易顺序
-
-
Application:
-
维护存放交易的数据库
-
验证交易签名
-
防止双花
-
查询交易
-
-
ABCI :
- 提供一个接口规范, 使得开发者可以开发出可以与Tendermint Core交互的 应用程序.
1.4 Tendermint 共识流程概述
Tendermint是一种易于理解的、大部分是异步的拜占庭容错(BFT)共识协议。该协议遵循一个简单的状态机模型,如下所示:
**共识逻辑: **
在该协议中,参与者被称为验证者;他们轮流提出交易块并对其进行投票。每个高度上都有一个区块链中的区块被确认。如果一个区块无法被确认,协议将进入下一个轮次,新的验证者将为该高度提出一个新的区块。为了成功确认一个区块,需要进行两个阶段的投票,我们称之为pre-vote和pre-commit。当超过2/3的验证者在同一轮次中pre-commit同一个区块时,该区块被确认。
1.5 ABCI的消息类型
ABCI(Application Blockchain Interface)是Tendermint使用的一种接口协议规范,用于Tendermint Core(共识引擎)与应用程序之间的交互。Tendermint能够通过在应用程序进程和共识进程之间提供一个简单的API(即ABCI)来分解区块链设计。
ABCI由三种主要的消息类型组成,这些消息从核心传递到应用程序,应用程序则回复相应的响应消息。这些消息在ABCI规范中进行了详细说明。
-
DeliverTx: 每个区块链中的交易都通过这个消息传递给应用程序。应用程序需要使用DeliverTx消息对接收到的每个交易进行验证,包括验证当前状态、应用程序协议以及交易的加密凭证。
- CheckTx消息与DeliverTx类似,但仅用于验证交易。Tendermint Core的内存池首先使用CheckTx验证交易的有效性,只将有效的交易传递给其他节点。例如,应用程序可以检查交易中的递增序列号,如果序列号过旧,就在CheckTx阶段返回错误。或者,它们可能使用基于能力的系统,要求每个交易都需要重新获得能力。
-
Commit消息用于计算对当前应用程序状态的加密承诺,并将其放入下一个区块头中。这具有一些便利的属性。对应用程序状态的不一致性现在会呈现为区块链分叉,从而捕捉到一整类编程错误。这还简化了安全轻量级客户端的开发,因为可以通过校验默克尔哈希证明与区块哈希进行对比,并验证区块哈希是否由一个法定人数签名。
一个应用程序可以与多个ABCI socket连接。Tendermint Core会创建三个ABCI连接到应用程序:一个用于验证内存池中的交易广播,一个用于共识引擎运行块提案,还有一个用于查询应用程序状态。以下图表描述了消息通过ABCI的流程。
1.6 Tendermint 技术概览图
TODO
1.7 Tendermint 质量保证( 性能测试)
可参见: https://docs.tendermint.com/v0.34/qa/#
二. Tendermint 部署与启动
[注]:
- 官方文档中,提供一个快速安装的脚本,但是该脚本中使用的go版本为1.10,且仅供一个全新的 Ubuntu 16.04 实例, 不便于我们后续学习和部署, 所以不使用.
- 从二进制文件安装, 仅能安装官方编译好的二进制文件, 我们无法进行二次开发, 所以也不使用.
2.1 从源代码安装Tendermint
2.1.1安装Go语言环境
- 安装Go
参考: https://go.dev/doc/install
-
设置环境变量
echo export GOPATH=\"\$HOME/go\" >> ~/.bashrc echo export PATH=\"\$PATH:\$GOPATH/bin\" >> ~/.bashrc
[注]:
后续make build的时候,可能会出现报错, 这是go代理 和 本地网络等多种原因造成的,可以尝试:
# 设置go 中国代理: unset GOPROXY export GOPROXY=https://goproxy.cn # 取消git 代理 git config --global --unset http.proxy git config --global --unset https.proxy # 取消本机的http代理和https代理 unset http_proxy unset https_proxy # 最后重启机器再次尝试
2.1.2 获取Tendermint 源码
git clone -b v0.34.24 https://github.com/tendermint/tendermint.git
2.1.3 编译
cd tendermint
make build
2.1.4 测试是否安装成功
tendermint version
2.2 启动Tendermint
- 单节点启动
tendermint init
tendermint node --proxy_app=kvstore
[注]:
如果报错为无法初始化, 认证信息已经存在, 那么可能是之前初始化过, 需要:
rm -rf ~/.tendermint
后续的应用开发中, 会修改该配置文件的位置, 注意每次启动所对应的配置文件.
- 单节点测试
curl -s 'localhost:26657/broadcast_tx_commit?tx="cdd=hello"'
curl -s 'localhost:26657/abci_query?data="cdd"'
curl -s 'localhost:26657/broadcast_tx_commit?tx="cdd=world"'
curl -s 'localhost:26657/abci_query?data="xyz"'
2.3 编译CLevelDB支持
TODO
https://docs.tendermint.com/v0.34/introduction/install.html
2.4 多机部署
TODO
三. 创建一个Tendermint 内置应用程序 示例
通过遵循本指南,您将创建一个名为kvstore的Tendermint Core项目,这是一个(非常)简单的分布式BFT键值存储。
内置应用程序与外部应用程序 对比:
- 内置应用程序: 在与Tendermint Core 相同的进程中 运行 应用程序 将为您提供 尽可能好的性能。
- 外部应用程序: 对于外部应用程序,必须通过TCP、Unix域套接字或gRPC与Tendermint Core进行通信。
3.1 创建一个应用程序项目
- 创建项目
mkdir tendermint-app
cd tendermint-app
- 创建包
创建一个app.go文件, 你可以将其放置在main包下
mkdir main
cd main
- 创建应用程序代码文件
vim app.go
app.go:
package main
import (
"bytes"
"github.com/dgraph-io/badger"
abcitypes "github.com/tendermint/tendermint/abci/types"
)
type KVStoreApplication struct {
db *badger.DB
currentBatch *badger.Txn
}
var _ abcitypes.Application = (*KVStoreApplication)(nil)
func NewKVStoreApplication(db *badger.DB) *KVStoreApplication {
return &KVStoreApplication{
db: db,
}
}
func (KVStoreApplication) Info(req abcitypes.RequestInfo) abcitypes.ResponseInfo {
return abcitypes.ResponseInfo{}
}
func (KVStoreApplication) SetOption(req abcitypes.RequestSetOption) abcitypes.ResponseSetOption {
return abcitypes.ResponseSetOption{}
}
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}
}
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}
}
func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit {
app.currentBatch.Commit()
return abcitypes.ResponseCommit{Data: []byte{}}
}
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
}
func (KVStoreApplication) InitChain(req abcitypes.RequestInitChain) abcitypes.ResponseInitChain {
return abcitypes.ResponseInitChain{}
}
func (app *KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock {
app.currentBatch = app.db.NewTransaction(true)
return abcitypes.ResponseBeginBlock{}
}
func (KVStoreApplication) EndBlock(req abcitypes.RequestEndBlock) abcitypes.ResponseEndBlock {
return abcitypes.ResponseEndBlock{}
}
func (KVStoreApplication) ListSnapshots(abcitypes.RequestListSnapshots) abcitypes.ResponseListSnapshots {
return abcitypes.ResponseListSnapshots{}
}
func (KVStoreApplication) OfferSnapshot(abcitypes.RequestOfferSnapshot) abcitypes.ResponseOfferSnapshot {
return abcitypes.ResponseOfferSnapshot{}
}
func (KVStoreApplication) LoadSnapshotChunk(abcitypes.RequestLoadSnapshotChunk) abcitypes.ResponseLoadSnapshotChunk {
return abcitypes.ResponseLoadSnapshotChunk{}
}
func (KVStoreApplication) ApplySnapshotChunk(abcitypes.RequestApplySnapshotChunk) abcitypes.ResponseApplySnapshotChunk {
return abcitypes.ResponseApplySnapshotChunk{}
}
在1.5小结将结果ABCI的消息类型, 现在将针对每种消息类型分类解释每个方法.
现在,我将解释每个方法,解释何时调用它,并添加所需的业务逻辑。
3.2 CheckTx
当一个新的事务被添加到Tendermint Core时,它会要求应用程序对其进行检查(验证格式、签名等)。
在app.go中, CheckTx 方法用于检查该事务是否合法, 该方法调用isValid方法分析参数req是否合法.
import "bytes"
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}
}
isValid方法核心思想:
如果事务没有{字节}={字节}的形式,我们返回1个代码。当已经存在相同的键=值(相同的键和值)时,我们返回2个代码。对于其他代码,我们返回一个零代码,表示它们是有效的。
请注意,任何具有非零代码的东西都将被Tendermint Core视为无效(-1、100等)。
对于底层键值存储,我们将使用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,
}
}
3.3 BeginBlock -> DeliverTx -> EndBlock -> Commit
当Tendermint Core决定出块时,它将分三部分传送到应用程序:BeginBlock,DeliverTx,最后是EndBlock。
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}
}
如果事务的格式不正确,或者已经存在相同的key=value,我们将再次返回非零代码。否则,我们将其添加到当前批次中。
在当前设计中,区块可能包括不正确的交易(通过CheckTx但未通过DeliverTx的交易或提案人直接包括的交易)。这样做是出于性能原因。
请注意,我们不能在DeliverTx内提交事务,因为在这种情况下,可以并行调用的Query将返回不一致的数据(即,即使实际块尚未提交,它也会报告某些值已经存在)。
Commit指示应用程序保持新状态。
func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit {
app.currentBatch.Commit()
return abcitypes.ResponseCommit{Data: []byte{}}
}
3.4 Query
当客户端想要知道某个特定的键/值何时存在时,它将调用Tendermint Core RPC/abci_query端点,后者将依次调用应用程序的query方法。
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
}
3.5 在一个进程中, 启动一个应用程序和一个Tendermint Core 实例
创建一个main.go文件,将以下代码放入.
package main
import (
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"github.com/dgraph-io/badger"
"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, fmt.Errorf("viper failed to read config file: %w", err)
}
if err := viper.Unmarshal(config); err != nil {
return nil, fmt.Errorf("viper failed to unmarshal config: %w", err)
}
if err := config.ValidateBasic(); err != nil {
return nil, fmt.Errorf("config is invalid: %w", err)
}
// 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, fmt.Errorf("failed to parse log level: %w", err)
}
// read private validator
pv := privval.LoadFilePV(
config.PrivValidatorKeyFile(),
config.PrivValidatorStateFile(),
)
// read node key
nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile())
if err != nil {
return nil, fmt.Errorf("failed to load node's key: %w", err)
}
// 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, fmt.Errorf("failed to create new Tendermint node: %w", err)
}
return node, nil
}
3.5.1 代码拆分讲解
- 首先,我们初始化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)
如果是Windows, 则需要设置:
db, err := badger.Open(badger.DefaultOptions("/tmp/badger").WithTruncate(true))
对于Windows用户来说,重新启动此应用程序将导致badger抛出错误,因为它需要截断值日志。可以通过将truncate选项设置为true来避免这种情况.
- 然后我们创建一个Tendermint核心节点实例:
flag.Parse()
node, err := newTendermint(app, configFile)
if err != nil {
fmt.Fprintf(os.Stderr, "%v", err)
os.Exit(2)
}
- 最后,我们启动节点, 并添加一些信号处理,以便在接收到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)
3.5.2 newTendermint的具体流程
其中,newTendermint的具体流程如下:
①. 读取配置文件
viper(打开新窗口)用于读取配置,我们稍后将使用tendermint-init命令生成该配置。
// read config
config := cfg.DefaultConfig()
config.RootDir = filepath.Dir(filepath.Dir(configFile))
viper.SetConfigFile(configFile)
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("viper failed to read config file: %w", err)
}
if err := viper.Unmarshal(config); err != nil {
return nil, fmt.Errorf("viper failed to unmarshal config: %w", err)
}
if err := config.ValidateBasic(); err != nil {
return nil, fmt.Errorf("config is invalid: %w", err)
}
②. 创建日志模块
使用内置库来创建日志模块
// 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, fmt.Errorf("failed to parse log level: %w", err)
}
③. 读取验证器
我们使用FilePV,它是一个私有验证器(即签署共识消息的东西)。通常,您会使用SignerRemote连接到外部HSM
// read private validator
pv := privval.LoadFilePV(
config.PrivValidatorKeyFile(),
config.PrivValidatorStateFile(),
)
④.读取nodeKey
nodeKey用于识别p2p网络中的节点。
// read node key
nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile())
if err != nil {
return nil, fmt.Errorf("failed to load node's key: %w", err)
}
⑤.创建节点
NewNode需要一些东西,包括配置文件、专用验证器、节点密钥和其他一些东西,才能构建完整的节点。
请注意,我们在这里使用proxy.NewLocalClientCreator来创建本地客户端,而不是通过套接字或gRPC进行通信的客户端。
// 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, fmt.Errorf("failed to create new Tendermint node: %w", err)
}
return node, nil
3.6 编译和启动 应用程序
- 获取依赖包, 将所有依赖打包进项目
cd tendermint-app
go mod init
go get github.com/tendermint/tendermint/@v0.34.24
go mod tidy
go mod vendor
- 编译
cd main
go build
编译成功后,会输出 main 的可执行文件.
- 初始化tendermint 配置文件
rm -rf /tmp/example
TMHOME="/tmp/example" tendermint init
- 启动
./main -config "/tmp/example/config/config.toml"
- 发送交易,测试
curl -s 'localhost:26657/broadcast_tx_commit?tx="cdd=hello"'
curl -s 'localhost:26657/abci_query?data="cdd"'
curl -s 'localhost:26657/broadcast_tx_commit?tx="cdd=world"'
curl -s 'localhost:26657/abci_query?data="xyz"'
- 具体的更多的ABCI规范:
https://github.com/tendermint/tendermint/tree/v0.34.x/spec/abci/
- Tendermint Core 保持三个连接:
零. TODO
-
编译CLevelDB支持
-
多机部署
-
编写自己的应用程序代码逻辑, 部署执行测试.
-
Docker-compose 部署 test network
-
将mysql 挂到应用下面,尝试编写逻辑。